using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; namespace SlamCross.Services; public record AudioDevice(int Index, string Name, string PactlId = ""); public interface IAudioService : IDisposable { List GetOutputDevices(); List GetInputDevices(); void Play(string filePath, int deviceIndex, float volume = 1.0f, int startSampleBytes = 0, int endSampleBytes = 0, string pactlId = ""); void Stop(); } public class LinuxAudioService : IAudioService { private Process? _ffmpegProc; private Process? _pacatProc; public List GetOutputDevices() => ParsePactl("sinks"); public List GetInputDevices() => ParsePactl("sources"); public void Play(string filePath, int deviceIndex, float volume = 1.0f, int startSampleBytes = 0, int endSampleBytes = 0, string pactlId = "") { Stop(); string sinkId = pactlId; if (string.IsNullOrEmpty(sinkId)) { var devices = GetOutputDevices(); sinkId = deviceIndex >= 0 && deviceIndex < devices.Count ? devices[deviceIndex].PactlId : ""; } var absPath = Path.GetFullPath(filePath); var ffmpegPsi = new ProcessStartInfo("ffmpeg") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; if (startSampleBytes > 0) { ffmpegPsi.ArgumentList.Add("-ss"); ffmpegPsi.ArgumentList.Add(BytesToSec(startSampleBytes).ToString("F5")); } ffmpegPsi.ArgumentList.Add("-i"); ffmpegPsi.ArgumentList.Add(absPath); if (endSampleBytes > startSampleBytes && endSampleBytes > 0) { ffmpegPsi.ArgumentList.Add("-t"); ffmpegPsi.ArgumentList.Add(BytesToSec(endSampleBytes - startSampleBytes).ToString("F5")); } if (Math.Abs(volume - 1.0f) >= 0.001f) { ffmpegPsi.ArgumentList.Add("-af"); ffmpegPsi.ArgumentList.Add($"volume={Math.Pow(volume, 2):F5}"); } ffmpegPsi.ArgumentList.Add("-f"); ffmpegPsi.ArgumentList.Add("s16le"); ffmpegPsi.ArgumentList.Add("-ar"); ffmpegPsi.ArgumentList.Add("48000"); ffmpegPsi.ArgumentList.Add("-ac"); ffmpegPsi.ArgumentList.Add("1"); ffmpegPsi.ArgumentList.Add("-loglevel"); ffmpegPsi.ArgumentList.Add("quiet"); ffmpegPsi.ArgumentList.Add("pipe:1"); var pacatPsi = new ProcessStartInfo("pacat") { UseShellExecute = false, RedirectStandardInput = true, CreateNoWindow = true }; pacatPsi.ArgumentList.Add("--playback"); pacatPsi.ArgumentList.Add("--rate=48000"); pacatPsi.ArgumentList.Add("--channels=1"); pacatPsi.ArgumentList.Add("--format=s16le"); if (!string.IsNullOrEmpty(sinkId)) { pacatPsi.ArgumentList.Add("--device"); pacatPsi.ArgumentList.Add(sinkId); } _ffmpegProc = Process.Start(ffmpegPsi)!; _pacatProc = Process.Start(pacatPsi)!; var ffOut = _ffmpegProc.StandardOutput.BaseStream; var paIn = _pacatProc.StandardInput.BaseStream; Task.Run(async () => { try { await ffOut.CopyToAsync(paIn); } catch { } try { paIn.Close(); } catch { } }); } private static double BytesToSec(int sampleBytes) => sampleBytes / (22050.0 * 2); public void Stop() { try { _ffmpegProc?.Kill(); } catch { } try { _pacatProc?.Kill(); } catch { } try { _ffmpegProc?.Dispose(); } catch { } try { _pacatProc?.Dispose(); } catch { } _ffmpegProc = null; _pacatProc = null; } private static List ParsePactl(string type) { try { var psi = new ProcessStartInfo("pactl", $"list {type}") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using var proc = Process.Start(psi); var output = proc?.StandardOutput.ReadToEnd() ?? ""; var devices = new List(); string? currentName = null; string? currentDesc = null; int idx = 0; foreach (var line in output.Split('\n')) { var trimmed = line.Trim(); if (trimmed.StartsWith("Name: ")) currentName = trimmed["Name: ".Length..]; else if (trimmed.StartsWith("Description: ")) currentDesc = trimmed["Description: ".Length..]; if (currentName != null && currentDesc != null) { devices.Add(new AudioDevice(idx++, currentDesc, currentName)); currentName = null; currentDesc = null; } } return devices; } catch { return new List(); } } public void Dispose() => Stop(); } #if WINDOWS public class WindowsAudioService : IAudioService { private NAudio.Wave.WaveOutEvent? _waveOut; public List GetOutputDevices() { var list = new List(); int count = NAudio.Wave.WaveInterop.waveOutGetNumDevs(); int size = System.Runtime.InteropServices.Marshal.SizeOf(); for (int i = 0; i < count; i++) { NAudio.Wave.WaveInterop.waveOutGetDevCaps((IntPtr)i, out var caps, size); list.Add(new AudioDevice(i, caps.ProductName)); } return list; } public List GetInputDevices() { var list = new List(); for (int i = 0; i < NAudio.Wave.WaveInEvent.DeviceCount; i++) { var caps = NAudio.Wave.WaveInEvent.GetCapabilities(i); list.Add(new AudioDevice(i, caps.ProductName)); } return list; } public void Play(string filePath, int deviceIndex, float volume = 1.0f, int startSampleBytes = 0, int endSampleBytes = 0, string pactlId = "") { Stop(); NAudio.Wave.WaveStream reader; if (Math.Abs(volume - 1.0f) < 0.001f && startSampleBytes <= 0 && endSampleBytes <= 0) { reader = new NAudio.Wave.AudioFileReader(filePath); } else { var wave = new NAudio.Wave.WaveChannel32(new NAudio.Wave.WaveFileReader(filePath)); if (Math.Abs(volume - 1.0f) >= 0.001f) wave.Volume = (float)Math.Pow(volume, 2); if (endSampleBytes > 0 && endSampleBytes > startSampleBytes) { int byteCount = (endSampleBytes - startSampleBytes) * 4; var buf = new byte[byteCount]; wave.Position = startSampleBytes * 4; wave.Read(buf, 0, byteCount); wave = new NAudio.Wave.WaveChannel32( new NAudio.Wave.RawSourceWaveStream(new MemoryStream(buf), wave.WaveFormat)); } wave.PadWithZeroes = false; reader = wave; } _waveOut = new NAudio.Wave.WaveOutEvent { DeviceNumber = deviceIndex }; _waveOut.Init(reader); _waveOut.Play(); } public void Stop() { _waveOut?.Stop(); _waveOut?.Dispose(); _waveOut = null; } public void Dispose() => Stop(); } #endif public static class AudioServiceFactory { public static IAudioService Create() { #if WINDOWS return new WindowsAudioService(); #else return new LinuxAudioService(); #endif } }