[feat](trx-backend-soapysdr): extend squelch and boost high denoise
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -424,7 +424,8 @@ impl StereoDenoise {
|
||||
}
|
||||
WfmDenoiseLevel::Low => 1.0 - (1.0 - broadband_gain) * 0.35,
|
||||
WfmDenoiseLevel::Medium => 1.0 - (1.0 - broadband_gain) * 0.65,
|
||||
WfmDenoiseLevel::High => broadband_gain,
|
||||
// Extra attenuation profile for noisy stereo difference channels.
|
||||
WfmDenoiseLevel::High => broadband_gain.powf(1.45),
|
||||
};
|
||||
diff_i * effective_gain.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use num_complex::Complex;
|
||||
use tokio::sync::broadcast;
|
||||
use trx_core::rig::state::RigMode;
|
||||
|
||||
pub use self::channel::ChannelDsp;
|
||||
pub use self::channel::{ChannelDsp, VirtualSquelchConfig};
|
||||
pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter};
|
||||
use self::spectrum::SpectrumSnapshotter;
|
||||
|
||||
@@ -107,6 +107,7 @@ impl SdrPipeline {
|
||||
frame_duration_ms: u16,
|
||||
wfm_deemphasis_us: u32,
|
||||
wfm_stereo: bool,
|
||||
squelch_cfg: VirtualSquelchConfig,
|
||||
channels: &[(f64, RigMode, u32, usize)],
|
||||
) -> Self {
|
||||
const IQ_BROADCAST_CAPACITY: usize = 64;
|
||||
@@ -118,9 +119,16 @@ impl SdrPipeline {
|
||||
let mut iq_senders = Vec::with_capacity(channels.len());
|
||||
let mut channel_dsps: Vec<Arc<Mutex<ChannelDsp>>> = Vec::with_capacity(channels.len());
|
||||
|
||||
for &(channel_if_hz, ref mode, audio_bandwidth_hz, fir_taps) in channels {
|
||||
for (channel_idx, &(channel_if_hz, ref mode, audio_bandwidth_hz, fir_taps)) in
|
||||
channels.iter().enumerate()
|
||||
{
|
||||
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(PCM_BROADCAST_CAPACITY);
|
||||
let (iq_tx, _iq_rx) = broadcast::channel::<Vec<Complex<f32>>>(IQ_BROADCAST_CAPACITY);
|
||||
let channel_squelch_cfg = if channel_idx == 0 {
|
||||
squelch_cfg
|
||||
} else {
|
||||
VirtualSquelchConfig::default()
|
||||
};
|
||||
let dsp = ChannelDsp::new(
|
||||
channel_if_hz,
|
||||
mode,
|
||||
@@ -132,6 +140,7 @@ impl SdrPipeline {
|
||||
wfm_deemphasis_us,
|
||||
wfm_stereo,
|
||||
fir_taps,
|
||||
channel_squelch_cfg,
|
||||
pcm_tx.clone(),
|
||||
iq_tx.clone(),
|
||||
);
|
||||
@@ -405,6 +414,7 @@ mod tests {
|
||||
20,
|
||||
75,
|
||||
true,
|
||||
VirtualSquelchConfig::default(),
|
||||
&[(200_000.0, RigMode::USB, 3000, 64)],
|
||||
);
|
||||
assert_eq!(pipeline.pcm_senders.len(), 1);
|
||||
@@ -421,6 +431,7 @@ mod tests {
|
||||
20,
|
||||
75,
|
||||
true,
|
||||
VirtualSquelchConfig::default(),
|
||||
&[],
|
||||
);
|
||||
assert_eq!(pipeline.pcm_senders.len(), 0);
|
||||
|
||||
@@ -10,6 +10,88 @@ use crate::demod::{DcBlocker, Demodulator, SoftAgc, WfmStereoDecoder};
|
||||
|
||||
use super::{BlockFirFilterPair, IQ_BLOCK_SIZE};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VirtualSquelchConfig {
|
||||
pub enabled: bool,
|
||||
pub threshold_db: f32,
|
||||
pub hysteresis_db: f32,
|
||||
pub tail_blocks: u32,
|
||||
}
|
||||
|
||||
impl Default for VirtualSquelchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
threshold_db: -65.0,
|
||||
hysteresis_db: 3.0,
|
||||
tail_blocks: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct VirtualSquelch {
|
||||
cfg: VirtualSquelchConfig,
|
||||
open: bool,
|
||||
tail_countdown: u32,
|
||||
}
|
||||
|
||||
impl VirtualSquelch {
|
||||
fn new(cfg: VirtualSquelchConfig) -> Self {
|
||||
Self {
|
||||
cfg,
|
||||
open: !cfg.enabled,
|
||||
tail_countdown: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.open = !self.cfg.enabled;
|
||||
self.tail_countdown = 0;
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
if self.cfg.enabled == enabled {
|
||||
return;
|
||||
}
|
||||
self.cfg.enabled = enabled;
|
||||
self.reset();
|
||||
}
|
||||
|
||||
fn set_threshold_db(&mut self, threshold_db: f32) {
|
||||
self.cfg.threshold_db = threshold_db;
|
||||
self.reset();
|
||||
}
|
||||
|
||||
fn supports_mode(mode: &RigMode) -> bool {
|
||||
!matches!(mode, RigMode::WFM)
|
||||
}
|
||||
|
||||
fn update(&mut self, mode: &RigMode, level_db: f32) -> bool {
|
||||
if !self.cfg.enabled || !Self::supports_mode(mode) {
|
||||
self.open = true;
|
||||
self.tail_countdown = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
let close_threshold_db = self.cfg.threshold_db - self.cfg.hysteresis_db.max(0.0);
|
||||
if self.open {
|
||||
if level_db >= close_threshold_db {
|
||||
self.tail_countdown = self.cfg.tail_blocks;
|
||||
} else if self.tail_countdown > 0 {
|
||||
self.tail_countdown -= 1;
|
||||
} else {
|
||||
self.open = false;
|
||||
}
|
||||
} else if level_db >= self.cfg.threshold_db {
|
||||
self.open = true;
|
||||
self.tail_countdown = self.cfg.tail_blocks;
|
||||
}
|
||||
|
||||
self.open
|
||||
}
|
||||
}
|
||||
|
||||
fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
|
||||
let sr = audio_sample_rate.max(1) as f32;
|
||||
match mode {
|
||||
@@ -87,6 +169,7 @@ pub struct ChannelDsp {
|
||||
audio_agc: SoftAgc,
|
||||
audio_dc: Option<DcBlocker>,
|
||||
processing_enabled: bool,
|
||||
squelch: VirtualSquelch,
|
||||
}
|
||||
|
||||
impl ChannelDsp {
|
||||
@@ -186,6 +269,7 @@ impl ChannelDsp {
|
||||
wfm_deemphasis_us: u32,
|
||||
wfm_stereo: bool,
|
||||
fir_taps: usize,
|
||||
squelch_cfg: VirtualSquelchConfig,
|
||||
pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||
) -> Self {
|
||||
@@ -264,6 +348,7 @@ impl ChannelDsp {
|
||||
audio_agc: agc_for_mode(mode, audio_sample_rate),
|
||||
audio_dc: dc_for_mode(mode),
|
||||
processing_enabled: true,
|
||||
squelch: VirtualSquelch::new(squelch_cfg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,12 +356,18 @@ impl ChannelDsp {
|
||||
self.processing_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_squelch(&mut self, enabled: bool, threshold_db: f32) {
|
||||
self.squelch.set_enabled(enabled);
|
||||
self.squelch.set_threshold_db(threshold_db);
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: &RigMode) {
|
||||
self.mode = mode.clone();
|
||||
if *mode != RigMode::WFM {
|
||||
self.audio_bandwidth_hz = default_bandwidth_for_mode(mode);
|
||||
}
|
||||
self.demodulator = Demodulator::for_mode(mode);
|
||||
self.squelch.reset();
|
||||
self.rebuild_filters(true);
|
||||
}
|
||||
|
||||
@@ -427,6 +518,12 @@ impl ChannelDsp {
|
||||
}
|
||||
}
|
||||
|
||||
let signal_power = decimated
|
||||
.iter()
|
||||
.map(|s| s.re * s.re + s.im * s.im)
|
||||
.sum::<f32>()
|
||||
/ decimated.len() as f32;
|
||||
let signal_db = 10.0 * signal_power.max(1e-12).log10();
|
||||
if self.wfm_decoder.is_some() {
|
||||
for sample in decimated.iter_mut() {
|
||||
let mag = (sample.re * sample.re + sample.im * sample.im).sqrt();
|
||||
@@ -437,7 +534,7 @@ impl ChannelDsp {
|
||||
}
|
||||
|
||||
const WFM_OUTPUT_GAIN: f32 = 0.50;
|
||||
let audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||
let mut audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||
let mut out = decoder.process_iq(decimated);
|
||||
for sample in &mut out {
|
||||
*sample = (*sample * WFM_OUTPUT_GAIN).clamp(-1.0, 1.0);
|
||||
@@ -462,6 +559,9 @@ impl ChannelDsp {
|
||||
raw
|
||||
}
|
||||
};
|
||||
if !self.squelch.update(&self.mode, signal_db) {
|
||||
audio.fill(0.0);
|
||||
}
|
||||
|
||||
self.frame_buf.extend_from_slice(&audio);
|
||||
while self.frame_buf.len().saturating_sub(self.frame_buf_offset) >= self.frame_size {
|
||||
@@ -499,6 +599,7 @@ mod tests {
|
||||
75,
|
||||
true,
|
||||
31,
|
||||
VirtualSquelchConfig::default(),
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
@@ -521,6 +622,7 @@ mod tests {
|
||||
75,
|
||||
true,
|
||||
31,
|
||||
VirtualSquelchConfig::default(),
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
|
||||
@@ -49,6 +49,10 @@ pub struct SoapySdrRig {
|
||||
gain_db: f64,
|
||||
/// Optional hard ceiling for the applied hardware gain in dB.
|
||||
max_gain_db: Option<f64>,
|
||||
/// Whether software squelch is enabled on primary channel (except WFM mode).
|
||||
squelch_enabled: bool,
|
||||
/// Software squelch threshold (dBFS) on primary channel.
|
||||
squelch_threshold_db: f32,
|
||||
/// Hidden AIS decoder channels (A and B) when available.
|
||||
ais_channel_indices: Option<(usize, usize)>,
|
||||
}
|
||||
@@ -90,6 +94,10 @@ impl SoapySdrRig {
|
||||
/// - `center_offset_hz`: the hardware is tuned this many Hz *below* the
|
||||
/// dial frequency so the desired signal lands off-DC. The DSP mixer
|
||||
/// shifts it back. Pass 0 to tune exactly to the dial frequency.
|
||||
/// - `squelch_enabled`: enable software squelch for all modes except WFM.
|
||||
/// - `squelch_threshold_db`: squelch open threshold in dBFS.
|
||||
/// - `squelch_hysteresis_db`: close hysteresis in dB.
|
||||
/// - `squelch_tail_ms`: tail hold time in milliseconds.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_with_config(
|
||||
args: &str,
|
||||
@@ -106,6 +114,10 @@ impl SoapySdrRig {
|
||||
sdr_sample_rate: u32,
|
||||
bandwidth_hz: u32,
|
||||
center_offset_hz: i64,
|
||||
squelch_enabled: bool,
|
||||
squelch_threshold_db: f32,
|
||||
squelch_hysteresis_db: f32,
|
||||
squelch_tail_ms: u32,
|
||||
) -> DynResult<Self> {
|
||||
tracing::info!(
|
||||
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
|
||||
@@ -161,6 +173,16 @@ impl SoapySdrRig {
|
||||
25_000,
|
||||
96,
|
||||
));
|
||||
let block_ms = if sdr_sample_rate == 0 {
|
||||
0.0
|
||||
} else {
|
||||
dsp::IQ_BLOCK_SIZE as f64 * 1000.0 / sdr_sample_rate as f64
|
||||
};
|
||||
let squelch_tail_blocks = if block_ms <= 0.0 {
|
||||
0
|
||||
} else {
|
||||
(squelch_tail_ms as f64 / block_ms).ceil().max(0.0) as u32
|
||||
};
|
||||
|
||||
let pipeline = dsp::SdrPipeline::start(
|
||||
iq_source,
|
||||
@@ -170,6 +192,12 @@ impl SoapySdrRig {
|
||||
frame_duration_ms,
|
||||
wfm_deemphasis_us,
|
||||
true, // wfm_stereo: enabled by default
|
||||
dsp::VirtualSquelchConfig {
|
||||
enabled: squelch_enabled,
|
||||
threshold_db: squelch_threshold_db,
|
||||
hysteresis_db: squelch_hysteresis_db,
|
||||
tail_blocks: squelch_tail_blocks,
|
||||
},
|
||||
&all_channels,
|
||||
);
|
||||
|
||||
@@ -244,6 +272,8 @@ impl SoapySdrRig {
|
||||
wfm_denoise: WfmDenoiseLevel::Auto,
|
||||
gain_db,
|
||||
max_gain_db,
|
||||
squelch_enabled,
|
||||
squelch_threshold_db,
|
||||
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
||||
};
|
||||
rig.apply_ais_channel_activity();
|
||||
@@ -269,6 +299,10 @@ impl SoapySdrRig {
|
||||
1_920_000,
|
||||
1_500_000, // bandwidth_hz
|
||||
0, // center_offset_hz
|
||||
false, // squelch_enabled
|
||||
-65.0, // squelch_threshold_db
|
||||
3.0, // squelch_hysteresis_db
|
||||
180, // squelch_tail_ms
|
||||
)
|
||||
}
|
||||
|
||||
@@ -497,6 +531,30 @@ impl RigCat for SoapySdrRig {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_sdr_squelch<'a>(
|
||||
&'a mut self,
|
||||
enabled: bool,
|
||||
threshold_db: f64,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
if !threshold_db.is_finite() {
|
||||
return Err("squelch threshold must be finite".into());
|
||||
}
|
||||
if !(-140.0..=0.0).contains(&threshold_db) {
|
||||
return Err("squelch threshold must be in range -140..=0 dBFS".into());
|
||||
}
|
||||
self.squelch_enabled = enabled;
|
||||
self.squelch_threshold_db = threshold_db as f32;
|
||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
||||
dsp_arc
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_squelch(enabled, self.squelch_threshold_db);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_signal_strength<'a>(
|
||||
&'a mut self,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
|
||||
@@ -667,6 +725,8 @@ impl RigCat for SoapySdrRig {
|
||||
.map(|max_gain| self.gain_db.min(max_gain))
|
||||
.unwrap_or(self.gain_db),
|
||||
),
|
||||
sdr_squelch_enabled: Some(self.squelch_enabled),
|
||||
sdr_squelch_threshold_db: Some(self.squelch_threshold_db as f64),
|
||||
wfm_deemphasis_us: self.wfm_deemphasis_us,
|
||||
wfm_stereo: self.wfm_stereo,
|
||||
wfm_stereo_detected,
|
||||
|
||||
Reference in New Issue
Block a user