fix(audio): make the idle level meter follow the operator's audio-source pick - #217
Merged
Merged
Conversation
…rce pick Selecting an audio source changed nothing about the idle meter. cerastream chose its own ALSA card while not streaming, entirely independent of config.asrc, so on a board carrying a RODE and a DJI Mic Mini the operator selected the RODE, the picker showed the RODE, and the meter reported the DJI - or "Meter unavailable". Nothing in CeraUI ever told the engine what had been chosen. config.asrc now reaches the engine. resolveMeterPreference() turns the picker value into the ALSA device the meter should prefer, reusing the same audioDevices map, alias reverse-lookup and hw:CARD= wrapping resolveAudioMode already uses for start, so the meter and the program leg cannot disagree about which card a pick names. It is deliberately not resolveAudioMode itself: this is the IDLE meter, it has no notion of network-embedded program audio, and it must keep following the card the picker is showing. Auto, both pipeline pseudo-sources and an unresolvable pick all yield null - the explicit "engine, choose for yourself" hand-back. The audio-meter bridge delivers it, because it already holds the one long-lived IDLE connection to the engine; cerastream-backend's client only exists while streaming. It pushes on every connect (the engine keeps no preference across a restart), and syncAudioMeterPreference() re-pushes from streaming.setConfig when asrc changed and from updateAudioDevices, since a re-enumeration can change which card an unchanged pick resolves to. reload-config carries it rather than switch-audio, which is stream-only and answers not_streaming exactly when the idle meter is running. The published client Zod-strips the additive audio.meter_device key, so it goes over rawRequest behind supportsMeterDevicePreference - fail-safe false, so an engine older than schema 0.9.0 is sent nothing and keeps auto-picking, which is what it did before. Absent means unchanged and null means Auto; they are not interchangeable, so a delay-only reload can never clear the preference. A refused push is logged and swallowed: the previous preference stands and the meter keeps running. This is a preference, not a pin, and the engine is what guarantees it - cerastream still demotes a selected card that delivers no samples, so choosing a powered-off receiver can never leave the meter dead.
Contributor
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Live page's Audio source selection now drives the idle audio-level meter.
config.asrcis resolved to an ALSA device and pushed to cerastream as the additivereload-config→audio.meter_devicefield (engine schema0.9.0).Backend-only. No frontend change: the picker, the meter component and the wire payload
they exchange are all untouched — the meter simply starts reporting the card the picker
is already showing.
Depends on CERALIVE/cerastream#72
for the engine half, but does NOT need it merged to land safely: the send is gated on the
engine advertising schema ≥ 0.9.0, so against today's engine this PR is a no-op.
Why
Found live during board QA. The operator selected the RØDE HDMI-to-USB-C,
SourceSectionshowed the RØDE, and the meter reported the DJI Mic Mini — or "Meter unavailable". The
engine picked its own idle card with no input from CeraUI, so with two healthy cards the
one whose vendor string sorts first simply won. Selecting an audio source changed nothing
about the meter, and there was no way for the operator to correct it.
How
resolveMeterPreference(asrc)(modules/streaming/audio.ts) turns the picker valueinto the device the meter should prefer, or
nullfor "engine, choose for yourself".It reuses the SAME
audioDevicesmap, alias reverse-lookup andhw:CARD=wrapping thatresolveAudioModealready uses forstart, so the meter and the program leg can neverdisagree about which card a pick names. It is deliberately NOT
resolveAudioMode: thisis the IDLE meter, it has no notion of network-embedded program audio, and it must keep
following the card the picker is showing.
nullcoversAUDIO_SOURCE_AUTO, bothpipeline pseudo-sources, an unset
asrc, and anything that resolves to no card.audio-meter-bridgedelivers it, because it already holds the ONE long-livedIDLE connection to the engine —
cerastream-backend's client only exists whilestreaming. It pushes on every connect (the engine keeps no preference across a restart),
and
syncAudioMeterPreference()re-pushes fromstreaming.setConfigwhenasrcchangedand from
updateAudioDevices, since a re-enumeration can change which card anunchanged pick resolves to.
reload-config, notswitch-audio.switch-audiois stream-only and answersnot_streamingexactly when the idle meter is running.reload-configalready carriesan
audiosection and is idle-safe on the engine side.rawRequestbehindsupportsMeterDevicePreference. The published@ceralive/cerastreamclient Zod-strips the additivemeter_devicekey, so the typedreloadConfig()would silently drop it — the same constraintaudio.modeandvideo_passthroughalready work around. The gate is fail-safefalse, so a pre-0.9.0engine is sent nothing and keeps auto-picking: exactly its current behaviour.
reloadAudioDelay'sdelay-only reload can never clear it);
nullmeans Auto. Not interchangeable.stands, and the next config change or reconnect re-pushes.
It is a preference, not a pin, and the engine guarantees that. cerastream only moves
the named card to the head of its candidate list; its delivery-confirmation demotion
(PR #71) is unchanged, so selecting a powered-off receiver still lands on a working card
rather than a dead meter. There is deliberately no CeraUI-side "force this device" path.
How to verify
Unit —
bun testinapps/backend(2256 pass):tests/audio-meter-bridge.test.ts— pushes the selected card on connect;"Auto"sendsan explicit
null; re-pushes after a source change; sends NOTHING to a pre-0.9.0 engine(and levels keep flowing); a refused reload leaves the meter running; syncing while down
is a silent no-op; plus the schema gate table.
tests/audio-sources.test.ts—resolveMeterPreference: alias, no-alias, everynullcase, and selector passthrough.
The three push assertions were confirmed RED against the un-wired bridge before the push
landed; the ten pre-existing bridge tests stayed green throughout.
On real hardware — Rock 5B+
192.168.78.131, RØDE HDMI-to-USB-C attached, operatorconfig already at
asrc: "USB audio"(untouched):and the meter then reported 41 live level events over 8 s, all
card:usbaudio, 0unavailable, with real moving audio (rms_db ≈ -31…-36, not the digital-silence floor).The
updateAudioDevicesre-push path was exercised live via SIGUSR2. A preference naming acard the engine does not enumerate was verified inert — the meter stayed on the RØDE and
never went unavailable.
Gates:
bun run lint(exit 0),apps/backendbun test2256 pass / 0 fail,tsc --noEmitclean,check:tech-debtOK.Two local pre-push lanes could not run here and are NOT regressions:
test-fefailsidentically on a clean
origin/maincheckout (a local jsdomlocalStoragequirk — all1319 frontend tests themselves pass), and the four e2e lanes fail at
playwright install-deps, the documented can't-run-locally caveat indocs/LOCAL-CI.md.Cloud CI is the authority for both.
Risks
PR inert — the engine advertises
0.8.0and is sent nothing. No flag day.rawRequestbypasses the typed client. Deliberate and pre-existing practice(
switch-audio,startwithaudio.mode); the payload is a two-key object and theengine validates it.
reload-configper connect and per audio re-enumeration. Cheap, idle-safe,and non-blocking; a failure cannot stall the meter.