[fix](trx-backend-soapysdr): demodulate CW as USB (real part, not envelope)
CW signals in SDR are centred at an audio offset (e.g. 700 Hz) by the upstream FIR filter, so demodulating as USB (taking the real part) produces the correct side-tone. The previous magnitude/envelope approach produced a DC pulse per key press with no audible tone. Re-enable the DC blocker for CW/CWR (r = 0.9999): the output is now audio that can carry a DC offset from BFO frequency error, identical to USB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -577,17 +577,14 @@ fn demod_fm(samples: &[Complex<f32>]) -> Vec<f32> {
|
||||
// CW
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// CW envelope detector: magnitude of IQ.
|
||||
/// CW demodulator: take the real part of each baseband IQ sample.
|
||||
///
|
||||
/// Returns the raw envelope amplitude. Level normalisation is handled
|
||||
/// downstream by the per-channel AGC. No DC blocker is applied to CW because
|
||||
/// the envelope is always non-negative; high-pass filtering would create
|
||||
/// negative-going artifacts on each key release.
|
||||
/// The upstream FIR filter centres the CW carrier at the configured audio
|
||||
/// offset (e.g. 700 Hz), so demodulating identically to USB produces the
|
||||
/// characteristic CW side-tone. Level normalisation is handled downstream
|
||||
/// by the per-channel AGC.
|
||||
fn demod_cw(samples: &[Complex<f32>]) -> Vec<f32> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|s| (s.re * s.re + s.im * s.im).sqrt())
|
||||
.collect()
|
||||
samples.iter().map(|s| s.re).collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -705,18 +702,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Test 7: CW envelope detector returns raw magnitudes.
|
||||
// Normalisation is handled downstream by the AGC.
|
||||
// Test 7: CW demodulator returns the real part (same as USB).
|
||||
#[test]
|
||||
fn test_cw_raw_magnitude() {
|
||||
fn test_cw_takes_real_part() {
|
||||
let input = vec![
|
||||
Complex::new(3.0_f32, 4.0), // magnitude 5.0
|
||||
Complex::new(0.0, 0.0), // magnitude 0.0
|
||||
Complex::new(1.0, 0.0), // magnitude 1.0
|
||||
Complex::new(3.0_f32, 4.0),
|
||||
Complex::new(0.0, 0.0),
|
||||
Complex::new(1.0, 0.0),
|
||||
];
|
||||
let out = Demodulator::Cw.demodulate(&input);
|
||||
assert_eq!(out.len(), 3);
|
||||
assert_approx_eq(out[0], 5.0, 1e-6, "CW sample 0");
|
||||
assert_approx_eq(out[0], 3.0, 1e-6, "CW sample 0");
|
||||
assert_approx_eq(out[1], 0.0, 1e-6, "CW sample 1");
|
||||
assert_approx_eq(out[2], 1.0, 1e-6, "CW sample 2");
|
||||
}
|
||||
|
||||
@@ -269,14 +269,13 @@ fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
|
||||
|
||||
/// Build the DC blocker for a given mode, or `None` if not applicable.
|
||||
///
|
||||
/// CW and WFM are excluded: CW envelope is always non-negative (DC blocking
|
||||
/// would create negative artifacts on key releases), and WFM 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.
|
||||
/// 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).
|
||||
fn dc_for_mode(mode: &RigMode) -> Option<DcBlocker> {
|
||||
match mode {
|
||||
RigMode::CW | RigMode::CWR | RigMode::WFM => None,
|
||||
RigMode::WFM => None,
|
||||
RigMode::AM => Some(DcBlocker::new(0.999)),
|
||||
_ => Some(DcBlocker::new(0.9999)),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user