[refactor](trx-backend-soapysdr): extract spectrum snapshot helper

Move the spectrum FFT snapshot logic into a dedicated dsp module so dsp.rs stays focused on pipeline orchestration.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 13:58:30 +01:00
parent 08b8c80cc3
commit 56d6d12d9e
2 changed files with 84 additions and 45 deletions
@@ -13,18 +13,17 @@
mod channel; mod channel;
mod filter; mod filter;
mod spectrum;
use std::f32::consts::PI;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use num_complex::Complex; use num_complex::Complex;
use rustfft::num_complex::Complex as FftComplex;
use rustfft::FftPlanner;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use trx_core::rig::state::RigMode; use trx_core::rig::state::RigMode;
pub use self::channel::ChannelDsp; pub use self::channel::ChannelDsp;
pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter}; pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter};
use self::spectrum::SpectrumSnapshotter;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// IQ source abstraction // IQ source abstraction
@@ -175,12 +174,6 @@ impl SdrPipeline {
pub const IQ_BLOCK_SIZE: usize = 4096; 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( fn iq_read_loop(
mut source: Box<dyn IqSource>, mut source: Box<dyn IqSource>,
sdr_sample_rate: u32, sdr_sample_rate: u32,
@@ -198,14 +191,7 @@ fn iq_read_loop(
}; };
let throttle = !source.is_blocking(); let throttle = !source.is_blocking();
// Pre-compute Hann window coefficients. let mut spectrum = SpectrumSnapshotter::new();
let hann_window: Vec<f32> = (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::<f32>::new();
let fft = planner.plan_fft_forward(SPECTRUM_FFT_SIZE);
let mut spectrum_counter: usize = 0;
let mut read_error_streak: u32 = 0; let mut read_error_streak: u32 = 0;
loop { loop {
@@ -283,34 +269,7 @@ fn iq_read_loop(
} }
} }
// Periodically compute and store a spectrum snapshot. spectrum.update(samples, &spectrum_buf);
spectrum_counter += 1;
if spectrum_counter >= SPECTRUM_UPDATE_BLOCKS {
spectrum_counter = 0;
let take = n.min(SPECTRUM_FFT_SIZE);
let mut buf: Vec<FftComplex<f32>> = 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<f32> = 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);
}
}
if throttle { if throttle {
std::thread::sleep(std::time::Duration::from_millis(block_duration_ms)); std::thread::sleep(std::time::Duration::from_millis(block_duration_ms));
@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// 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<f32>,
fft: std::sync::Arc<dyn rustfft::Fft<f32>>,
counter: usize,
}
impl SpectrumSnapshotter {
pub(super) fn new() -> Self {
let hann_window: Vec<f32> = (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::<f32>::new();
let fft = planner.plan_fft_forward(SPECTRUM_FFT_SIZE);
Self {
hann_window,
fft,
counter: 0,
}
}
pub(super) fn update(
&mut self,
samples: &[Complex<f32>],
spectrum_buf: &Arc<Mutex<Option<Vec<f32>>>>,
) {
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<FftComplex<f32>> = 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<f32> = 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);
}
}
}