Commit 5367121e by PLN (Algolia)

docs(armada): the J-5 dropout, logged as the documentary it is

Every intuition on the way to this one was wrong, including several of mine, so the
log records the wrong turns as well as the answer — that is the part worth reading
back.

The shape: nothing crashed. sclang up, scsynth up on 6.7% CPU, sink RUNNING,
routing intact, and no sound. Any monitor aimed at SuperCollider would have
reported a healthy rig. PLN found the discriminator himself by switching the
default sink by hand — internal speaker plays, UMC does not — which killed the
pure-CPU theory and pointed at the interface, which was also not at fault: 128
vs 1024 period_size is an 8x tighter deadline, so the UMC is the canary.

Recorded because they will recur:
  - `state: RUNNING` lied while the device was dying once a second; only repeated
    hw_ptr sampling is honest, and FOUR samples, not two — my two-sample read
    called it "still stalling" when a ring-buffer wrap looks identical to a stall.
  - a rule scoped to one device but applied to the whole graph stops being a fix
    and becomes a load (52-no-suspend matched every node; two nodes nobody used
    held 2.1M xruns).
  - the laptop mic had auto-wired itself into the d1 stem, invisible in Ardour's
    GUI and in the saved session, and Take95 is already exported (#132).
  - #7 survived two fixes because the metric counted the BOUNDED map.
  - my own journalctl scans burned a core twice while he was losing audio. I was
    the load I was measuring.
parent 8cd149b2
# 2026-08-03 — The J-5 dropout: nothing crashed, and that was the problem
**Status:** resolved. **Tasks:** #7 (fixed), #132 (opened), #133 (fixed), #22 (fixed),
#128/#129/#130/#131/#134 (opened). **Commits:** `db42ffc`, `ba77c4f`, `a46db24`,
`b72defe`, `5945ccc`, `05df7fe`, `f9ec204`, `21a5577`.
Five days before OPAL, mid-rehearsal, the sound went glitchy and then silent. This is
the log of what it actually was, because almost every intuition along the way was wrong
— including several of mine.
## The symptom set that mattered
PLN, live: *"still seeing HLs, midi reacts colors, n sound forem computer vol up/down,
ardou dstill shows up, WOW its just back but for 3s glitchy as if struggling to keep
up?"* then *"silent again"*.
Highlights firing = Tidal alive and sending OSC. MIDI colours = the tap alive. **No
sound even from the system volume beep** = not a Tidal problem at all. And critically,
after `ctrl+.` (hush) the beep was *still* cut in half — with the music stopped, the
audio path was still broken.
## Everything healthy, and still silent
- `sclang` and `scsynth` both alive.
- UMC202HD sink present, `RUNNING`, default sink correct, mixer 127/127 unmuted.
- Full routing intact: `SuperCollider:out_1 → ardour:Tidal 01`, `ardour:Master → UMC`.
- **scsynth using 6.7% CPU.** The audio engine was the *small* consumer.
A monitor pointed at SuperCollider would have reported everything fine. That is the
lesson worth keeping: this class of fault is invisible to any check aimed at a component.
## The discriminator PLN found
He switched the default sink by hand: **"Speaker" → hears system sounds. "UMC202HD" →
nothing.** That one test killed the "CPU starvation" theory as a complete explanation
and pointed at the interface — and it was *also* not the interface's fault:
| | period_size | deadline |
|---|---|---|
| UMC202HD | 128 | **2.67 ms** |
| SOF internal speaker | 1024 | 21.3 ms |
An 8× tighter deadline. The UMC is the only device in the box with a real-time deadline
tight enough to miss, so it dies first and recovers last. **It is the canary, not the
fault.** Confirmed independently: `/sys/bus/usb/devices/1-3.3/devnum` unchanged across
samples, so not the 2026-07-26 USB re-enumeration bug either.
## The measurement that finally told the truth
`state: RUNNING` on the PCM was a lie — it read RUNNING while the device was dying.
Sampling `hw_ptr` repeatedly:
t1 state=XRUN hw_ptr=2976 delta=0
t2 state=RUNNING hw_ptr=720 delta=-2256 <- NEGATIVE: stream restarted
t3 state=RUNNING hw_ptr=3264 delta=2544
t4 state=RUNNING hw_ptr=1392 delta=-1872
t5 state=RUNNING hw_ptr=1200 delta=-192
t6 state=RUNNING hw_ptr=2689 delta=1489
Restarting roughly once a second, never getting past ~3000 frames (~60 ms) — exactly
"back for 3 seconds, glitchy, then silent".
**I got this wrong once first.** A two-sample read gave `28992 frames in 2s` and I
called it "STILL STALLING". That reading was ambiguous: a ring-buffer wrap looks
identical to a stall, and 28992 is also consistent with ~98% of realtime. Four samples
resolved it. Never verdict an intermittent fault on two samples.
## Root cause: a fix that became a load
`pw-top` named the cost:
sof_sdw.HiFi__Headset__source 1,421,369 xruns nothing used it
sof_sdw.HiFi__HDMI1__sink 704,081 xruns 2.8ms BUSY/cycle, no client
UMC202HD Line sink 32,338 xruns the victim
alsa_input.hw_U192k_0 14,338 xruns W/Q 1.24 — waits > a full quantum
**Two nodes nobody used produced 2.1 million of them.** `52-no-suspend.conf` matched
`~alsa_output.*` and `~alsa_input.*` — every node in the graph. It was written for a
real reason (a woken device eats the first note's attack) but applied graph-wide it held
six SOF sinks and a broken mic source permanently open *inside* the real-time graph.
HDMI1 appeared when the screen was mirrored for the WYSIWYG show.
> **A rule scoped to one device but applied to the whole graph stops being a fix and
> becomes a load.** Suspension is not the enemy — suspension of the device you are
> playing through is.
## The bycatch, which may matter more
`pw-link` showed `Headset__source:capture_FL → ardour:Tidal 01/audio_in 1`. Ardour
auto-connects physical inputs, so **the laptop microphone was summed into the input of
the track that records d1**, alongside SuperCollider. Invisible in Ardour's GUI and in
the saved session file. Take95 — already exported as a master and an 11-track album —
was recorded with it connected. That is #132, and it is the only open item from this
incident with a publishing deadline.
## The fix, and the result
`config/wireplumber/53-rig-graph.conf` disables the headset source and all three HDMI
sinks; `52-no-suspend.conf` narrowed to the UMC plus the internal Speaker (kept
deliberately — it is the fallback that *proved* the machine could still make sound).
Both are symlinked from the repo, so they survive a fresh checkout and there is one
source of truth.
load average 7.17 -> 4.80
UMC hw_ptr restarting ~1/s -> 48,145 frames/s across 6 samples, 0 restarts
UMC sink 32,338 xruns -> 0
## And #7, which was the jitter source all along
`pulsar` was at 86–105% throughout. Chasing it found that #7 had been "fixed" twice and
survived both times **because the metric watched the wrong map**: `#countMarkers()`
counted `this.markers` (position markers, bounded by file length) while
`this.highlights` — one marker plus *two live decorations* per entry, evicted never —
sat uncounted. "580 markers live vs 16 active" was a reading of the bounded structure.
It also explained PLN's crash dialogs: *"Cannot decorate a destroyed marker,
decoration-manager.js:220"*. `editorsGroupedByTextBufferId()` can hand back an editor
whose display layer is gone; `markBufferRange` on it returns a marker born destroyed and
`decorateMarker` throws inside a RAF callback. `isValid()` is a different question from
`isDestroyed()`, and only the former was being asked.
Fixed with an LRU cap (never evicting a *visible* entry; sweeping to 75% of the cap so
the fix is not itself a per-event cost) and by reporting the cache separately from the
position markers.
## Self-inflicted, worth recording
`journalctl --since` on a box up 7 days scans the whole journal. Wrapped by rtk it
**burned a full core, twice, while PLN was losing audio.** I was the load I was
measuring. Everything in `check-audio-graph.sh` is now `/proc`, `pw-link` and a bounded
`pw-top`; no journal scans.
I also told him to press `ctrl-alt-0` to kill the backdrop mid-crisis. It is bound to
Tidal's mute/unmute — my keymap choice, never checked against the other package's
keymap. That is #134.
## The standing gap this closed
`gig-up` proved the set **boots**. It had never once proved the rig has **margin**.
`tools/check-audio-graph.sh` is now a hard gate: recording integrity first (a dropout
you hear and redo; a stem with room noise you *ship*), then Master→UMC, then is the
interface clocking, then is the prune still live. `check-mix` reads the *saved* session;
this reads the *live* graph — different questions, and the saved file has been wrong
before.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment