diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs index d6796fd..3a60401 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs @@ -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, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/am.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/am.rs index 4320954..8c31e36 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/am.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/am.rs @@ -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]) -> Vec { - 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> = (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::() / (out.len().saturating_sub(1) as f32); + assert!( + avg > 0.95, + "AM rotating carrier: expected strong average coherent output, got {avg}" + ); + } } diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs index 6697187..febd39c 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs @@ -478,6 +478,7 @@ mod tests { #[test] fn channel_dsp_processes_silence() { let (pcm_tx, _pcm_rx) = broadcast::channel::>(8); + let (iq_tx, _iq_rx) = broadcast::channel::>>(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::>(8); + let (iq_tx, _) = broadcast::channel::>>(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);