Loudnorm per transmission#1142
Open
TheGreatCodeholio wants to merge 2 commits into
Open
Conversation
…alls
The old two-pass linear loudnorm computed one gain offset for the entire concatenated call and applied it as a single static gain. Inter-transmission loudness variation was preserved by design — a quiet capture and a loud capture in the same call stayed quiet and loud relative to each other.
New pipeline runs a single ffmpeg invocation with -filter_complex each transmission gets its own cleanup+loudnorm chain, all chains feed the concat filter, then a brick-wall alimiter as the final stage. Each transmission lands at the same target loudness regardless of capture level.
CPU drops ~40-50% vs. the old two-pass default (no separate analysis pass), roughly equivalent to the old single-pass mode.
Changes:
- Remove analyze_loudnorm_from_concat + LoudnormMeasured + analysis-pass
helpers (~150 lines of now-dead code).
- Remove loudnorm_two_pass config field. The new architecture has no
two-pass mode. The field was also a latent bug — it was never copied
from the system into call_info, so user config never took effect.
- New voice-tuned defaults: loudnorm_tp -0.1 -> -1.5, loudnorm_lra 11 -> 7.
- Add final_limiter config (default true). Ceiling = loudnorm_tp + 0.5 dB,
attack 1 ms, release 50 ms.
- Custom ffmpeg_filter now runs per-transmission instead of per-call.
…er switch for all audio processing The analog recorder's hardcoded 300/3000 Hz FIR bandpass moves out of the GNURadio chain into audio_postprocess as configurable highpass_hz/lowpass_hz defaults. This makes the bandpass user-tunable per system, drops two realtime FIR filters from every analog recorder, and means the original transmission WAV files now contain real post-demod/post-deemph discriminator audio. outputRawAudio and transmissionArchive in raw mode actually deliver what they advertise. To match the existing analog sound out of the box, audio_postprocess defaults shift: enabled true (was false), highpass_hz 300 (was 0), lowpass_hz 3000 (was 0). The biquad rolloff differs slightly from the GR-chain FIR but the difference should be unnoticeable. audio_postprocess.enabled is also redefined as the master switch for the entire post-processing pipeline. enabled=false now skips cleanup, loudnorm, and the final limiter — the call's main file becomes a stream-copy concat of the raw transmission WAVs. Loudnorm and final_limiter only take effect when enabled=true. Config parser logs a WARNING when the user explicitly sets enabled=false so the semantic change can't surprise anyone silently. Updated CONFIGURE.md including a migration note
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.
Per-transmission audio post-processing pipeline
Summary
Two related changes that together rework how call audio is filtered and normalized:
Per-transmission loudnorm + final limiter — the old two-pass linear loudnorm applied a single static gain to the entire concatenated call, so a quiet capture and a loud capture in the same call stayed quiet and loud
relative to each other. Now each transmission is independently cleaned, normalized, and limited before being concatenated, so speech sits at the same target loudness across every transmission in a call.
Analog bandpass moves from GNURadio into ffmpeg post-processing — the hardcoded 300/3000 Hz FIR bandpass that lived inside
analog_recorderis nowaudio_postprocess.highpass_hz/lowpass_hz(defaulting to thosesame values). This makes the bandpass user-tunable, drops two realtime FIR filters from every analog recorder, and means the original transmission WAVs are now real post-demod / post-deemph discriminator audio — so
outputRawAudioandtransmissionArchivefinally deliver what they advertise.Along with that,
audio_postprocess.enabledbecomes the master switch for the whole pipeline.audio_postprocess.enabled=falseskips cleanup, loudnorm, and the limiter; the call's main file becomes a stream-copy concat of the rawtransmission WAVs (the same shape
outputRawAudioproduces as a side file).Why this matters
Measured on a real multi-transmission call (3 transmissions, ~20 s):
LRA dropped 7.6×, the peak no longer clips, and listeners no longer hear "whisper then shout" within a single call. Quiet transmissions get pulled up to the loudness target; loud ones get tamed.
The two-pass loudnorm pipeline also cost ~2× the CPU because it ran an analysis pass and then a render pass. The new pipeline is a single ffmpeg invocation, equivalent in cost to the old single-pass mode and roughly half the
cost of the old default (two-pass).
What changed
Audio post-processing pipeline
cleanup → loudnorm → limiter, then all per-tx chains feedconcat. Single ffmpeg invocation via-filter_complex.analyze_loudnorm_from_concat,LoudnormMeasured,build_loudnorm_analysis_filter,build_loudnorm_render_filter, and therun_process_capture_combined_outputplumbing theyused).
loudnorm_two_passconfig field. It was also a latent bug — it was never copied from the system into call_info, so the user's config never took effect anyway.final_limiterconfig (defaulttrue). Ceiling derived fromloudnorm_tp + 0.5 dB, attack 1 ms, release 50 ms.loudnorm_tp-0.1 → -1.5,loudnorm_lra11.0 → 7.0.enabledas master switchaudio_postprocess.enabled=falsenow skips the entire pipeline (cleanup + loudnorm + limiter), not just cleanup.loudnormandfinal_limiteronly take effect whenenabled=true.WARNat startup if a user has explicitly setenabled=falsein their config so the semantic change can't surprise anyone silently. Migration note added toCONFIGURE.md.Analog GR chain
high_f/low_fFIR blocks, their taps, and their connections fromanalog_recorder.{h,cc}. Chain is nowdecim_audio → squelch_two → levels → converter → wav_sink.decoder_sinkalready tapped atdecim_audio(pre-bandpass), so CTCSS/MDC/etc. decoder behavior is unchanged.audio_postprocess.enabled=true,highpass_hz=300,lowpass_hz=3000.transmissionArchive
audio_postprocess.enabled=trueandtransmissionArchive=true, per-tx archive files are written into the capture directory already processed (cleanup + loudnorm + limiter applied). They're produced in the same ffmpeg invocation asthe main render via
aspliton each per-tx pad, so no extra encode pass.enabled=false, archived per-tx files are copies of the recorder's raw WAVs (existing behavior — and they're now meaningfully more raw for analog systems).audio_archive || archive_files_on_failure), avoiding wasted disk writes.Custom
ffmpeg_filterenabled=true.loudnorm, the user's loudnorm runs per-tx (and the built-in loudnorm is skipped, same rule as before).Behavior changes / migration
Most users will see no behavior change beyond uniform speech loudness. The cases that do change:
audio_postprocess.enabled=falseset explicitlytransmissionArchive=true(withaudio_postprocess.enabled=true)ffmpeg_filtersetoutputRawAudio=true.raw.wavwas bandpass-filtered.raw.wavis true for analog that makes it post-demod / post-deemph discriminator audio for digital post vocoder audio