TASK 4 audio follow-up: game audio bar + mic status dot + always-on capture — the footer's second audio control (desktop/game, a mirror of the mic bar: meter + mute + volume) appears only while a full-screen game is producing sound (IGameAudioDetector seam + GameAudioHysteresis: show ~500ms of fullscreen+sound, hide ~1s after leaving fullscreen, silence never hides an active bar; VM polls on a 250ms timer); capture now runs for the app's lifetime so both meters preview live (started at startup via StartMicCaptureAsync, disposed in Shutdown — no longer go-live driven); MIC label is a button with a status dot (Models/MicStatus: green via the source Started event, yellow = mic problem, red = no device); PickMicrophone swaps the live device immediately via AudioMixer.RestartMic; fixed a latent ?.Invoke(meter.Push(...)) short-circuit that skipped the meter update when nothing subscribed — 167 tests passing, 0 warnings

This commit is contained in:
2026-08-13 11:29:44 -07:00
parent ea250c02a6
commit 9f9ed34627
20 changed files with 897 additions and 82 deletions
+41 -25
View File
@@ -7,41 +7,57 @@
## Session state (last updated: 2026-08-13)
- **Branch:** `main`. TASK 4 **ship step 5.5** is **committed and pushed** (`ac60a26`,
"TASK 4 ship step 5.5: social bar bug fixes + bar on the live output").
- **This session (follow-up):** the drag-snap from 5.5 failed in practice — it snapped
up but wouldn't come back down (jitter around the deadzone, per the user). **Superseded
by a click-toggle** (user decision, KISS): `MainWindow.SocialBar_MouseLeftButtonDown`
`MainViewModel.ToggleSocialBarPosition()` flips the bar top ⇄ bottom; the bar rides
`{Binding SocialBarTop}` alone; `SocialBarSnap` + its 2 tests removed. Build 0 warnings,
**153 tests passing** (155 2 snap units). **Uncommitted:** this click-toggle change +
its memory corrections (TASKS.md / ai.md / HANDOFF). Next action: commit + push.
- **Verified:** 0 warnings; 153/153 tests pass.
- **Branch:** `main`. The **game audio bar + mic status dot + always-on capture**
work is **uncommitted** (see list below). The last committed+pushed baseline is
`ea250c0` (social bar click-toggle).
- **This session (audio UX follow-up):** per the creator's requests —
1. The MIC label is now a **button with a status dot** (`Models/MicStatus`:
green = capturing, yellow = requested mic problem, red = no device).
2. **Mic capture runs for the app's lifetime** (started at startup via
`StartMicCaptureAsync`, disposed in `Shutdown`; `BeginGoLive`/`StopStream`
no longer start/stop the mixer) so both meters preview live.
3. A **game audio bar** (desktop/game — meter + mute + volume, mirror of the
mic bar) sits centered beneath the preview and appears only while a
**full-screen game is producing sound** (decision: fullscreen + loopback
sound; **silence never hides an active bar** — the creator's final rule).
Show after ~500ms of fullscreen+sound, hide ~1s after leaving fullscreen.
- **Uncommitted files:** `MainWindow.xaml`/`.cs`, `ViewModels/MainViewModel.cs`,
`Services/Audio/{IAudioSource,WasapiMicAudioSource,WasapiLoopbackAudioSource,AudioMixer}.cs`,
new `Models/MicStatus.cs`, new `Services/{IGameAudioDetector,GameAudioHysteresis,GameAudioDetector}.cs`,
`ytLive.Tests/AudioMixerTests.cs`, new `GameAudioHysteresisTests.cs` +
`GameAudioDetectorTests.cs`, memory docs (TASKS.md, ai.md, Services/index.md,
ViewModels/index.md, Models/index.md, HANDOFF).
- **Verified:** build 0 warnings, 0 errors; **167 tests passing** (full suite,
after the final rebuild).
- **Landmines:**
- Never set a local `Canvas.SetTop` on the social bar — a local value permanently
overrides `{Binding SocialBarTop}` (the `ClearValue` lesson from 5.5).
- `AudioMixer` meter `Push` is unconditional **by design now**: `OnMicSample`/
`OnLoopbackSample` compute the level first, then raise the event — a
`?.Invoke(meter.Push(...))` short-circuit skipped the meter update when
nothing was subscribed (found by `RestartMic_ResetsLevel`, fixed).
- `MicConnected` comes from the source `Started` event, raised right after
`StartRecording()` succeeds — tests must `MarkStarted()` the fake source
before asserting connection state.
- Zero mic devices at startup = red dot AND the mixer is never started, so
loopback + the game bar can't run either (no capture at all) — acceptable.
- The game detector is polled on the UI thread via a 250ms `DispatcherTimer`;
`OnGameAudioPollTick` wraps `Poll()` in try/catch + `AppLog`.
- The pump reads the active scene on a background thread while the UI can still
edit it — a concurrent-mutation exception is contained (logged + `Failed` +
the pump stops), not a crash. The background thread + video pipeline is the
new reality since ship step 5.
the pump stops), not a crash.
- `StopAsync` must stop the encoder (closes stdin) **before** awaiting the pump
loop — closing stdin unblocks a write stuck on pipe backpressure; the reverse
order deadlocks.
- `FramePump.IsRunning` must be set true before the loop starts (a completed-task
delay can run the first iteration synchronously on the caller's thread).
- `StartAsync` never throws; the VM fires-and-forgets it. `Failed` while live
flips `StreamStatus.Error` (minimal — real health surfacing is ship step 6).
- The heal runs off the UI thread and applies via the dispatcher; a config swap
mid-heal (dialog Save) can orphan the healed values on the old entries — harmless
(best-effort, re-healed next load).
- Tests never instantiate `MainViewModel` directly except the round-clip
integration test (a real `MainWindow`), which never goes live — keep it that way.
- Sandbox can't reach outbound HTTPS — the subdomain-probe logic is verified via
stub-handler tests only, not against the real `mastodon.llamachile.tube`.
- **Next step:** commit + push the click-toggle follow-up. Then TASK 4 ship step 6 —
health stats: bind `FramePump.HealthUpdated` (bitrate/FPS/duration) into the bottom
bar. Nothing else queued — do not expand the task queue on your own. Optional, not
queued: rewriting the healed entry's `ProfileUrl` to
- Sandbox can't reach outbound HTTPS — `HttpSocialValidator` stub-handler tests
only, never the real instance.
- **Next step:** commit + push the audio UX follow-up (the whole uncommitted set
above in one commit). Then TASK 4 ship step 6 — health stats: bind
`FramePump.HealthUpdated` (bitrate/FPS/duration) into the bottom bar. Nothing
else queued — do not expand the task queue on your own. Optional, not queued:
rewriting the healed entry's `ProfileUrl` to
`https://mastodon.llamachile.tube/@gramps` (user must say the word).
- **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`;