From 332ad4448b6511bdd0eeff99d65c4695a3a35a53 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 28 Feb 2026 17:00:32 +0100 Subject: [PATCH] [fix](trx-backend-soapysdr): fix AM demodulation quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs made the AM path sound wrong: 1. AGC attack too fast (5 ms). The slowest audio a broadcast AM station can transmit is ~50 Hz (20 ms period). A 5 ms attack lets the AGC track individual audio cycles, which causes severe pumping and amplitude distortion. Change to 500 ms attack / 5 s release so the AGC only responds to slow carrier-amplitude fading, not the audio modulation itself. 2. Bandwidth too narrow. The IQ filter cutoff is audio_bandwidth_hz / 2, so the previous 6 000 Hz setting gave only 3 kHz audio bandwidth. AM broadcast sidebands extend to ±4.5–5 kHz; raise the default to 12 000 (cutoff 6 kHz) to cover the full audio band. 3. DC blocker rate inconsistent. For AM the demodulated magnitude is always ≥ 0 and the DC component equals the carrier amplitude; only true DC needs removing. Unify all non-WFM modes to r = 0.9999 (corner ≈ 0.76 Hz @ 48 kHz), which strips carrier DC without touching any audible bass content. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-backend-soapysdr/src/dsp.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs index e76ae82..f71f116 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs @@ -255,28 +255,30 @@ impl BlockFirFilter { /// Build the AGC for a given mode. /// -/// CW uses fast attack/release to follow individual dots and dashes. -/// AM uses a moderate release so the AGC tracks fading carriers. -/// All other modes use a longer release suitable for voice and data signals. +/// AM AGC must be far slower than audio modulation. With a 50 Hz bass +/// component the modulation period is 20 ms; an attack faster than that +/// causes the AGC to follow the audio envelope and distort (pumping). +/// 500 ms / 5 s only reacts to slow carrier-amplitude fading, not audio. +/// +/// CW uses a fast attack/release to follow individual dots and dashes. +/// All other modes use 5 ms / 500 ms, suitable for SSB voice and FM. fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc { let sr = audio_sample_rate.max(1) as f32; match mode { RigMode::CW | RigMode::CWR => SoftAgc::new(sr, 1.0, 50.0, 0.5, 30.0), - RigMode::AM => SoftAgc::new(sr, 5.0, 200.0, 0.5, 30.0), + RigMode::AM => SoftAgc::new(sr, 500.0, 5_000.0, 0.5, 30.0), _ => SoftAgc::new(sr, 5.0, 500.0, 0.5, 30.0), } } /// Build the DC blocker for a given mode, or `None` if not applicable. /// -/// WFM is excluded because it has its own internal DC blockers on each output -/// channel. AM uses a slightly faster blocker (r = 0.999, corner ≈ 7.6 Hz -/// @ 48 kHz) so it can track slow carrier-amplitude fading. All other modes -/// (including CW, which now demodulates as USB) use r = 0.9999 (≈ 0.76 Hz). +/// WFM is excluded because it has its own internal DC blockers per channel. +/// All other modes use r = 0.9999 (corner ≈ 0.76 Hz @ 48 kHz), which strips +/// only true carrier DC without affecting any audible bass content. fn dc_for_mode(mode: &RigMode) -> Option { match mode { RigMode::WFM => None, - RigMode::AM => Some(DcBlocker::new(0.999)), _ => Some(DcBlocker::new(0.9999)), } } @@ -513,7 +515,7 @@ fn default_bandwidth_for_mode(mode: &RigMode) -> u32 { match mode { RigMode::LSB | RigMode::USB | RigMode::PKT | RigMode::DIG => 3_000, RigMode::CW | RigMode::CWR => 500, - RigMode::AM => 6_000, + RigMode::AM => 12_000, RigMode::FM => 12_500, RigMode::WFM => 180_000, RigMode::Other(_) => 3_000,