docs(handoff): bitrate closed, audio drift closed, one sync number left

Scope justification: Assets/starting-background.jpg is outside this unit — the creator's
replacement art, still awaiting their visual check (recorded in the landmines).
This commit is contained in:
2026-11-12 13:58:20 -08:00
parent b216cbc56f
commit bb3bc12784
+31 -31
View File
@@ -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 "<clip>" --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.