Repository navigation
The mixer knobs that are still constants #20
Closed
robert-dean
started this conversation in
Ideas
Replies: 1 comment
|
Closing: built in #808, and the deciding question turned out not to need answering. The duck was already live in
Three of the six went differently:
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Written: 2026-08-09, when
VOICE_GAIN_DBbecame the fourth of them.Updated: 2026-08-11, when the containers learned to restart themselves onto a re-rendered config,
which removes the reason this was deferred.
Updated: 2026-10-10, when #421 made the duck a level rather than an amount and added two more.
State of the tree: six values that decide how a break sounds are hardcoded in
apps/api/src/modules/stream/stream.service.ts, materialized intoradio.env, and changed only byediting code and deploying.
What they are
radio.envkeyTALK_OVER_TRACKSTALK_OVER_TRACKSDUCK_GAIN_DBDUCK_GAIN_DBDUCK_MIN_DBDUCK_MIN_DBDUCK_BED_LUFSDUCK_BED_LUFSDUCK_FADE_MSDUCK_FADE_MSVOICE_GAIN_DBVOICE_GAIN_DBTwo of the six are not as constant as the table makes them look.
DUCK_BED_LUFSis only a fallback:the app stamps every cue it arms with its own figure (
deadair_duck_bed_lufs, fromduckBedLufsForin
playout/gain.ts), so the bed already followsplayout.targetLufsand the speech trim without arestart. Making it a setting changes only what an unstamped cue gets. And
DUCK_FADE_MSis exported,because the talk-up allows for the bed coming back up after the last word, so a setting for it has to
reach the talk-up as well as
radio.env, or the two ends disagree about how long the ramp is.Every one of them is something an operator tunes by ear, over several listens, on their own library
and their own speech engine. That is exactly the profile of a setting and exactly not the profile of
a constant.
The seam
STREAM_KEYSin apps/api/src/modules/stream/stream.settings.tsand
resolveStreamSettingsbeside it. Every other value the materializer writes already comesthrough there — the mount, the bitrate, the station name, all five secrets — so these six are the
only stream values that bypass a mechanism already built for them. Adding them is a
STREAM_KEYSentry, a field on
StreamSettings, a default, and passing them throughplayoutConfiginstead ofreading a module constant.
Why it is not just that
radio.liqreads all six at startup. So a settings row that changed while Liquidsoap wasrunning would re-render
radio.env(the app already does that on a settings write) and changenothing anyone can hear until the container restarts. A console knob that silently does nothing is
worse than no knob.
Which made the real work the restart trigger — and that trigger was built on 2026-08-11, for a
different reason, and this file is smaller because of it.
stream/config-watch.shruns inside eachstream container, polls the mtime of the file it was started from, and stops its own container when
it moves; the compose restart policy brings it back on the new config. No Docker socket anywhere,
and nothing can restart anything but itself, which is why the authority could live there and not
here.
stream.staleness.tsis the second line behind it: it warns only once the drift has outlivedDRIFT_GRACE_MS, by which point the self-restart demonstrably did not happen.So the remaining work is the small half after all: six
STREAM_KEYSentries, six fields onStreamSettings, six defaults, and passing them throughplayoutConfig. Writing the settingre-renders
radio.env, the watch sees the mtime move, and Liquidsoap comes back on the new valuewithout anybody being asked to restart anything.
One decision is left, and it is about cost rather than mechanism. A restart takes the station off
air for as long as the container takes to come back, which is audible to anyone listening. Three
answers, still undecided:
broadcast. Honest, and puts the cost in front of the person choosing it.
when nobody is listening, and means a saved change can sit unapplied for hours with no visible
reason why.
radio.liq— read per use from a ref, refreshed by a/control/*call —so no restart is needed at all. The most work, and the only answer with no cost to a listener.
Check how
duck_gain_dbis used before assuming it is possible:lin_of_dBis evaluated once,when the ramp closure is built.
What must NOT move
CONTROL_TTL_SandPLAYOUT_PREFETCHlook like they belong on the same list and do not. Both comefrom constants in
apps/api/src/modules/playout/liquidsoap.control.ts
that the app uses for its own behaviour as well: the TTL is what the pusher renews against, and the
prefetch is the same number as its push lead. They are shared deliberately, so the two ends of the
bridge cannot disagree. Splitting either into a setting gives it two owners and one place for them
to drift, and the failure — a lease that expires between renewals, a skip that lands on an unfetched
item — is intermittent and hard to attribute.
Moved from
docs/todo/mixer-settings-in-db.md. The file as it last stood: https://github.com/robert-dean/deadair/blob/71e431d45ce7b35a189c7d226832ec4a6247e9dc/docs/todo/mixer-settings-in-db.mdAll reactions