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
84 lines
3.1 KiB
C#
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();
|
|
}
|