| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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<AudioDevice> GetOutputDevices();
- List<AudioDevice> 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<AudioDevice> GetOutputDevices() => ParsePactl("sinks");
- public List<AudioDevice> 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<AudioDevice> 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<AudioDevice>();
- 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<AudioDevice>(); }
- }
- public void Dispose() => Stop();
- }
- #if WINDOWS
- public class WindowsAudioService : IAudioService
- {
- private NAudio.Wave.WaveOutEvent? _waveOut;
- public List<AudioDevice> GetOutputDevices()
- {
- var list = new List<AudioDevice>();
- int count = NAudio.Wave.WaveInterop.waveOutGetNumDevs();
- int size = System.Runtime.InteropServices.Marshal.SizeOf<NAudio.Wave.WaveOutCapabilities>();
- 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<AudioDevice> GetInputDevices()
- {
- var list = new List<AudioDevice>();
- 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
- }
- }
|