# 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.
