[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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user