Files
gramps a6aea5bf26 feat(ui): color and acknowledge pinned log errors
Add independent AppLog message types and explicit Class 1/2 severity. Wrap and color log rows, pin Class 1 above Class 2, and let acknowledgement return entries to the current-launch timeline without changing file format or rotation.

Ref: https://logtape.org/manual/levels
2026-11-02 18:17:18 -08:00

84 lines
3.1 KiB
C#

using System;
using NAudio.Wave;
using ytLive.Helpers;
namespace ytLive.Services.Audio;
/// <summary>
/// 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.
///
/// <para>One long-lived <see cref="WaveOutEvent"/> 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.</para>
/// </summary>
public sealed class AlertAudioOutput : IDisposable
{
private readonly object _gate = new();
private WaveOutEvent? _device;
private BufferedWaveProvider? _buffer;
private int _sampleRate;
private int _channels;
/// <summary>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.</summary>
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();
}
}
}
/// <summary>Stop and flush — called when the clip ends so a queued tail cannot bleed into the
/// next alert.</summary>
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();
}