using NAudio.Wave; namespace Robovoice.App; internal sealed class AudioOutput : IDisposable { private WaveOutEvent? _waveOut; private BufferedWaveProvider? _bufferProvider; private int _sampleRate; private string _deviceName = string.Empty; private float _lastSample; private bool _disposed; public Action? Log { get; set; } public void Start(int sampleRate, string? deviceName = null) { ObjectDisposedException.ThrowIf(_disposed, this); _sampleRate = sampleRate; _deviceName = deviceName ?? string.Empty; _lastSample = 0f; Stop(); int deviceNumber = FindDevice(_deviceName); _waveOut = new WaveOutEvent { DeviceNumber = deviceNumber, DesiredLatency = 200 }; _bufferProvider = new BufferedWaveProvider( WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 1)) { BufferDuration = TimeSpan.FromSeconds(60), DiscardOnBufferOverflow = true, ReadFully = true, }; _waveOut.Init(_bufferProvider); _waveOut.PlaybackStopped += OnPlaybackStopped; _waveOut.Play(); } public void WriteSamples(float[] samples) { ObjectDisposedException.ThrowIf(_disposed, this); if (_bufferProvider is null) return; if (samples.Length > 0) _lastSample = samples[^1]; byte[] bytes = new byte[samples.Length * sizeof(float)]; Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); _bufferProvider.AddSamples(bytes, 0, bytes.Length); } public void Flush() { ObjectDisposedException.ThrowIf(_disposed, this); if (_bufferProvider is null || _sampleRate == 0) return; // Fade out from last sample to zero over 5ms to avoid click int fadeSamples = (int)(_sampleRate * 0.005); float[] fade = new float[fadeSamples]; for (int i = 0; i < fadeSamples; i++) { float t = (float)i / fadeSamples; fade[i] = _lastSample * (1f - t); } WriteSamples(fade); // Then 500ms of silence to let NAudio drain int padSamples = (int)(_sampleRate * 0.5); WriteSamples(new float[padSamples]); _lastSample = 0f; } private void OnPlaybackStopped(object? sender, StoppedEventArgs e) { } public void Stop() { if (_waveOut is not null) { _waveOut.PlaybackStopped -= OnPlaybackStopped; _waveOut.Stop(); _waveOut.Dispose(); _waveOut = null; } _bufferProvider?.ClearBuffer(); _bufferProvider = null; _lastSample = 0f; } private static int FindDevice(string? deviceName) { if (string.IsNullOrEmpty(deviceName)) return -1; for (int i = 0; i < WaveOut.DeviceCount; i++) { var caps = WaveOut.GetCapabilities(i); if (caps.ProductName.Contains(deviceName, StringComparison.OrdinalIgnoreCase)) return i; } return -1; } public static IReadOnlyList<(int Index, string Name)> GetDevices() { var devices = new List<(int, string)>(); for (int i = 0; i < WaveOut.DeviceCount; i++) { var caps = WaveOut.GetCapabilities(i); devices.Add((i, caps.ProductName)); } return devices; } public void Dispose() { if (_disposed) return; Stop(); _disposed = true; } }