[feat](trx-backend-soapysdr): improve AM coherent demodulation
Replace the raw AM envelope detector with a limiter-derived coherent detector and update backend tests for the current channel DSP constructor. Co-authored-by: Stan Grams <sjg@haxx.space> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -135,7 +135,7 @@ pub enum Demodulator {
|
||||
Usb,
|
||||
/// Lower sideband SSB: negate imaginary part before taking real part.
|
||||
Lsb,
|
||||
/// AM envelope detector: magnitude of IQ, DC-removed.
|
||||
/// AM coherent detector using a limiter-derived carrier reference.
|
||||
Am,
|
||||
/// Narrow-band FM: instantaneous frequency via quadrature.
|
||||
Fm,
|
||||
|
||||
@@ -4,12 +4,42 @@
|
||||
|
||||
use num_complex::Complex;
|
||||
|
||||
/// AM envelope detector: magnitude of IQ.
|
||||
/// AM demodulator using a limiter-derived coherent reference.
|
||||
///
|
||||
/// This is a practical DSP analogue of the patent's split/limit/mix approach:
|
||||
/// derive a fixed-amplitude carrier reference from the incoming IQ, smooth that
|
||||
/// reference to follow carrier phase/frequency, then mix the original branch by
|
||||
/// the conjugate reference and take the in-phase projection.
|
||||
pub(super) fn demod_am(samples: &[Complex<f32>]) -> Vec<f32> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| (sample.re * sample.re + sample.im * sample.im).sqrt())
|
||||
.collect()
|
||||
const EPSILON: f32 = 1.0e-12;
|
||||
const REF_BLEND: f32 = 0.08;
|
||||
|
||||
let mut out = Vec::with_capacity(samples.len());
|
||||
let mut carrier_ref = Complex::new(1.0_f32, 0.0);
|
||||
|
||||
for &sample in samples {
|
||||
let mag_sq = sample.re * sample.re + sample.im * sample.im;
|
||||
if mag_sq <= EPSILON {
|
||||
out.push(0.0);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mag = mag_sq.sqrt();
|
||||
let limited = sample / mag;
|
||||
let blended = carrier_ref * (1.0 - REF_BLEND) + limited * REF_BLEND;
|
||||
let blended_mag_sq = blended.re * blended.re + blended.im * blended.im;
|
||||
if blended_mag_sq > EPSILON {
|
||||
carrier_ref = blended / blended_mag_sq.sqrt();
|
||||
} else {
|
||||
carrier_ref = limited;
|
||||
}
|
||||
|
||||
// Project the original signal onto the limiter-derived carrier phase.
|
||||
let mixed = sample * carrier_ref.conj();
|
||||
out.push(mixed.re.max(0.0));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -53,4 +83,21 @@ mod tests {
|
||||
assert_approx_eq(got, exp, 1e-6, &format!("AM sample {idx}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_am_tracks_rotating_carrier_reference() {
|
||||
let input: Vec<Complex<f32>> = (0..16)
|
||||
.map(|idx| {
|
||||
let angle = idx as f32 * 0.05;
|
||||
Complex::new(angle.cos(), angle.sin())
|
||||
})
|
||||
.collect();
|
||||
let out = demod_am(&input);
|
||||
assert_eq!(out.len(), input.len());
|
||||
let avg = out.iter().skip(1).copied().sum::<f32>() / (out.len().saturating_sub(1) as f32);
|
||||
assert!(
|
||||
avg > 0.95,
|
||||
"AM rotating carrier: expected strong average coherent output, got {avg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +478,7 @@ mod tests {
|
||||
#[test]
|
||||
fn channel_dsp_processes_silence() {
|
||||
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(8);
|
||||
let (iq_tx, _iq_rx) = broadcast::channel::<Vec<Complex<f32>>>(8);
|
||||
let mut dsp = ChannelDsp::new(
|
||||
0.0,
|
||||
&RigMode::USB,
|
||||
@@ -490,6 +491,7 @@ mod tests {
|
||||
true,
|
||||
31,
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
|
||||
dsp.process_block(&block);
|
||||
@@ -498,6 +500,7 @@ mod tests {
|
||||
#[test]
|
||||
fn channel_dsp_set_mode() {
|
||||
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
||||
let (iq_tx, _) = broadcast::channel::<Vec<Complex<f32>>>(8);
|
||||
let mut dsp = ChannelDsp::new(
|
||||
0.0,
|
||||
&RigMode::USB,
|
||||
@@ -510,6 +513,7 @@ mod tests {
|
||||
true,
|
||||
31,
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
assert_eq!(dsp.demodulator, Demodulator::Usb);
|
||||
dsp.set_mode(&RigMode::FM);
|
||||
|
||||
Reference in New Issue
Block a user