diff --git a/MyMistakes.md b/MyMistakes.md index 5c11844..dffcb40 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -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. diff --git a/Services/Audio/AudioGainProvider.cs b/Services/Audio/AudioGainProvider.cs index c230c56..e4d5480 100644 --- a/Services/Audio/AudioGainProvider.cs +++ b/Services/Audio/AudioGainProvider.cs @@ -28,8 +28,12 @@ public sealed class AudioGainProvider /// Mic gain for the live mix (0 = muted). public double MicGain() => _micMuted() ? 0 : _micVolume(); - /// 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. - public double LoopbackGain() => _gameMuted() ? 0 : 1.0; + /// 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). + public double LoopbackGain() => _gameMuted() ? 0 : _gameVolume(); } diff --git a/Services/Audio/AudioMixer.cs b/Services/Audio/AudioMixer.cs index e63f8c0..d99a062 100644 --- a/Services/Audio/AudioMixer.cs +++ b/Services/Audio/AudioMixer.cs @@ -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); diff --git a/TASKS.md b/TASKS.md index d84286c..7576b18 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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. diff --git a/ai.md b/ai.md index 5e7ee4b..c7bbeb2 100644 --- a/ai.md +++ b/ai.md @@ -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 →