Files
ytLlive/Services/Audio/TinyResampler.cs
T
gramps 6d71acef8c 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
2026-08-14 18:09:26 -07:00

55 lines
1.9 KiB
C#

namespace ytLive.Services.Audio;
/// <summary>
/// A tiny stateful linear-interpolation sample-rate converter (TASK 9). NAudio's
/// WASAPI capture rate follows the device mix format; the stream needs a fixed
/// 48 kHz, so each source normalizes through one of these. Purely a fallback —
/// the mic requests 48 kHz float up front, and most loopback mix formats are
/// already 48 kHz, in which case <see cref="NeedsResampling"/> is false and the
/// chunk passes through untouched. Pure — unit-tested.
/// </summary>
public sealed class TinyResampler
{
private readonly double _ratio;
private double _pos;
/// <param name="inputRate">The source sample rate.</param>
/// <param name="outputRate">The target sample rate (48 kHz for the stream).</param>
public TinyResampler(int inputRate, int outputRate)
{
if (inputRate <= 0 || outputRate <= 0)
throw new ArgumentOutOfRangeException();
_ratio = (double)inputRate / outputRate;
}
public bool NeedsResampling => Math.Abs(_ratio - 1.0) > 1e-9;
/// <summary>Converts one interleaved chunk, keeping the phase across calls.</summary>
public float[] Process(float[] input)
{
if (!NeedsResampling)
return input;
var outLen = (int)((_pos + input.Length) / _ratio);
var output = new float[outLen];
for (var i = 0; i < outLen; i++)
{
var inPos = _pos + i * _ratio;
var idx = (int)inPos;
if (idx >= input.Length)
{
output[i] = input[input.Length - 1];
continue;
}
var frac = (float)(inPos - idx);
var a = input[idx];
var b = idx + 1 < input.Length ? input[idx + 1] : a;
output[i] = a + (b - a) * frac;
}
_pos += outLen * _ratio - input.Length;
return output;
}
public void Reset() => _pos = 0;
}