[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,
|
Usb,
|
||||||
/// Lower sideband SSB: negate imaginary part before taking real part.
|
/// Lower sideband SSB: negate imaginary part before taking real part.
|
||||||
Lsb,
|
Lsb,
|
||||||
/// AM envelope detector: magnitude of IQ, DC-removed.
|
/// AM coherent detector using a limiter-derived carrier reference.
|
||||||
Am,
|
Am,
|
||||||
/// Narrow-band FM: instantaneous frequency via quadrature.
|
/// Narrow-band FM: instantaneous frequency via quadrature.
|
||||||
Fm,
|
Fm,
|
||||||
|
|||||||
@@ -4,12 +4,42 @@
|
|||||||
|
|
||||||
use num_complex::Complex;
|
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> {
|
pub(super) fn demod_am(samples: &[Complex<f32>]) -> Vec<f32> {
|
||||||
samples
|
const EPSILON: f32 = 1.0e-12;
|
||||||
.iter()
|
const REF_BLEND: f32 = 0.08;
|
||||||
.map(|sample| (sample.re * sample.re + sample.im * sample.im).sqrt())
|
|
||||||
.collect()
|
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)]
|
#[cfg(test)]
|
||||||
@@ -53,4 +83,21 @@ mod tests {
|
|||||||
assert_approx_eq(got, exp, 1e-6, &format!("AM sample {idx}"));
|
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]
|
#[test]
|
||||||
fn channel_dsp_processes_silence() {
|
fn channel_dsp_processes_silence() {
|
||||||
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(8);
|
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(
|
let mut dsp = ChannelDsp::new(
|
||||||
0.0,
|
0.0,
|
||||||
&RigMode::USB,
|
&RigMode::USB,
|
||||||
@@ -490,6 +491,7 @@ mod tests {
|
|||||||
true,
|
true,
|
||||||
31,
|
31,
|
||||||
pcm_tx,
|
pcm_tx,
|
||||||
|
iq_tx,
|
||||||
);
|
);
|
||||||
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
|
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
|
||||||
dsp.process_block(&block);
|
dsp.process_block(&block);
|
||||||
@@ -498,6 +500,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn channel_dsp_set_mode() {
|
fn channel_dsp_set_mode() {
|
||||||
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
||||||
|
let (iq_tx, _) = broadcast::channel::<Vec<Complex<f32>>>(8);
|
||||||
let mut dsp = ChannelDsp::new(
|
let mut dsp = ChannelDsp::new(
|
||||||
0.0,
|
0.0,
|
||||||
&RigMode::USB,
|
&RigMode::USB,
|
||||||
@@ -510,6 +513,7 @@ mod tests {
|
|||||||
true,
|
true,
|
||||||
31,
|
31,
|
||||||
pcm_tx,
|
pcm_tx,
|
||||||
|
iq_tx,
|
||||||
);
|
);
|
||||||
assert_eq!(dsp.demodulator, Demodulator::Usb);
|
assert_eq!(dsp.demodulator, Demodulator::Usb);
|
||||||
dsp.set_mode(&RigMode::FM);
|
dsp.set_mode(&RigMode::FM);
|
||||||
|
|||||||
Reference in New Issue
Block a user