feat(media): play the clip's soundtrack and share one decode pipe

A video file was picture-only: the decode pipe passed `-an`, so a starting-soon
or BRB clip with music played silent everywhere. Adds sound to every clip and
collapses the two decoders into one.

- `Services/VideoClipPipe.cs`: the shared pipe behind `MediaVideoSource` (TASK 21)
  and `AlertClipDecoder` (TASK 47) — video (`rawvideo bgra`, scaled) + audio
  (`f32le -ac 2 -ar 48000`, 50 ms chunks, byte-carry across reads), both paced to
  real time, loop restarts both so picture and sound stay together. Alert
  behaviour is unchanged.
- `MediaVideoSourceManager` applies the layer's Level/Mute per session and raises
  `AudioAvailable`; `MainViewModel` feeds it to the new
  `AudioMixer.EnqueueDesktopMediaAudio`, which joins the DESKTOP/GAME ring so the
  desktop fader and the mic auto-ducker apply exactly as they do for game audio
  and TRAX (creator ruling 2026-11-05: audio joins the group).
- Per-layer `MediaMuted` + existing `MediaVolume` surfaced as Mute/Level in the
  properties panel; `MediaMuted` persisted (ALTER TABLE guard + SELECT/INSERT).
- Level/mute are per SESSION because one decoder is shared per file path — the
  last layer touched wins; one decoder per layer would play the file twice.

Test: `MediaSourceAudioTests` — the pipe emits 50 ms chunks at 48k stereo, the
manager scales by level and drops when muted, and a media layer's sound moves the
desktop meter. Alert/decode suites unchanged and green.

Docs: task-21 closed on item 10, the "silent soundtrack" facts corrected in
frame-pipeline/output/overlays, HANDOFF rewritten. verify.sh green (529 passed;
the two documented pre-existing failures only).
This commit is contained in:
2026-11-05 07:54:06 -08:00
parent 776f84fb88
commit 2670ac022b
21 changed files with 669 additions and 328 deletions
+5 -6
View File
@@ -97,12 +97,11 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
`AlertClipDecoder.RunVideoAsync` and `MediaVideoSource.RunAsync` are near-duplicates —
same ffmpeg args (`-f rawvideo -pix_fmt bgra -vf scale=W:H -an -`), same
`RawVideoFrameReader` feed loop, same `IFrameRateProbe` + delay pacing, both behind
`IDecodeProcess`. What differs is ownership and role, not decoding: alerts are per-play
and disposed at drain, media is a refcounted per-path session; alerts also run an
`f32le` audio pipe into the mixer, media runs none at all (`-an`) — so a media file's
soundtrack is silent today, and task-21 item 10 holds the two routes for changing that;
alerts play once, media loops. A shared raw-video pump
is the obvious consolidation if a third consumer appears — do not do it for two.
`IDecodeProcess`. Consolidated 2026-11-05 with the media audio slice: both are now thin wrappers over
one `Services/VideoClipPipe.cs` (video pipe + `f32le` audio pipe, real-time paced, loop restarts both),
so decode behaviour cannot drift between them again. What still differs is ownership and role: alerts
are per-play and disposed at drain, media is a refcounted per-path session; alerts play once, media
loops; alert audio is mixed at unity and never ducked, media audio joins the desktop/game group.
Fades in/out via **`FadeDurationSeconds = 0.30`**
(`AlertOverlayLayer` const): fade-in by `elapsed` on straight-alpha frame copies;
EOF → freeze-frame → fade-out → drain (the clip branch of `Advance` **clears