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 5f9b657..e1b5997 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 @@ -13,18 +13,17 @@ mod channel; mod filter; +mod spectrum; -use std::f32::consts::PI; use std::sync::{Arc, Mutex}; use num_complex::Complex; -use rustfft::num_complex::Complex as FftComplex; -use rustfft::FftPlanner; use tokio::sync::broadcast; use trx_core::rig::state::RigMode; pub use self::channel::ChannelDsp; pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter}; +use self::spectrum::SpectrumSnapshotter; // --------------------------------------------------------------------------- // IQ source abstraction @@ -175,12 +174,6 @@ impl SdrPipeline { pub const IQ_BLOCK_SIZE: usize = 4096; -/// Number of FFT bins for the spectrum display. -const SPECTRUM_FFT_SIZE: usize = 1024; - -/// Update the spectrum buffer every this many IQ blocks (~10 Hz at 1.92 MHz / 4096 block). -const SPECTRUM_UPDATE_BLOCKS: usize = 4; - fn iq_read_loop( mut source: Box, sdr_sample_rate: u32, @@ -198,14 +191,7 @@ fn iq_read_loop( }; let throttle = !source.is_blocking(); - // Pre-compute Hann window coefficients. - let hann_window: Vec = (0..SPECTRUM_FFT_SIZE) - .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos())) - .collect(); - - let mut planner = FftPlanner::::new(); - let fft = planner.plan_fft_forward(SPECTRUM_FFT_SIZE); - let mut spectrum_counter: usize = 0; + let mut spectrum = SpectrumSnapshotter::new(); let mut read_error_streak: u32 = 0; loop { @@ -283,34 +269,7 @@ fn iq_read_loop( } } - // Periodically compute and store a spectrum snapshot. - spectrum_counter += 1; - if spectrum_counter >= SPECTRUM_UPDATE_BLOCKS { - spectrum_counter = 0; - let take = n.min(SPECTRUM_FFT_SIZE); - let mut buf: Vec> = samples[..take] - .iter() - .enumerate() - .map(|(i, s)| FftComplex::new(s.re * hann_window[i], s.im * hann_window[i])) - .collect(); - buf.resize(SPECTRUM_FFT_SIZE, FftComplex::new(0.0, 0.0)); - fft.process(&mut buf); - - // FFT-shift: rearrange so negative frequencies come first (DC in centre). - let half = SPECTRUM_FFT_SIZE / 2; - let bins: Vec = buf[half..] - .iter() - .chain(buf[..half].iter()) - .map(|c| { - let mag = (c.re * c.re + c.im * c.im).sqrt() / SPECTRUM_FFT_SIZE as f32; - 20.0 * (mag.max(1e-10_f32)).log10() - }) - .collect(); - - if let Ok(mut guard) = spectrum_buf.lock() { - *guard = Some(bins); - } - } + spectrum.update(samples, &spectrum_buf); if throttle { std::thread::sleep(std::time::Duration::from_millis(block_duration_ms)); diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/spectrum.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/spectrum.rs new file mode 100644 index 0000000..1d54ab5 --- /dev/null +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/spectrum.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::f32::consts::PI; +use std::sync::{Arc, Mutex}; + +use num_complex::Complex; +use rustfft::num_complex::Complex as FftComplex; +use rustfft::FftPlanner; + +/// Number of FFT bins for the spectrum display. +pub(super) const SPECTRUM_FFT_SIZE: usize = 1024; + +/// Update the spectrum buffer every this many IQ blocks (~10 Hz at 1.92 MHz / 4096 block). +pub(super) const SPECTRUM_UPDATE_BLOCKS: usize = 4; + +pub(super) struct SpectrumSnapshotter { + hann_window: Vec, + fft: std::sync::Arc>, + counter: usize, +} + +impl SpectrumSnapshotter { + pub(super) fn new() -> Self { + let hann_window: Vec = (0..SPECTRUM_FFT_SIZE) + .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos())) + .collect(); + + let mut planner = FftPlanner::::new(); + let fft = planner.plan_fft_forward(SPECTRUM_FFT_SIZE); + + Self { + hann_window, + fft, + counter: 0, + } + } + + pub(super) fn update( + &mut self, + samples: &[Complex], + spectrum_buf: &Arc>>>, + ) { + self.counter += 1; + if self.counter < SPECTRUM_UPDATE_BLOCKS { + return; + } + self.counter = 0; + + let take = samples.len().min(SPECTRUM_FFT_SIZE); + let mut buf: Vec> = samples[..take] + .iter() + .enumerate() + .map(|(i, sample)| { + FftComplex::new( + sample.re * self.hann_window[i], + sample.im * self.hann_window[i], + ) + }) + .collect(); + buf.resize(SPECTRUM_FFT_SIZE, FftComplex::new(0.0, 0.0)); + self.fft.process(&mut buf); + + let half = SPECTRUM_FFT_SIZE / 2; + let bins: Vec = buf[half..] + .iter() + .chain(buf[..half].iter()) + .map(|value| { + let mag = + (value.re * value.re + value.im * value.im).sqrt() / SPECTRUM_FFT_SIZE as f32; + 20.0 * mag.max(1e-10_f32).log10() + }) + .collect(); + + if let Ok(mut guard) = spectrum_buf.lock() { + *guard = Some(bins); + } + } +}