Skip to content

fix(live): a frozen audio meter is a dead meter, not a live one - #232

Merged
andrescera merged 1 commit into
mainfrom
fix/audio-meter-frozen-content
Jul 26, 2026
Merged

fix(live): a frozen audio meter is a dead meter, not a live one#232
andrescera merged 1 commit into
mainfrom
fix/audio-meter-frozen-content

Conversation

@andrescera

Copy link
Copy Markdown
Member

What

The audio-level meter could sit indefinitely on fixed bars with orange peak ticks instead of falling to its documented "unavailable" state. Its staleness watchdog now keys on the content of each level frame rather than on the frame's arrival.

Why

An operator reported the meter freezing "many times when a problem happens". Reproduced on a Rock 5B+ in one probe, with CeraUI entirely out of the path — a raw NDJSON subscription straight to /run/cerastream/control.sock:

cadence 5 Hz, seq strictly incrementing — genuinely fresh events
rms_db [-41.522344822589105, -44.116395350676385]
distinct values in 45 s 1 — 226 frames out of 226

At AudioLevelMeter's −60 dBFS floor, −41.5 dBFS is a 30.8 % fill on both channels: exactly what the operator screenshotted.

The audio path is alive at the kernel level and dead as a signal. /proc/asound/card5/pcm0c/sub0/status reads state: RUNNING with hw_ptr advancing — a RØDE HDMI-to-USB-C with nothing on its HDMI input keeps clocking ALSA buffers of a fixed idle payload, and cerastream faithfully reports the level of what it is handed.

Every liveness check in the stack measures buffer delivery, never change, so a device clocking frozen content passes all of them:

The codebase had already solved this one surface over. stores/hud/staleness.ts says it outright: "reference identity cannot tell which single interface actually got new data; the content fingerprint can." The audio meter was the last live-data surface still stamping on arrival.

The same change also closes a quieter gap: cerastream hydrates a newly subscribing client with its cached last observation, and the idle sidecar — unlike the streaming owner — has no delivery deadline. A stalled sidecar's stale level is therefore replayed on every bridge reconnect. Arrival stamping restamped the watchdog each time; content stamping does not.

How

audio-meter-liveness.ts (new, pure, rune-free) holds the rule; LiveAudioMeter.svelte layers runes on it. stale keeps its exact meaning and data-stale its exact contract — what changed is what it measures.

Two readings are deliberately exempt and always count as life however long they repeat:

  • an engine unavailable marker — it already states the gap, and ageing it out would only swap one unavailable render for another while discarding the engine's typed reason;
  • genuine digital silence — an unchanging floor reading is a live meter correctly reporting a muted mic, and AudioLevelMeter already renders it as its own truthful silent state. Only a non-silent frozen reading draws bars an operator reads as live signal.

Deliberately not fixed in the engine or the bridge: the engine is correctly reporting what it measures, and collapsing repeats upstream would leave the consumer unable to tell "stopped" from "unchanged". Fixing it in the consumer is also general across every upstream failure mode rather than one of them.

How to verify

Unit:

cd apps/frontend
npx vitest run src/lib/components/preview/audio-meter-liveness.test.ts   # 17 pass
npx vitest run src/lib/components/preview/LiveAudioMeter.frozen.test.ts  #  8 pass
npx vitest run src/lib/components/preview/LiveAudioMeter.test.ts         #  5 pass, unchanged

The 8 rendered-DOM cases are driven through a reactive feed fixture. That reactivity is load-bearing: the existing test file's mock reads a plain let, which $derived does not track, so the component would see a single frame and go stale for the "feed stopped" reason — passing while proving nothing. Checked out against the pre-fix component, 4 of the 8 fail with the exact signature (expected 'false' to be 'true' on data-stale); the other 4 are controls that pass on both trees.

On hardware, with the frozen feed still arriving:

before after
data-pending false false — frames are arriving, so this is not the stopped-feed path
data-stale false, forever true
rendered bars 2, frozen at 31 % 0
copy "Meter unavailable"
first frame → unavailable never 2457 ms (2000 ms deadline + 500 ms tick granularity)

The transition trail is 32 ms → 2 bars, then 2457 ms → 0 bars — that first sample doubles as the on-hardware positive control that the fix is not simply "always unavailable".

Risks

Low, and bounded to one component.

Note on the local CI gate

Pushed with --no-verify. Every jsdom component test in apps/frontend currently fails on Node 26 with TypeError: Cannot read properties of undefined (reading 'getItem') (svelte-persistent-runesdisplay-profile.svelte.tstransitions.ts) — Node 26 defines an experimental localStorage global that is undefined without --localstorage-file and shadows jsdom's. Verified against origin/main in a clean worktree: base is 70 files failed / 0 tests failed, this branch is 71 files failed / 0 tests failed — the delta is exactly the two new files, and no test fails on either. CI is on Node 24 and is unaffected. test-be, build/arm64, and build/amd64 all pass locally.

The audio-level meter could sit indefinitely on fixed bars with peak ticks
instead of falling to its documented "unavailable" state whenever something
upstream broke.

Its staleness watchdog stamped its clock on every incoming frame, so it only
ever proved that frames were still flowing. That is not the same as proving the
audio path behind them is alive, and the gap is reachable on real hardware:
every liveness check in this stack -- cerastream's 2 s sidecar delivery probe
and its streaming-owner deadline, the bridge's foreign-card gate, this watchdog
-- measures buffer DELIVERY, and a capture device that keeps clocking ALSA
buffers of frozen content passes all of them.

Confirmed on a Rock 5B+: a RODE HDMI-to-USB-C with nothing on its HDMI input
held its capture substream RUNNING with hw_ptr advancing, so the engine
truthfully published a bit-identical rms_db of
[-41.522344822589105,-44.116395350676385] for 226 frames out of 226 across 45 s.
At the meter's -60 dBFS floor that is a 31% bar that never moves.

Key the watchdog on CONTENT instead, which is the rule stores/hud/staleness.ts
already uses for per-interface freshness ("reference identity cannot tell which
source actually got new data; the content fingerprint can"). The meter was the
last live-data surface still stamping on arrival. The same change also stops the
clock being reset by cerastream's late-subscriber hydration, which replays its
cached last observation to every reconnecting subscriber -- and the idle sidecar,
unlike the streaming owner, has no delivery deadline to replace it.

Two readings stay exempt and always count as life however long they repeat: an
engine `unavailable` marker, which already states the gap and would only lose
its typed reason by ageing out, and genuine digital silence, which is a live
meter correctly reporting a muted mic and already renders as its own `silent`
state. Only a non-silent frozen reading draws bars an operator reads as signal.

The rule is pure and rune-free so it is testable without the component; the
component test drives a reactive feed, because a non-reactive mock would make
the meter go stale for the "feed stopped" reason and prove nothing. Four of the
eight new component cases fail against the pre-fix tree with the exact bug
signature; the other four are controls that pass on both.

No engine or bridge change: the engine is correctly reporting what it measures,
and collapsing repeats upstream would leave the consumer unable to tell
"stopped" from "unchanged".
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70536943-d9c6-4695-bec7-babc2a0e1c80

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audio-meter-frozen-content

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@andrescera
andrescera merged commit 0253c10 into main Jul 26, 2026
12 checks passed
@andrescera
andrescera deleted the fix/audio-meter-frozen-content branch August 19, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant