Skip to main content

Sweepers & Station IDs (sweepers)

Sweepers are the voice of the station: a voice tag ("LOKLOK FM…") mixed over a real music bed with the bed ducked under the voice, the voice landing on a musical beat. Station IDs (ID_*) are the shorter "who we are" stickers. All live in media/sweepers, are Break-tagged, and are R128-mastered to −14 LUFS / −1.5 dBTP so they sit at exactly the same loudness as the songs around them.

GeneratorFile prefixWhat it isTypical length
imaging.renderSweeperSWP_INTRO_*Voice over a real music bed, sidechain-ducked~8.5–9 s
imaging.renderSweeper (song-specific)SWP_DROP_*Voice sliced from an actual song, beat-aligned~8.5–9 s
imaging.renderVoiceDropSWP_VOX_*Voice over a pink-noise whoosh~3.5–4 s
imaging.renderStationIdID_*Station IDs~4–5 s

The sweeper recipe (renderSweeper)

Voice chain

deesser=i=0.12 → highpass 100 → presence EQ (4k +3dB, 300 −2dB)
→ acompressor (−24dB, 3.5:1) → alimiter 0.92 → adelay → asplit [vc][vcs]

The de-esser removes sibilance energy, the highpass kills room rumble, the presence EQ adds air at 4 kHz and de-muds at 300 Hz, and the compressor rides the level so the voice sits consistently over the bed.

Bed chain

lowpass 15k → stereotools (mlev=1, slev=0.6) → soft fades

The bed is low-passed to leave headroom for the voice and slightly narrowed (slev=0.6) so it does not fight the voice for the centre image.

Mix (the critical parts)

[vcs] apad=pad_dur=total ← CRITICAL
[bed0][vcsP] sidechaincompress=threshold=0.1:ratio=6:attack=8:release=350
→ amix=inputs=2:normalize=0:dropout_transition=0 → alimiter
→ two-pass normalizeR128 (−14 LUFS / −1.5 dBTP)

The apad trap: sidechaincompress truncates its output to the shorter of its two inputs. The voice is short and the bed is long, so without [vcs]apad=pad_dur=total the bed tail was cut to ~3 s. Padding the (voice-side) duck control to the full target length keeps the bed swelling back after the voice ends.

The ducking equation

The sidechain compressor drops the bed gain whenever the voice control signal is above threshold. In its simplest (dB-domain) form the gain reduction is:

GGR={0if VTR(VT)if V>TdB,R=6:1, T=0.1G_{GR} = \begin{cases} 0 & \text{if } V \le T \\[2pt] R \cdot (V - T) & \text{if } V > T \end{cases} \quad\text{dB},\qquad R = 6{:}1,\ T = 0.1

with attack = 8 ms (the bed ducks almost instantly under the voice) and release = 350 ms (the bed swells back smoothly after). The voice itself never passes through the sidechain — it stays full level; only the bed is pulled down.

Beat-aligned placement

Song-specific sweepers slice an actual track at a musically meaningful offset. detectBpmAndOnsets analyses the file, then beatAlignedStart picks the closest onset to the wanted time:

  • Intro lead-in: start the bed ~1.5 s into the song so the voice lands right after the intro hits.
  • Energy drop: start ~45 % of the way into the track (min(duration*0.45, 120)) — the chorus/build that carries energy.

Onset detection (tempogram)

The engine computes a BPM and an onset track from a 3-second spectral flux analysis:

flux(n)=kmax(0, Xk(n)Xk(n1))\mathrm{flux}(n) = \sum_k \max\bigl(0,\ X_k(n) - X_k(n-1)\bigr)

An autocorrelation over the flux (lag for candidate BPM lag = hopsPerSecond * 60 / bpm) selects the strongest candidate, and a harmonic check prefers 2·bpm when it correlates ≥ 5 % better — avoiding the half-time lock that makes 128 BPM read as 64:

score(bpm)=iflux(i)flux(i+lag(bpm))\mathrm{score}(bpm) = \sum_i \mathrm{flux}(i) \cdot \mathrm{flux}(i + \mathrm{lag}(bpm))

How the scheduler uses them

  • Sweepers are imaging elements: 10 min cooldown (ELEMENT_COOLDOWN), not the 3 h music cooldown.
  • liq_cross_duration = 0.5 (below the shortest ~3.5 s sweeper) so the 2 s cross pre-buffer never starves.
  • Regenerated every content refresh (archiveOldImaging(['SWP_','ID_','SFX_']) archives the previous set) — the deck used to bloat to 1125 files; it is now capped at ~25 (10 SWP_INTRO, 5 SWP_DROP, 10 ID_, 5 SWP_VOX).

Endpoints & CLI

  • POST /api/imaging/generate, POST /api/imaging/sweepers ({songDrops, ids, voiceDrops, archiveOld}).
  • MCP: generate_sweepers.
  • CLI: node scripts/run-imaging.js --sweepers N --ids N --vox N.

Verify on air

  1. media/sweepers holds the capped set — SWP_INTRO_*, SWP_DROP_*, SWP_VOX_*, ID_* — no runaway growth.
  2. m3u entries: annotate:type=sweepers,liq_cross_duration=0.5:.
  3. ffprobe: ~8.5–9 s stereo, Break-tagged, R128 ≈ −14 LUFS.
  4. Aurally: the bed should duck under the voice then swell back after it — that swell is the apad fix doing its job.