Files
ytLlive/HANDOFF.md
T
gramps 6d71acef8c TASK 9 audio milestone: real stream audio + voice filters + auto-duck + free TRAX music — the mixer now feeds the encoder real audio (ffmpeg reads a Windows named pipe, -f f32le -ar 48000 -ac 2 -i \.\pipe\ytllive_audio, replacing anullsrc silence; explicit -map 0:v -map 1:a via EncoderOptions.AudioPipeName), with honest gains reaching the stream (MicVolume scales mic, GameAudioVolume + mute scale loopback, Func<double> seams on AudioMixer), an always-on pure-C# voice chain on the mic BEFORE the meter and mix (LowShelfFilter 120Hz +4dB → HighShelfFilter 8kHz +3dB → NoiseGate 0.005/hysteresis 0.5 → Compressor 0.5 4:1, all TDF2), AutoDucker (mic RMS>0.02 → loopback ×0.25, attack 0.05/release 0.005), and TRAX free background music (MusicPlayer = MediaFoundationReader → VolumeWaveProvider16 at fixed 0.20 → WaveOutEvent via the new sibling NAudio.WinMM 2.2.1 package; plays to the default device so the existing loopback carries it, ducked with game; footer TRAX button with red/yellow/green status dot, left-click toggles/picks, right-click opens the OpenFileDialog picker, tooltip shows the track name; persisted via schema v9 single-row Music). Build-time deviations from the plan (recorded in ai.md + TASKS.md): WasapiCapture in NAudio 2.2.1 exposes no overridable GetDefaultMixFormat so sources run device mix format and the mixer's TinyResampler normalizes any rate to 48kHz (resampler IS the design); FfmpegEncoder.cs untouched — MainViewModel.StopStream calls _audioMixer.StopLive() (pipe EOF) before the frame pump stops; sound-bar relabelled 'Desktop Audio', IsGameAudioBarVisible = game sound OR music playing. Tests: new AudioPipelineTests (DSP/ring-buffer/ducker/resampler units + the ONE integration test Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe reading real pipe bytes; ring-buffer overwrite bug found + fixed), FfmpegEncoderTests/LayoutStorePersistenceTests updated — 196 tests passing, 0 warnings
2026-08-14 18:09:26 -07:00

