fix(audio): _delayedMix never initialized — one NRE line behind the 'known failure', the class hang, AND the log flood; loopback gain now honest
The creator-declared known failure (Mix_HonorsProviderGains) and AudioPipelineTests'
standalone hang shared ONE root cause, born in TASK 22: AudioMixer.FillAndMix
dereferenced _delayedMix (declared float[]? , never assigned) as delayed.Length —
every live-mix tick NRE'd before the pipe write, so NO audio ever reached the wire
(tests starved -> hung/fail; app -> swallowed catch logged only ex.Message, 10ms
flood). Now: ctor-allocated + null-check, catch logs WITH stack via AppLog and is
throttled 5s, and a cancelled token breaks out before logging.
Bonus contract fix: AudioGainProvider.LoopbackGain returned unity on a disproven
premise ('loopback scales with endpoint volume'). The creator's tonight observation —
volume at 20%, meter pegged — proves the WASAPI tap is pre-endpoint-volume, so the
mixer must multiply by GameAudioVolume for stream honesty (this is what ai.md always
specified; the 'failing' test encoded the same and was RIGHT).
AudioPipelineTests: 25/25 in 26ms, standalone, no hang. Known-failure count: ZERO.
ai.md tests paragraph rewritten (no more suite-total claims, both ex-'knowns'
explained); TASK 22 regression recorded; MyMistakes: the known-failure-label rules.
This commit is contained in:
@@ -28,8 +28,12 @@ public sealed class AudioGainProvider
|
||||
/// <summary>Mic gain for the live mix (0 = muted).</summary>
|
||||
public double MicGain() => _micMuted() ? 0 : _micVolume();
|
||||
|
||||
/// <summary>Loopback (desktop/game) gain for the live mix (0 = muted, 1 = unity).
|
||||
/// Unity because the slider now drives system volume via PushSystemVolume,
|
||||
/// so the WASAPI loopback signal is already scaled by the user's volume choice.</summary>
|
||||
public double LoopbackGain() => _gameMuted() ? 0 : 1.0;
|
||||
/// <summary>Loopback (desktop/game) gain for the live mix (0 = muted).
|
||||
/// Honesty rule (2026-09-01): WASAPI loopback capture does NOT shrink with the
|
||||
/// endpoint volume — the creator dropped system volume to 20% and the captured
|
||||
/// level stayed pegged — so the mixer must multiply by GameAudioVolume itself
|
||||
/// for the stream to follow the knob (the old unity assumption left the stream
|
||||
/// at full desktop volume while the headphones went quiet; ai.md always
|
||||
/// specified loopbackGain = GameAudioVolume — the code had drifted).</summary>
|
||||
public double LoopbackGain() => _gameMuted() ? 0 : _gameVolume();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ytLive.Helpers;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.Services.Audio;
|
||||
@@ -48,6 +49,7 @@ public sealed class AudioMixer : IDisposable
|
||||
private float[]? _loopbackChunk;
|
||||
private float[]? _mixBuffer;
|
||||
private float[]? _delayedMix;
|
||||
private DateTime _lastLoopErrorLogged = DateTime.MinValue;
|
||||
|
||||
public AudioMixer(
|
||||
IAudioSource mic,
|
||||
@@ -78,6 +80,7 @@ public sealed class AudioMixer : IDisposable
|
||||
_micChunk = new float[framesPerTick];
|
||||
_loopbackChunk = new float[framesPerTick * 2];
|
||||
_mixBuffer = new float[framesPerTick * 2];
|
||||
_delayedMix = new float[framesPerTick * 2];
|
||||
|
||||
_mic.Started += OnMicStarted;
|
||||
_mic.SampleReady += OnMicSample;
|
||||
@@ -280,7 +283,18 @@ public sealed class AudioMixer : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.Invoke($"Audio live loop error: {ex.Message}");
|
||||
// Throttled + never on a dead session: the 2026-09-01 first-launch flood
|
||||
// was one logged NRE every 10ms tick (see _delayedMix fix). Log at most
|
||||
// once per 5s, and stop looping if the pipe writer is gone for good.
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - _lastLoopErrorLogged >= TimeSpan.FromSeconds(5))
|
||||
{
|
||||
_lastLoopErrorLogged = now;
|
||||
AppLog.Write(ex, "Audio live loop error");
|
||||
_log?.Invoke($"Audio live loop error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var delay = nextTick - DateTime.UtcNow;
|
||||
@@ -343,8 +357,8 @@ public sealed class AudioMixer : IDisposable
|
||||
_masterLimiter.Process(mix);
|
||||
|
||||
_syncDelay.Configure(_syncOffsetMs?.Invoke() ?? 0);
|
||||
var delayed = _delayedMix!;
|
||||
if (delayed.Length < mix.Length)
|
||||
var delayed = _delayedMix;
|
||||
if (delayed is null || delayed.Length < mix.Length)
|
||||
delayed = _delayedMix = new float[mix.Length];
|
||||
_syncDelay.Process(mix, delayed);
|
||||
Array.Copy(delayed, mix, mix.Length);
|
||||
|
||||
Reference in New Issue
Block a user