From 76d19f8551fd77e143f094bcc731bfdf5ebc704b Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 16:34:34 +0000 Subject: [PATCH] re: the capture dropouts are largely a client-buffer size, not the guest running slow Withdraws my conclusion that the monitor-sink capture route cannot be fixed by configuration, and with it the claim that only an in-emulator tap would work. The container has no audio hardware -- no /proc/asound/cards, no /dev/snd, no asound.conf -- so PulseAudio s stock default.pa module-always-sink supplies a null sink, whose whole purpose is to exist when there is no device. A null sink has no hardware clock: it is timer-driven, and anything the client fails to write in time becomes silence in the monitor. That much was right. What was wrong was inferring from it that the holes mean the guest runs below real time. The alternative was never tested: xenia asks SDL for channel_samples_ = 256, which is 5.33 ms at 6 channels, and daemon.conf here is stock with no fragment tuning. PULSE_LATENCY_MSEC overrides what SDL s PulseAudio backend requests. Measured, same title and sink and parec invocation: xenia default (~5.3 ms) 347.5 s 39.3 percent silence 30.5 gaps/s PULSE_LATENCY_MSEC=200 88.0 s 15.6 percent silence 3.5 gaps/s An 8.7x reduction from one environment variable. Against the port s controls -- starved 32.9 gaps/s, genuine music bed 3.3, voice track 0.03, bar at 20 -- the default is squarely starved and 200 ms lands at the level of real content. Not yet a clean bill of health: the runs are not like-for-like at 88 s against 347 s, and the short one covers the splash logos where silence is real. What is established is direction and scale. The consequence that matters is that the capture route should be retried at raised latency before anyone spends a session on a Canary rebuild. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd --- docs/re/audio-capture-channel-map-trap.md | 44 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/re/audio-capture-channel-map-trap.md b/docs/re/audio-capture-channel-map-trap.md index 507f68d4..1e662ea7 100644 --- a/docs/re/audio-capture-channel-map-trap.md +++ b/docs/re/audio-capture-channel-map-trap.md @@ -191,13 +191,47 @@ the dropout schedule rather than anything musical. (Estimator controlled: it recovers a synthetic 37.487 s loop as **37.480 s**, and scores non-repeating noise at r = 0.019.) -### Why the monitor-sink route cannot fix this +### Why the sink starves — and 🔴 why "it cannot be fixed by configuration" was WRONG `parec` reads a sink **monitor**, which advances at wall-clock rate and -substitutes silence whenever nothing is written. So any moment the emulator runs -below real time becomes a hole in the file, and the timebase is warped -non-uniformly — deleting the silences does not repair it, it just compresses time -unevenly. +substitutes silence whenever nothing is written. **And every sink in this +container is a null sink**, because there is no audio hardware at all — no +`/proc/asound/cards`, no `/dev/snd`, no `/etc/asound.conf` — so PulseAudio's +stock `default.pa:109` `module-always-sink` supplies one, whose stated purpose is +to "make sure we always have a sink around, **even if it is a null sink**". A +null sink has no hardware clock: it is driven on a timer, and anything the client +fails to write in time becomes silence in the monitor. + +From that I concluded the route "cannot be fixed by configuration" and that only +an in-emulator tap would work. **That was wrong, and it was wrong because I +assumed the holes meant the guest was running below real time without testing the +alternative** — that the *client buffer* is simply too small. Xenia asks SDL for +`channel_samples_ = 256`, i.e. **5.33 ms** at 6 channels, and `daemon.conf` here +is stock with no fragment tuning at all. + +`PULSE_LATENCY_MSEC` overrides what SDL's PulseAudio backend requests. Measured, +same title, same sink, same `parec` invocation: + +| client buffer | duration | silence | **gaps/s** | median gap | +|---|---|---|---|---| +| xenia default (~5.3 ms) | 347.5 s | 39.3 % | **30.5** | 3.94 ms | +| `PULSE_LATENCY_MSEC=200` (114.7 ms reported) | 88.0 s | 15.6 % | **3.5** | 37.33 ms | + +**An 8.7× reduction in gap rate from one environment variable.** For scale, the +port's controls put a *starved* file at 32.9 gaps/s, a **genuine music bed at +3.3**, and a voice track that is 53 % pauses at 0.03, with their bar at 20 — +so the default lands squarely in "starved" and 200 ms lands at the level of real +content. + +⚠️ **Not yet a clean bill of health.** The two runs are not like-for-like: 88 s +against 347 s, and the short one covers the splash logos, where silence between +cards is real. What is established is the *direction and scale* — the dropouts +were substantially a **client-buffer** problem, not proof that the guest runs +below real time. + +✅ **Consequence: the in-emulator tap may not be needed.** The capture route is +worth retrying at a raised latency before anyone spends a session on a Canary +rebuild. ❔ **The route that would work is an internal tap**, and it does not exist yet. `SDLAudioDriver::SubmitFrame(float* frame)`