137 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — session state
> Current operational state, read right after `TASKS.md`. Trust this file as the
> truth of what is in flight — do not re-derive from git/fs unless it points at
> a problem. Conventions: [`schema.md`](schema.md). Rewrite this file at session
> end, compaction, or any interruption.
## Session state (last updated: 2026-08-14)
- **Branch:** `main` (audio milestone work is uncommitted, see "Uncommitted").
- **AUDIO MILESTONE (TASK 9) — SHIPPED this session.** Real stream audio + voice filters + auto-duck
+ free TRAX background music. Build **0 warnings**, full suite **196 passing** (ONE integration
test: `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe`). Everything below in the
milestone section is DONE. **NOT yet committed** — next step is review + commit (with `ai.md`/
`TASKS.md`/this file already updated in the same changeset).
- **Two build-time deviations from the settled plan (both recorded in ai.md + TASKS.md):**
1. `MusicPlayer` uses **`WaveOutEvent`** (new sibling package `NAudio.WinMM` 2.2.1) — not `WasapiOut`
as planned; `WaveOutEvent` isn't in `NAudio.Wasapi`/`NAudio.Core`.
2. No forced 48 kHz capture — NAudio 2.2.1's `WasapiCapture` has no overridable
`GetDefaultMixFormat`, so both sources run the device mix format and the mixer's
`TinyResampler` normalizes any rate to 48 kHz (resampler IS the design, not a fallback).
3. `FfmpegEncoder.cs` is **untouched** — the VM closes the audio pipe (`_audioMixer.StopLive()`) in
`StopStream` BEFORE the frame pump stops (audio EOF then video EOF, in order).
- **SERVER RECOVERY (llamachile.tube / YunoHost / DO droplet) — done earlier session.** Side task
complete; **rotate the DO API token** (control-panel only) and any password no longer trusted.
- **Shipped, all pushed before this session:** TASK 21 (creator-hub About, `3d92bd0`), TASK 8 (meter
+10 dB), TASK 4 ship step 7 (one-click go-live + private-only). **174 tests passing, 0 warnings.**
- **Uncommitted (whole TASK 9 milestone):** `Services/Audio/` (new `VoiceFilterChain.cs`,
`AudioRingBuffer.cs`, `AutoDucker.cs`, `TinyResampler.cs`, `MusicPlayer.cs`, `NamedPipeAudioWriter.cs`,
rewritten `AudioMixer.cs`, simplified `WasapiMicAudioSource.cs`), `Models/Music.cs` (new),
`FfmpegArgs.cs`, `EncoderOptions.cs`, `LayoutStore.cs` (v9), `MainViewModel.cs`, `MainWindow.xaml`,
`MainWindow.xaml.cs`, `ytLive.csproj` (NAudio.WinMM), `ytLive.Tests/` (`AudioPipelineTests.cs` new,
`FfmpegEncoderTests.cs` + `LayoutStorePersistenceTests.cs` updated), and docs `ai.md`, `TASKS.md`,
`HANDOFF.md`.
- **Next step:** commit the milestone (docs already staged in the same changeset), push, then start
**TASK 5** (reusable stream → `_rtmpUrlProvider` — the last blocker before go-live actually encodes).
Then **itch.io product page setup** (premium URL lands in `MainViewModel.PremiumUrl`, lights up
Unlock Premium). Optional, not queued: rewriting the healed entry's `ProfileUrl` to
`https://mastodon.llamachile.tube/@gramps` (user must say the word).
## TASK 9 audio milestone — SHIPPED 2026-08-14 (what changed)
The creator's feature review settled this as the single next branch ("all the audio issues done and
tested — a huge milestone"). Final spec and full shipped-state records live in `TASKS.md` (TASK 9)
and `ai.md` ("Live audio capture"). Highlights:
- **Real audio into the encoder.** `FfmpegArgs` now builds `-f f32le -ar 48000 -ac 2 -i \\.\pipe\ytllive_audio`
+ explicit `-map 0:v -map 1:a` (replaces `anullsrc` silence). `MainViewModel.BeginGoLive`
`_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName)`; `StopStream``_audioMixer.StopLive()`
before `_framePump.StopAsync()`.
- **2-input mix (mic + loopback)**, honest gains: `micGain = MicVolume` (mute = 0),
`loopbackGain = GameMuted ? 0 : GameAudioVolume` × duck. No third music channel.
- **Voice chain on the mic** (TASK 22), before meter AND mix: bass 120 Hz +4 dB → treble 8 kHz +3 dB →
gate (0.005 / hysteresis 0.5) → compressor (0.5, 4:1). Pure TDF2 DSP, per-sample, always on.
- **Auto-duck:** mic RMS > 0.02 → loopback ×0.25 (12 dB), attack 0.05 / release 0.005.
- **TRAX (free BGM):** `MusicPlayer` = MediaFoundationReader → `VolumeWaveProvider16` at fixed **0.20**
`WaveOutEvent`. Plays to the default device → rides the loopback into the stream (ducked with game).
Footer TRAX button (dot red/yellow/green + "TRAX"), left-click toggles/picks, right-click opens the
picker (`OpenFileDialog`), tooltip shows the track name. Track persists via **schema v9** single-row
`Music`. Sound-bar label "Game Audio Capture" → **"Desktop Audio"**;
`IsGameAudioBarVisible = gameDetectorProducingSound || IsMusicPlaying`.
- **Tests:** new `AudioPipelineTests.cs` (DSP/ring-buffer/ducker/resampler units + the ONE integration
test reading real pipe bytes via `NamedPipeClientStream`); `FfmpegEncoderTests` + layout persistence
updated for pipe args / Music roundtrip. Ring-buffer overwrite bug found by the unit test and fixed
(head must NOT advance on eviction — the write itself advances it). Integration test pre-fills the
ring buffers before `StartLive` so the first pipe tick already carries audio (deterministic — a
start-of-stream silence race was seen and eliminated).
**Out of scope this branch:** IP webcam, chat box, alt-key crop, credits, bg removal, music-off-VOD
track (YouTube mutes VODs with copyrighted music — future feature), itch.io `PremiumUrl`.
- **Landmines:**
- Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host
for the one WPF App per AppDomain; round-clip + source-naming tests).
- 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.
- The mic dot is red until a resource connects (see contract below) — green
only after `Started`, yellow on `Failed`.
- 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.
- `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. Same rule for audio: `StopLive()` (pipe EOF) before the pump
stop, so ffmpeg's two inputs end in order.
- Tests never instantiate `MainViewModel` directly except via a real `MainWindow`
on the `RealAppHost` STA thread (round-clip + naming), which never go live.
- Sandbox can't reach outbound HTTPS — `HttpSocialValidator` stub-handler tests
only, never the real instance.
- **Mic status contract (creator's rule, verified — do NOT "fix"):** the status dot is
**red until a mic resource is actually connected**. `MicStatus` starts `NotConnected`
(red); it goes green ONLY when the mixer's `MicConnected` fires, which comes strictly
from the mic source's `Started` event raised after `StartRecording()` succeeds. Zero
devices at startup → stays red and the mixer is never started; capture failure → yellow.
- **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`;
OAuth session token in `Helpers/TokenStore.cs` (DPAPI → `%APPDATA%\ytLlive\ytLlive.auth`);
layout DB `%APPDATA%\ytLlive\ytLlive.db` (**schema v9** — single-row `Music`; the
`SocialEntry.Software` column is a column-presence migration like the others); OAuth callback
`http://localhost:8765/oauth2/callback`; crash log `%APPDATA%\ytLlive\startup.log`.
## Server recovery (llamachile.tube / YunoHost / DO) — 2026-08-14
**Facts (hunted this session — do not re-hunt):**
- Droplet **llamachile.tube**: DO droplet ID `473190301`, IP `143.244.176.131`, sfo3, 4GB/2vCPU/50GB.
**SSH port = 2214** (matches git remote `ssh://llgit.llamachile.tube:2214/gramps/ytLlive.git`);
22/2222/2200/etc all closed. SSH as `gramps` works with `~/.ssh/id_ed25519` (key auth, no password).
- **Root is YunoHost-locked**: `PermitRootLogin no` in `/etc/ssh/sshd_config` (there's a conflicting
cloud-init override section below it, but DO's "Reset root password" email does NOT work on this
box — cloud-init `set-passwords` only ran once at install). Root is reachable via `sudo -i` or the
DO web Recovery Console (Settings → Recovery console — VNC-based; the droplet page's Console button
is SSH-based and fails with "all configured authentication methods failed" when no account has a
working password).
- **gramps is a YunoHost LDAP account** (local `/etc/passwd` entry has `*`, auth via pam_ldap) — its
password is changed with `sudo yunohost user update gramps -p '<pw>'`, NOT `passwd`.
- **fail2ban bans the whole IP for 10-12 min** after repeated failed SSH/root logins (all ports
refuse; `ping` still works; droplet API still shows `active`). Wait it out — do NOT power-cycle.
- Password reset this session: `sudo yunohost tools rootpw -n '<pw>'` sets root (rootpw takes `-n`).
Verification: `getent shadow root` shows a `$y$` yescrypt hash + `passwd -S root` = `P`.
- Mastodon services run as systemd units `mastodon-web/sidekiq/streaming` (all were `active` after
the reboot); gitea/nginx/mariadb/postgres/redis/yunohost-api also systemd units. Disk was **93% full**
(3.6G free) — keep an eye on it before any big upgrade.
- The original outage was an interrupted Mastodon upgrade + a DO `password_reset` reboot; everything
came back on its own after boot. No code/schema damage observed.