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:
@@ -124,3 +124,25 @@ with `#Name`) + panel properties (`IsHitTestVisible/Opacity/children/actual size
|
||||
test process shares `%APPDATA%\ytLlive\startup.log` with the real app: lines like
|
||||
`camera 'test-camera' failed` are test noise, not DB state — to check pollution, query
|
||||
the DB directly (`python3 sqlite3`, `SELECT DeviceId FROM Webcam`), not the log.
|
||||
|
||||
## A "known failure" label without a recorded cause = a bug on life support
|
||||
|
||||
(2026-09-01, the audio triple-take) One line — `_delayedMix` (nullable, added by TASK 22,
|
||||
never initialized) dereferenced as `delayed.Length` — produced THREE symptoms that lived in
|
||||
the map as two separate "pre-existing, do-not-chase" entries: (a)
|
||||
`Mix_HonorsProviderGains…` "known failure", (b) `AudioPipelineTests` hangs when run at all
|
||||
(a test reading a named pipe with no writer blocks — hung test ≠ flaky test, it's a starved
|
||||
producer), (c) startup.log flooded "Audio live loop error" every 10ms (the mixer loop caught
|
||||
and logged only `ex.Message` — stack thrown away).
|
||||
|
||||
Rules derived:
|
||||
1. NEVER label a test "known/pre-existing" without writing WHY (exception type + first app
|
||||
frame). An unexplained known-failure is deferred archaeology that hardens into fog.
|
||||
2. A test that waits on IPC + a producer whose output vanished are usually ONE bug — look
|
||||
for the producer before blaming the test.
|
||||
3. Catch-and-log-swallow of `ex.Message` hides root causes; log with stack (`AppLog.Write(ex,
|
||||
...)`) and throttle (5s) instead of dropping or flooding.
|
||||
Bonus: the "failing" test encoded the MAP's contract (`loopbackGain = GameAudioVolume`); the
|
||||
code had drifted to unity on a disproven premise (loopback capture does NOT follow endpoint
|
||||
volume — creator's 20%-volume/pegged-meter observation killed it). The test was right all
|
||||
along — failing tests may be the last honest witnesses; interrogate, don't pardon.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1127,11 +1127,12 @@ change. First GUI smoke test: add a short real mp4 on native Windows, confirm it
|
||||
3. ☑ UI: compact "SYNC" slider (0..500) on the mic bar with a status dot (green = no-op, amber = offset set) via `IntToSyncBrushConverter`.
|
||||
4. ☑ Persisted in `LayoutStore.Settings` (`Audio.SyncOffsetMs`) via `LoadAudioSyncOffsetMs`/`SaveAudioSyncOffsetMs`; saved from `SaveLayoutNow`.
|
||||
5. ☑ Tests: `AudioSyncDelayTests` — zero-delay identity, negative→0 clamp, >500 ms clamp to 500 ms, and 10 ms → 960 interleaved-sample shift.
|
||||
6. ⚠ **Post-ship regression (found 2026-09-01, first real launch):** this task's sync-delay line `_delayedMix` was declared nullable and never initialized — the first `delayed.Length` deref NRE'd EVERY live-mix tick, silently killing all live/record audio, hanging `AudioPipelineTests`, and being mislabeled a "known failure". Fixed in the recording-verification pass (init + null-check + throttled loop errors logged with stack); `AudioPipelineTests` 25/25 green afterward. Lesson recorded in MyMistakes.
|
||||
|
||||
### Design decisions
|
||||
|
||||
- **Global offset first** — one setting for all audio sources. Per-source is v1.1+.
|
||||
- **Positive-only (delay audio)** — the physically-correct direction (audio runs ahead of the video). True "advance" needs a video-side delay and is tracked as per-source/advance in v1.1 (line 1388).
|
||||
- **Positive-only (delay audio)** — the physically-correct direction (audio runs ahead of the video). True "advance" needs a video-side delay and is PERMANENTLY OUT with per-source sync (TASKS.md → "Out of product" — v1.x phrasing retired 2026-09-01).
|
||||
- **Simple slider** — 0 to +500 ms, default 0. No numeric input needed.
|
||||
- **Visual feedback** — "sync OK" status dot shows when an offset is dialled in.
|
||||
|
||||
|
||||
@@ -126,18 +126,18 @@ the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegration
|
||||
GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests
|
||||
(TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests
|
||||
(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests (2026-09-01
|
||||
rollback) — **~250 total (exact whole-suite count not claimable — the full run hangs and
|
||||
`AudioPipelineTests` hangs even STANDALONE (confirmed 2026-09-01 — treat the class as
|
||||
un-runnable until the audio channel-init fix lands). Every other class passes individually
|
||||
through the Windows dotnet.exe host, including the real-`MainWindow`/RealApp tests — 247 pass
|
||||
was the last full-suite number (2026-08-31); since then: the ONE remaining known failure is
|
||||
`AudioPipelineTests.Mix_HonorsProviderGains…` (the creator-declared known, suspected WASAPI
|
||||
channel declaration/init), `RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable…` was
|
||||
root-caused and FIXED 2026-09-01 (two stale-test layers — namescoped `FindName` +
|
||||
`VisualTreeHelper.HitTest` used where `UIElement.InputHitTest` models input; see MyMistakes
|
||||
recipe; no product bug), and three tests were ADDED (locator dead-pin 404, EndBroadcast
|
||||
close-out, SessionTeardown rollback). "GUI suites can't run from here" was an overstatement —
|
||||
per-class Windows-host vstest runs them fine.)**
|
||||
rollback) — **ZERO known failures as of 2026-09-01. `AudioPipelineTests` 25/25 green in 26ms —
|
||||
the "known failing" `Mix_HonorsProviderGains…` and the class's notorious STANDALONE HANG shared
|
||||
one root cause: TASK 22's `_delayedMix` (nullable, never initialized) was dereferenced
|
||||
(`delayed.Length` on null) every live-mix tick — a swallowed NRE starved the pipe the tests
|
||||
read and flooded startup.log. Test was right, code drifted from the map's own contract
|
||||
(loopbackGain = GameAudioVolume — now honored; the "unity because loopback scales with the
|
||||
endpoint" assumption was disproven by the creator's 20%-volume meter observation). The former
|
||||
RoundClip "known failure" (stale-test layers: namescoped `FindName` + `VisualTreeHelper.HitTest`
|
||||
where `UIElement.InputHitTest` models input — see MyMistakes) was fixed the same day.
|
||||
Per-class runs through the Windows dotnet.exe host execute the RealApp/MainWindow suites fine;
|
||||
only the FULL-suite run still hangs (WASAPI teardown, pre-existing) — and the whole-suite
|
||||
"247 total" era count is stale; trust per-class results.**
|
||||
|
||||
Reward-event capture (monetization awareness, see the Monetization section) will add its integration
|
||||
tests here when it ships: one real chat-poll payload containing all seven reward event types →
|
||||
|
||||
Reference in New Issue
Block a user