# 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 ''`, 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 ''` 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.