TASK 9 audio milestone: real stream audio + voice filters + auto-duck + free TRAX music — the mixer now feeds the encoder real audio (ffmpeg reads a Windows named pipe, -f f32le -ar 48000 -ac 2 -i \.\pipe\ytllive_audio, replacing anullsrc silence; explicit -map 0:v -map 1:a via EncoderOptions.AudioPipeName), with honest gains reaching the stream (MicVolume scales mic, GameAudioVolume + mute scale loopback, Func<double> seams on AudioMixer), an always-on pure-C# voice chain on the mic BEFORE the meter and mix (LowShelfFilter 120Hz +4dB → HighShelfFilter 8kHz +3dB → NoiseGate 0.005/hysteresis 0.5 → Compressor 0.5 4:1, all TDF2), AutoDucker (mic RMS>0.02 → loopback ×0.25, attack 0.05/release 0.005), and TRAX free background music (MusicPlayer = MediaFoundationReader → VolumeWaveProvider16 at fixed 0.20 → WaveOutEvent via the new sibling NAudio.WinMM 2.2.1 package; plays to the default device so the existing loopback carries it, ducked with game; footer TRAX button with red/yellow/green status dot, left-click toggles/picks, right-click opens the OpenFileDialog picker, tooltip shows the track name; persisted via schema v9 single-row Music). Build-time deviations from the plan (recorded in ai.md + TASKS.md): WasapiCapture in NAudio 2.2.1 exposes no overridable GetDefaultMixFormat so sources run device mix format and the mixer's TinyResampler normalizes any rate to 48kHz (resampler IS the design); FfmpegEncoder.cs untouched — MainViewModel.StopStream calls _audioMixer.StopLive() (pipe EOF) before the frame pump stops; sound-bar relabelled 'Desktop Audio', IsGameAudioBarVisible = game sound OR music playing. Tests: new AudioPipelineTests (DSP/ring-buffer/ducker/resampler units + the ONE integration test Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe reading real pipe bytes; ring-buffer overwrite bug found + fixed), FfmpegEncoderTests/LayoutStorePersistenceTests updated — 196 tests passing, 0 warnings

This commit is contained in:
2026-08-14 18:09:26 -07:00
parent 3d92bd0225
commit 6d71acef8c
22 changed files with 1605 additions and 123 deletions
+95
View File
@@ -0,0 +1,95 @@
namespace ytLive.Services.Audio;
/// <summary>
/// A thread-safe float ring buffer for the live audio path (TASK 9): NAudio
/// raises capture chunks on its own threads, the mixer's live loop drains on
/// its own. Writes overwrite the OLDEST samples when full so the buffer never
/// grows unbounded — the stream can't stall on capture jitter, it just skips
/// the stale tail.
/// </summary>
public sealed class AudioRingBuffer
{
private readonly object _sync = new();
private readonly float[] _buffer;
private int _head;
private int _count;
public AudioRingBuffer(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_buffer = new float[capacity];
}
public int Count
{
get
{
lock (_sync)
{
return _count;
}
}
}
/// <summary>Appends a chunk, dropping the oldest samples if the buffer is full.</summary>
public void Write(float[] samples)
{
if (samples.Length == 0)
return;
lock (_sync)
{
if (samples.Length >= _buffer.Length)
{
Array.Copy(samples, samples.Length - _buffer.Length, _buffer, 0, _buffer.Length);
_head = 0;
_count = _buffer.Length;
return;
}
if (_count + samples.Length > _buffer.Length)
{
// Evict the oldest samples to make room; the incoming write then
// overwrites them as it wraps. Head stays put — only writing
// advances it.
var drop = _count + samples.Length - _buffer.Length;
_count -= drop;
}
var writeAt = _head;
var first = Math.Min(samples.Length, _buffer.Length - writeAt);
Array.Copy(samples, 0, _buffer, writeAt, first);
if (first < samples.Length)
Array.Copy(samples, first, _buffer, 0, samples.Length - first);
_head = (writeAt + samples.Length) % _buffer.Length;
_count += samples.Length;
}
}
/// <summary>Reads up to <paramref name="count"/> samples, removing them;
/// returns the number actually read (fewer on underrun).</summary>
public int Read(float[] destination, int count)
{
lock (_sync)
{
var n = Math.Min(count, _count);
var tail = (_head - _count + _buffer.Length) % _buffer.Length;
var first = Math.Min(n, _buffer.Length - tail);
Array.Copy(_buffer, tail, destination, 0, first);
if (first < n)
Array.Copy(_buffer, 0, destination, first, n - first);
_count -= n;
return n;
}
}
public void Clear()
{
lock (_sync)
{
_head = 0;
_count = 0;
}
}
}