Skip to content

fix(broadcast): micro edge fades on DJ voice clips to kill boundary clicks#830

Merged
perminder-klair merged 1 commit into
developfrom
fix/voice-edge-fade
Jul 4, 2026
Merged

fix(broadcast): micro edge fades on DJ voice clips to kill boundary clicks#830
perminder-klair merged 1 commit into
developfrom
fix/voice-edge-fade

Conversation

@perminder-klair

Copy link
Copy Markdown
Owner

What

Adds a 40 ms fade-in/fade-out to every spoken clip on both DJ voice channels (voice_queue / intro_queue) in radio.liq, applied per track before amplify/mic_chain.

Why

The voice WAV/MP3 previously started and ended dry. Some TTS engines cut the file hard at the boundary, and the mic-chain compressor's +7 dB makeup gain amplifies that discontinuity into an audible click/tick at the start or end of a spoken segment. The existing softness (0.8 s smooth_add duck ramps + silent lead-in) shapes the music around the voice but never touched the voice file's own edges.

40 ms is below speech-onset perception, so the first word is not softened; the silent lead-in clip gets faded too, which is harmless.

Performance

Effectively zero: fade.in/fade.out are amplitude envelopes (one multiply per sample during the 40 ms windows, passthrough otherwise) on a source that is silent most of the time — noise next to the MP3/Opus encoders already running.

Verification

liquidsoap --check passes against savonet/liquidsoap:v2.4.4 (the broadcast image base), confirming the fades type-check on the request.queue sources with no "Early computation of source content-type" error (the failure mode that blocked HPF/EQ here).

…licks

Some TTS engines cut the WAV hard at the file boundary; once the mic-chain
compressor's makeup gain lifts the clip, that hard edge lands as an audible
click. Fade each spoken clip in/out over 40 ms — below speech-onset
perception, so the first word is untouched. Applied per track on both voice
queues, before amplify/mic_chain so the compressor never sees the raw edge.

Verified with liquidsoap --check against savonet/liquidsoap:v2.4.4 (the
broadcast image base).
@perminder-klair perminder-klair marked this pull request as ready for review July 4, 2026 12:00
@perminder-klair perminder-klair merged commit 9f6737c into develop Jul 4, 2026
2 checks passed
@perminder-klair perminder-klair deleted the fix/voice-edge-fade branch July 4, 2026 12:00
perminder-klair added a commit that referenced this pull request Jul 4, 2026
… kills the whole clip (#837)

* fix(broadcast): drop fade.out from voice edge_fade — it silenced every DJ clip

fade.out on a request.queue source doesn't know the track's remaining time
in this Liquidsoap build, so it treats the whole clip as inside the fade
zone and multiplies it to ~0: since #830 every voice segment (links, idents,
hourly, weather, request intros) aired as dead air — listeners heard only
the duck engaging as a 3-4s volume dip mid-song.

Verified empirically against the broadcast image: a sine pushed through the
exact chain renders at rms≈13600 plain, ≈13500 with fade.in only, and 0
with the shipped fade.in+fade.out composition. fade.in alone is kept — it
still kills the head-boundary click #830 targeted, and is proven harmless.
On-air check after deploy: manual /dj/segment station-id shows voice peaks
riding the duck instead of silence.

* feat(tts): bake 40ms edge fades into rendered voice clips

Render time is the only place a clip's true length is known, so the tail
fade that can't live in radio.liq (see previous commit) is applied here.
audio/wav-edges.ts applies a 40ms linear head+tail ramp in-place to
canonical PCM WAVs (16-bit int and 32-bit float); anything else — notably
the cloud engine's mp3 output, where encoder padding already avoids edge
clicks — is left untouched. Best-effort by design: a clip never fails to
air because polish couldn't be applied.

Wired into both success paths of tts.speak(), so every engine and every
voice kind gets faded edges from the one chokepoint. Verified on rendered
on-air WAVs: heads ramp from 0, tails land at 0, speech body untouched.
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