using System; using NAudio.Wave; using ytLive.Helpers; namespace ytLive.Services.Audio; /// /// Plays alert audio on the **default output device**, which is what makes it visible in the /// desktop sound bar (creator ruling 2026-11-01): the app mixes for the encoder, but only audio /// that actually reaches the speakers is picked up by the WASAPI loopback that feeds both the /// meter and the stream. Injecting straight into the mixer, as this used to do, produced sound /// nobody could see and nobody could hear. /// /// One long-lived with a buffered float provider: the alert clip /// submits PCM as it decodes, and a sound byte can be submitted through the same device, so they /// mix with each other and with everything else on the machine. /// public sealed class AlertAudioOutput : IDisposable { private readonly object _gate = new(); private WaveOutEvent? _device; private BufferedWaveProvider? _buffer; private int _sampleRate; private int _channels; /// Queue one decoded pulse of the alert clip. Never blocks the decoder: if the device /// cannot keep up the buffer simply grows, and it is cleared when the alert ends. public void Submit(AudioSample sample) { if (sample.Samples.Length == 0) return; lock (_gate) { try { EnsureDevice(sample.SampleRate, sample.Channels); if (_buffer == null) return; var bytes = new byte[sample.Samples.Length * sizeof(float)]; Buffer.BlockCopy(sample.Samples, 0, bytes, 0, bytes.Length); _buffer.AddSamples(bytes, 0, bytes.Length); } catch (Exception ex) { // No output device (a headless stream box, a locked-down session): the alert still // renders, it is simply silent. Never let audio take the alert down. AppLog.Write(ex, "Alert audio output failed", AppLogErrorClass.Class2); Stop(); } } } /// Stop and flush — called when the clip ends so a queued tail cannot bleed into the /// next alert. public void Stop() { lock (_gate) { try { _device?.Stop(); } catch { /* already stopped */ } _buffer?.ClearBuffer(); _device?.Dispose(); _device = null; _buffer = null; } } private void EnsureDevice(int sampleRate, int channels) { if (_device != null && _sampleRate == sampleRate && _channels == channels) return; _device?.Dispose(); _sampleRate = sampleRate; _channels = channels; _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 32, channels)) { BufferDuration = TimeSpan.FromSeconds(2), DiscardOnBufferOverflow = true, }; _device = new WaveOutEvent { DesiredLatency = 120 }; _device.Init(_buffer); _device.Play(); } public void Dispose() => Stop(); }