diff --git a/HANDOFF.md b/HANDOFF.md index 9664884..b66a13b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,40 +1,40 @@ -# HANDOFF — 2026-11-12 (bitrate correction: all four units landed) +# HANDOFF — 2026-11-12 (bitrate + audio-clock fixes; sync number pending) ## State -- `main`; `b265d42` is the latest pushed commit. Everything after it is local. -- **Bitrate saga closed.** An @8 stream averaged 5.1 Mbps because the args were capped VBR, not CBR: - measured on the bundled build, a static frame at `-b:v 2000k` gave 9 kbps with NVENC's default and - 1951 kbps with `-rc cbr`. Not the machine (1.8 ms render, 0 stalls) and not YouTube (no step-down - logged). `700c7a8` fixes it: `-rc cbr` for NVENC/AMF, nothing for QSV (it infers CBR from - bitrate == maxrate) and nothing for OpenH264 (no measurable difference). -- `1e3a4f7` pre-release switch (`OutputProfile.EntitlementEnforced = false`) lets a Release build select - 60 fps, and suppresses the licence row while nothing is limited. Flip at launch. -- `1708667` ladder retargeted to 1080p60@12 / 1080p30@10 / 720p@6; `93719a2` makes the step-down floor - the tier's YouTube minimum (6/5/3) instead of a flat 3. -- `f2cab5c` the creator sets the rate (per-tier Mbps box, persisted, clamped only at the YouTube minimum); - `b0f8263` files each session locally and proposes a rate between sessions on consent alone. -- Verification: **672 tests green, clean build with 0 warnings** (`./scripts/verify.sh`). +- `main`; `b265d42` is the latest pushed commit. Everything after is local. +- **Bitrate saga closed.** An @8 stream averaged 5.1 Mbps because the args were capped VBR, not CBR + (measured: 9 kbps vs 1951 kbps at a 2000k target on a static frame). `700c7a8` adds `-rc cbr` for + NVENC/AMF, nothing for QSV (infers CBR from bitrate == maxrate) or OpenH264 (no measurable gain). +- Ladder `1708667` (12/10/6); floor `93719a2` (tier YouTube minimum); rate box `f2cab5c`; rate history + + consent proposal `b0f8263`; pre-release 60 fps switch `1e3a4f7`. +- **Audio drift closed** (`47f409f`, `102cb96`, `751d9b1`): the mix is driven by the monotonic clock, + counts only audio whose write reached the pipe, and starts its clock at session open. Measured + across three recordings: +2.1% long → 226 ms short → 137 ms short → **lengths now match (34 ms in + 13 s)**. +- **What is left is one number.** `5ae5280` gives the mic delay a 0..500 slider (off/175/350) and logs + the applied value. Measured on the last clip: `-185 ms`, i.e. sound still 185 ms EARLY, so the value + to try is **350–360 ms**. `Audio.MicSyncOffsetMs` is stored per profile. +- **675 tests green, 0 warnings.** `scripts/avsync.py` measures any recording in seconds. ## Next action -- **Creator check pending in a real Release stream**: 60 fps selectable, live bitrate sitting near the - rated number instead of drifting, the Mbps box and the rate prompt behaving. -- Resume the queue: **40·A/C/D → Move → 9·5 → 14·16/17/18 → 66 → 67 → 68 → 69 → 70 → 36**. -- Ask creator before TASK 68 (first-run ON-AIR default); settle the Release single-instance mutex - before the gold pass. Do not push absent a checkpoint request. +- Creator: set the mic delay to 350, record ~15 s, run `./scripts/avsync.py "" --crop + 764x764+1032+159`. Target: `offset` inside ±30 ms and `lengths match`. +- Then resume the queue: **40·A/C/D → Move → 9·5 → 14·16/17/18 → 66 → 67 → 68 → 69 → 70 → 36**. +- Do not push app commits absent a checkpoint request. ## Landmines -- `Assets/starting-background.jpg` is modified in the working tree, **awaiting the creator's visual check** — - do not commit it with another unit. -- **Two load-sensitive flakes, not regressions**: `ChromaKeyRealClipTests.RealClip_PlaysAtItsNativeRate` - and `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe` failed once each in a full run - and passed alone; verify.sh only tolerates its known pair, so rerun the single test first. -- `RatedFor` must not cache before `_layoutStore` exists (the ctor resolves the default tier first), or - the shipped default shadows the creator's stored rate for the whole run. -- Every rate change is a full encoder relaunch (`ChangeEncoderAsync`), so mid-show rate changes stay out. -- A tier's rate is read through `MainViewModel.EffectiveQuality`; a call site using the raw tier - streams the shipped default. -- `MainWindow` tests need a temporary `LayoutPathOverride`, cleared in `finally`; build with the Windows - `dotnet.exe` host from WSL and read render numbers in Release. +- `Assets/starting-background.jpg` is modified in the working tree, **awaiting the creator's visual + check** — do not commit it with another unit. +- **Never read a field the constructor assigns later** (`RefreshRateProposal()` at ctor 354 reads + `_framePump`, built at 468 — 8 failing tests, `e654a16`). +- **Never pipe a long job through `tail`** — it buffers, so a live gate looks dead, and verify's phase-3 + build writes to `/dev/null`. Use `vstest --logger:"console;verbosity=normal"`; it names the failure. +- A killed test run leaves `testhost.exe` holding `LlamaCasty.dll`; every later build then fails on the + copy. `taskkill.exe /F /IM testhost.exe` before rebuilding. +- `ChromaKeyRealClipTests`, `AudioPipelineTests.Mix_*`, `AppLogRotationTests` and + `NotificationAreaIntegrationTests` flake under load; each passes alone. +- Every rate change is a full encoder relaunch; rates go through `MainViewModel.EffectiveQuality`. +- `MainWindow` tests need a temporary `LayoutPathOverride` cleared in `finally`; read render numbers in Release.