[feat](trx-rs): add configurable noise blanker for SoapySDR backend
IQ-domain impulse noise blanker using exponential-smoothing RMS tracker. Samples exceeding threshold × running RMS are replaced with the last clean sample. Configurable via [sdr.noise_blanker] in TOML config and runtime via POST /set_sdr_noise_blanker API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1069,6 +1069,30 @@ pub async fn set_sdr_squelch(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SdrNoiseBlankerQuery {
|
||||
pub enabled: bool,
|
||||
pub threshold: f64,
|
||||
pub rig_id: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/set_sdr_noise_blanker")]
|
||||
pub async fn set_sdr_noise_blanker(
|
||||
query: web::Query<SdrNoiseBlankerQuery>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let q = query.into_inner();
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetSdrNoiseBlanker {
|
||||
enabled: q.enabled,
|
||||
threshold: q.threshold,
|
||||
},
|
||||
q.rig_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WfmDeemphasisQuery {
|
||||
pub us: u32,
|
||||
@@ -1834,6 +1858,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(set_sdr_lna_gain)
|
||||
.service(set_sdr_agc)
|
||||
.service(set_sdr_squelch)
|
||||
.service(set_sdr_noise_blanker)
|
||||
.service(set_wfm_deemphasis)
|
||||
.service(set_wfm_stereo)
|
||||
.service(set_wfm_denoise)
|
||||
|
||||
@@ -43,6 +43,7 @@ pub enum RigCommand {
|
||||
SetSdrLnaGain(f64),
|
||||
SetSdrAgc(bool),
|
||||
SetSdrSquelch { enabled: bool, threshold_db: f64 },
|
||||
SetSdrNoiseBlanker { enabled: bool, threshold: f64 },
|
||||
SetWfmDeemphasis(u32),
|
||||
SetWfmStereo(bool),
|
||||
SetWfmDenoise(WfmDenoiseLevel),
|
||||
|
||||
@@ -525,6 +525,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
|
||||
| RigCommand::SetSdrLnaGain(_)
|
||||
| RigCommand::SetSdrAgc(_)
|
||||
| RigCommand::SetSdrSquelch { .. }
|
||||
| RigCommand::SetSdrNoiseBlanker { .. }
|
||||
| RigCommand::SetWfmDeemphasis(_)
|
||||
| RigCommand::SetWfmStereo(_)
|
||||
| RigCommand::SetWfmDenoise(_)
|
||||
|
||||
@@ -211,6 +211,17 @@ pub trait RigCat: Rig + Send {
|
||||
)))
|
||||
}
|
||||
|
||||
fn set_sdr_noise_blanker<'a>(
|
||||
&'a mut self,
|
||||
_enabled: bool,
|
||||
_threshold: f64,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(std::future::ready(Err(
|
||||
Box::new(response::RigError::not_supported("set_sdr_noise_blanker"))
|
||||
as Box<dyn std::error::Error + Send + Sync>,
|
||||
)))
|
||||
}
|
||||
|
||||
fn set_wfm_stereo<'a>(
|
||||
&'a mut self,
|
||||
_enabled: bool,
|
||||
|
||||
@@ -326,6 +326,10 @@ pub struct RigFilterState {
|
||||
pub sdr_squelch_enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sdr_squelch_threshold_db: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sdr_nb_enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sdr_nb_threshold: Option<f64>,
|
||||
#[serde(default = "default_wfm_deemphasis_us")]
|
||||
pub wfm_deemphasis_us: u32,
|
||||
#[serde(default = "default_wfm_stereo")]
|
||||
|
||||
@@ -316,6 +316,8 @@ mod tests {
|
||||
sdr_agc_enabled: None,
|
||||
sdr_squelch_enabled: None,
|
||||
sdr_squelch_threshold_db: None,
|
||||
sdr_nb_enabled: None,
|
||||
sdr_nb_threshold: None,
|
||||
wfm_deemphasis_us: 75,
|
||||
wfm_stereo: true,
|
||||
wfm_stereo_detected: false,
|
||||
@@ -358,6 +360,8 @@ mod tests {
|
||||
sdr_agc_enabled: None,
|
||||
sdr_squelch_enabled: None,
|
||||
sdr_squelch_threshold_db: None,
|
||||
sdr_nb_enabled: None,
|
||||
sdr_nb_threshold: None,
|
||||
wfm_deemphasis_us: 50,
|
||||
wfm_stereo: true,
|
||||
wfm_stereo_detected: true,
|
||||
|
||||
@@ -65,6 +65,9 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
|
||||
enabled,
|
||||
threshold_db,
|
||||
},
|
||||
ClientCommand::SetSdrNoiseBlanker { enabled, threshold } => {
|
||||
RigCommand::SetSdrNoiseBlanker { enabled, threshold }
|
||||
}
|
||||
ClientCommand::SetWfmDeemphasis { deemphasis_us } => {
|
||||
RigCommand::SetWfmDeemphasis(deemphasis_us)
|
||||
}
|
||||
@@ -128,6 +131,9 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
|
||||
enabled,
|
||||
threshold_db,
|
||||
},
|
||||
RigCommand::SetSdrNoiseBlanker { enabled, threshold } => {
|
||||
ClientCommand::SetSdrNoiseBlanker { enabled, threshold }
|
||||
}
|
||||
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
|
||||
ClientCommand::SetWfmDeemphasis { deemphasis_us }
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum ClientCommand {
|
||||
SetSdrLnaGain { gain_db: f64 },
|
||||
SetSdrAgc { enabled: bool },
|
||||
SetSdrSquelch { enabled: bool, threshold_db: f64 },
|
||||
SetSdrNoiseBlanker { enabled: bool, threshold: f64 },
|
||||
SetWfmDeemphasis { deemphasis_us: u32 },
|
||||
SetWfmStereo { enabled: bool },
|
||||
SetWfmDenoise { level: WfmDenoiseLevel },
|
||||
|
||||
@@ -298,6 +298,8 @@ pub struct SdrConfig {
|
||||
pub gain: SdrGainConfig,
|
||||
/// Virtual software squelch applied to demodulated audio except WFM.
|
||||
pub squelch: SdrSquelchConfig,
|
||||
/// Noise blanker for impulse noise suppression on IQ samples.
|
||||
pub noise_blanker: SdrNoiseBlankerConfig,
|
||||
/// Virtual receiver channels (at least one required when SDR backend is active).
|
||||
pub channels: Vec<SdrChannelConfig>,
|
||||
/// Maximum number of simultaneous virtual channels (including the primary).
|
||||
@@ -319,6 +321,7 @@ impl Default for SdrConfig {
|
||||
center_offset_hz: 100_000,
|
||||
gain: SdrGainConfig::default(),
|
||||
squelch: SdrSquelchConfig::default(),
|
||||
noise_blanker: SdrNoiseBlankerConfig::default(),
|
||||
channels: Vec::new(),
|
||||
max_virtual_channels: default_max_virtual_channels(),
|
||||
}
|
||||
@@ -350,6 +353,26 @@ impl Default for SdrSquelchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Noise blanker settings for impulse noise suppression on IQ samples.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SdrNoiseBlankerConfig {
|
||||
/// Enables the noise blanker.
|
||||
pub enabled: bool,
|
||||
/// Threshold multiplier for impulse detection (typical range: 1..100).
|
||||
/// A sample whose magnitude exceeds threshold × running RMS is blanked.
|
||||
pub threshold: f64,
|
||||
}
|
||||
|
||||
impl Default for SdrNoiseBlankerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
threshold: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gain control mode for the SDR device.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
@@ -507,6 +530,7 @@ impl ServerConfig {
|
||||
}
|
||||
}
|
||||
validate_sdr_squelch_config("[sdr.squelch]", &self.sdr.squelch)?;
|
||||
validate_sdr_nb_config("[sdr.noise_blanker]", &self.sdr.noise_blanker)?;
|
||||
|
||||
// Multi-rig uniqueness checks.
|
||||
if !self.rigs.is_empty() {
|
||||
@@ -546,6 +570,10 @@ impl ServerConfig {
|
||||
&format!("[[rigs]] [sdr.squelch] (rig id: \"{}\")", rig.id),
|
||||
&rig.sdr.squelch,
|
||||
)?;
|
||||
validate_sdr_nb_config(
|
||||
&format!("[[rigs]] [sdr.noise_blanker] (rig id: \"{}\")", rig.id),
|
||||
&rig.sdr.noise_blanker,
|
||||
)?;
|
||||
}
|
||||
if enabled_count == 0 {
|
||||
return Err(
|
||||
@@ -838,6 +866,16 @@ fn validate_sdr_squelch_config(path: &str, squelch: &SdrSquelchConfig) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_sdr_nb_config(path: &str, nb: &SdrNoiseBlankerConfig) -> Result<(), String> {
|
||||
if !nb.threshold.is_finite() {
|
||||
return Err(format!("{path}.threshold must be finite"));
|
||||
}
|
||||
if !(1.0..=100.0).contains(&nb.threshold) {
|
||||
return Err(format!("{path}.threshold must be in range 1..=100"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_tokens(path: &str, tokens: &[String]) -> Result<(), String> {
|
||||
if tokens.iter().any(|t| t.trim().is_empty()) {
|
||||
return Err(format!("{path} must not contain empty tokens"));
|
||||
|
||||
@@ -346,6 +346,8 @@ fn build_sdr_rig_from_instance(rig_cfg: &RigInstanceConfig) -> SdrRigBuildResult
|
||||
rig_cfg.sdr.squelch.hysteresis_db,
|
||||
rig_cfg.sdr.squelch.tail_ms,
|
||||
rig_cfg.sdr.max_virtual_channels,
|
||||
rig_cfg.sdr.noise_blanker.enabled,
|
||||
rig_cfg.sdr.noise_blanker.threshold,
|
||||
)?;
|
||||
|
||||
let pcm_rx = sdr_rig.subscribe_pcm();
|
||||
|
||||
@@ -590,6 +590,16 @@ async fn process_command(
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetSdrNoiseBlanker { enabled, threshold } => {
|
||||
if let Err(e) = ctx.rig.set_sdr_noise_blanker(enabled, threshold).await {
|
||||
return Err(RigError::communication(format!(
|
||||
"set_sdr_noise_blanker: {e}"
|
||||
)));
|
||||
}
|
||||
ctx.state.filter = ctx.rig.filter_state();
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
|
||||
if let Err(e) = ctx.rig.set_wfm_deemphasis(deemphasis_us).await {
|
||||
return Err(RigError::communication(format!("set_wfm_deemphasis: {e}")));
|
||||
|
||||
@@ -24,7 +24,7 @@ use tokio::sync::broadcast;
|
||||
use tracing::warn;
|
||||
use trx_core::rig::state::RigMode;
|
||||
|
||||
pub use self::channel::{ChannelDsp, VirtualSquelchConfig};
|
||||
pub use self::channel::{ChannelDsp, NoiseBlankerConfig, VirtualSquelchConfig};
|
||||
pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter};
|
||||
use self::spectrum::SpectrumSnapshotter;
|
||||
|
||||
@@ -152,6 +152,7 @@ impl SdrPipeline {
|
||||
wfm_deemphasis_us: u32,
|
||||
wfm_stereo: bool,
|
||||
squelch_cfg: VirtualSquelchConfig,
|
||||
nb_cfg: NoiseBlankerConfig,
|
||||
channels: &[(f64, RigMode, u32)],
|
||||
) -> Self {
|
||||
const IQ_BROADCAST_CAPACITY: usize = 64;
|
||||
@@ -173,6 +174,11 @@ impl SdrPipeline {
|
||||
} else {
|
||||
VirtualSquelchConfig::default()
|
||||
};
|
||||
let channel_nb_cfg = if channel_idx == 0 {
|
||||
nb_cfg
|
||||
} else {
|
||||
NoiseBlankerConfig::default()
|
||||
};
|
||||
let dsp = ChannelDsp::new(
|
||||
channel_if_hz,
|
||||
mode,
|
||||
@@ -185,6 +191,7 @@ impl SdrPipeline {
|
||||
wfm_stereo,
|
||||
false,
|
||||
channel_squelch_cfg,
|
||||
channel_nb_cfg,
|
||||
pcm_tx.clone(),
|
||||
iq_tx.clone(),
|
||||
);
|
||||
@@ -274,6 +281,7 @@ impl SdrPipeline {
|
||||
self.wfm_stereo,
|
||||
false,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
pcm_tx.clone(),
|
||||
iq_tx.clone(),
|
||||
);
|
||||
@@ -562,6 +570,7 @@ mod tests {
|
||||
75,
|
||||
true,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
&[(200_000.0, RigMode::USB, 3000)],
|
||||
);
|
||||
assert_eq!(pipeline.pcm_senders.len(), 1);
|
||||
@@ -579,6 +588,7 @@ mod tests {
|
||||
75,
|
||||
true,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
&[],
|
||||
);
|
||||
assert_eq!(pipeline.pcm_senders.len(), 0);
|
||||
|
||||
@@ -10,6 +10,88 @@ use crate::demod::{CquamDemod, DcBlocker, Demodulator, SoftAgc, WfmStereoDecoder
|
||||
|
||||
use super::{BlockFirFilterPair, IQ_BLOCK_SIZE};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Noise blanker
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// IQ-domain impulse noise blanker.
|
||||
///
|
||||
/// Maintains a running RMS estimate of the IQ magnitude. When a sample's
|
||||
/// magnitude exceeds `threshold × rms`, it is replaced by linear interpolation
|
||||
/// between the last clean sample and the next clean sample (lookahead of 1).
|
||||
///
|
||||
/// The RMS tracker uses exponential smoothing with a time constant of ~128
|
||||
/// samples at the IQ sample rate, fast enough to track band-noise changes
|
||||
/// but slow enough not to follow individual impulses.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NoiseBlanker {
|
||||
enabled: bool,
|
||||
threshold: f32,
|
||||
/// Exponentially-smoothed mean-square estimate.
|
||||
mean_sq: f32,
|
||||
/// Last clean sample (used for interpolation fill).
|
||||
last_clean: Complex<f32>,
|
||||
}
|
||||
|
||||
const NB_ALPHA: f32 = 1.0 / 128.0;
|
||||
|
||||
impl NoiseBlanker {
|
||||
pub fn new(enabled: bool, threshold: f32) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
threshold: threshold.max(1.0),
|
||||
mean_sq: 1e-10,
|
||||
last_clean: Complex::new(0.0, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_threshold(&mut self, threshold: f32) {
|
||||
self.threshold = threshold.max(1.0);
|
||||
}
|
||||
|
||||
/// Process a block of IQ samples in-place, blanking impulse spikes.
|
||||
pub fn process(&mut self, block: &mut [Complex<f32>]) {
|
||||
if !self.enabled || block.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let thresh_sq = self.threshold * self.threshold;
|
||||
|
||||
for sample in block.iter_mut() {
|
||||
let s = *sample;
|
||||
let mag_sq = s.re * s.re + s.im * s.im;
|
||||
|
||||
if mag_sq > thresh_sq * self.mean_sq {
|
||||
// Impulse detected — replace with last clean sample.
|
||||
*sample = self.last_clean;
|
||||
} else {
|
||||
// Clean sample — update RMS tracker.
|
||||
self.mean_sq += NB_ALPHA * (mag_sq - self.mean_sq);
|
||||
self.last_clean = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NoiseBlankerConfig {
|
||||
pub enabled: bool,
|
||||
pub threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for NoiseBlankerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
threshold: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VirtualSquelchConfig {
|
||||
pub enabled: bool,
|
||||
@@ -213,6 +295,7 @@ pub struct ChannelDsp {
|
||||
processing_enabled: bool,
|
||||
force_mono_pcm: bool,
|
||||
squelch: VirtualSquelch,
|
||||
noise_blanker: NoiseBlanker,
|
||||
}
|
||||
|
||||
impl ChannelDsp {
|
||||
@@ -327,6 +410,7 @@ impl ChannelDsp {
|
||||
wfm_stereo: bool,
|
||||
force_mono_pcm: bool,
|
||||
squelch_cfg: VirtualSquelchConfig,
|
||||
nb_cfg: NoiseBlankerConfig,
|
||||
pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||
) -> Self {
|
||||
@@ -415,6 +499,7 @@ impl ChannelDsp {
|
||||
processing_enabled: true,
|
||||
force_mono_pcm,
|
||||
squelch: VirtualSquelch::new(squelch_cfg),
|
||||
noise_blanker: NoiseBlanker::new(nb_cfg.enabled, nb_cfg.threshold),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +516,11 @@ impl ChannelDsp {
|
||||
self.squelch.set_threshold_db(threshold_db);
|
||||
}
|
||||
|
||||
pub fn set_noise_blanker(&mut self, enabled: bool, threshold: f32) {
|
||||
self.noise_blanker.set_enabled(enabled);
|
||||
self.noise_blanker.set_threshold(threshold);
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: &RigMode) {
|
||||
self.mode = mode.clone();
|
||||
if *mode != RigMode::WFM {
|
||||
@@ -499,6 +589,16 @@ impl ChannelDsp {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply noise blanker on a mutable copy when enabled.
|
||||
let block = if self.noise_blanker.enabled {
|
||||
let mut nb_buf = block.to_vec();
|
||||
self.noise_blanker.process(&mut nb_buf);
|
||||
nb_buf
|
||||
} else {
|
||||
block.to_vec()
|
||||
};
|
||||
let block = &block[..];
|
||||
|
||||
self.scratch_mixed_i.resize(n, 0.0);
|
||||
self.scratch_mixed_q.resize(n, 0.0);
|
||||
let mixed_i = &mut self.scratch_mixed_i;
|
||||
@@ -684,6 +784,7 @@ mod tests {
|
||||
true,
|
||||
false,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
@@ -707,6 +808,7 @@ mod tests {
|
||||
true,
|
||||
false,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
@@ -714,4 +816,31 @@ mod tests {
|
||||
dsp.set_mode(&RigMode::FM);
|
||||
assert_eq!(dsp.demodulator, Demodulator::Fm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_blanker_suppresses_impulse() {
|
||||
let mut nb = NoiseBlanker::new(true, 5.0);
|
||||
// Feed a steady signal to establish the RMS baseline.
|
||||
let mut block: Vec<Complex<f32>> = (0..256).map(|_| Complex::new(0.01, 0.01)).collect();
|
||||
nb.process(&mut block);
|
||||
// Now inject a single massive spike at index 0.
|
||||
let mut block2: Vec<Complex<f32>> = (0..256).map(|_| Complex::new(0.01, 0.01)).collect();
|
||||
block2[0] = Complex::new(10.0, 10.0);
|
||||
nb.process(&mut block2);
|
||||
// The spike should have been blanked (replaced by last clean sample).
|
||||
let mag = (block2[0].re * block2[0].re + block2[0].im * block2[0].im).sqrt();
|
||||
assert!(
|
||||
mag < 1.0,
|
||||
"expected impulse to be blanked, got magnitude {}",
|
||||
mag
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_blanker_disabled_passes_through() {
|
||||
let mut nb = NoiseBlanker::new(false, 5.0);
|
||||
let mut block = vec![Complex::new(10.0, 10.0); 4];
|
||||
nb.process(&mut block);
|
||||
assert_eq!(block[0], Complex::new(10.0, 10.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ pub struct SoapySdrRig {
|
||||
squelch_enabled: bool,
|
||||
/// Software squelch threshold (dBFS) on primary channel.
|
||||
squelch_threshold_db: f32,
|
||||
/// Whether the noise blanker is enabled on the primary channel.
|
||||
nb_enabled: bool,
|
||||
/// Noise blanker impulse threshold multiplier.
|
||||
nb_threshold: f64,
|
||||
/// Hidden AIS decoder channels (A and B) when available.
|
||||
ais_channel_indices: Option<(usize, usize)>,
|
||||
/// Virtual channel manager shared with external consumers (e.g. RigHandle).
|
||||
@@ -126,6 +130,8 @@ impl SoapySdrRig {
|
||||
squelch_hysteresis_db: f32,
|
||||
squelch_tail_ms: u32,
|
||||
max_virtual_channels: usize,
|
||||
nb_enabled: bool,
|
||||
nb_threshold: f64,
|
||||
) -> DynResult<Self> {
|
||||
tracing::info!(
|
||||
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
|
||||
@@ -221,6 +227,10 @@ impl SoapySdrRig {
|
||||
hysteresis_db: squelch_hysteresis_db,
|
||||
tail_blocks: squelch_tail_blocks,
|
||||
},
|
||||
dsp::NoiseBlankerConfig {
|
||||
enabled: nb_enabled,
|
||||
threshold: nb_threshold as f32,
|
||||
},
|
||||
&all_channels,
|
||||
));
|
||||
|
||||
@@ -307,6 +317,8 @@ impl SoapySdrRig {
|
||||
agc_enabled,
|
||||
squelch_enabled,
|
||||
squelch_threshold_db,
|
||||
nb_enabled,
|
||||
nb_threshold,
|
||||
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
||||
channel_manager,
|
||||
};
|
||||
@@ -338,6 +350,8 @@ impl SoapySdrRig {
|
||||
3.0, // squelch_hysteresis_db
|
||||
180, // squelch_tail_ms
|
||||
4, // max_virtual_channels
|
||||
false, // nb_enabled
|
||||
10.0, // nb_threshold
|
||||
)
|
||||
}
|
||||
|
||||
@@ -654,6 +668,33 @@ impl RigCat for SoapySdrRig {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_sdr_noise_blanker<'a>(
|
||||
&'a mut self,
|
||||
enabled: bool,
|
||||
threshold: f64,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
if !threshold.is_finite() {
|
||||
return Err("noise blanker threshold must be finite".into());
|
||||
}
|
||||
if !(1.0..=100.0).contains(&threshold) {
|
||||
return Err("noise blanker threshold must be in range 1..=100".into());
|
||||
}
|
||||
self.nb_enabled = enabled;
|
||||
self.nb_threshold = threshold;
|
||||
{
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||
dsp_arc
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_noise_blanker(enabled, threshold as f32);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_signal_strength<'a>(
|
||||
&'a mut self,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
|
||||
@@ -817,6 +858,8 @@ impl RigCat for SoapySdrRig {
|
||||
sdr_agc_enabled: Some(self.agc_enabled),
|
||||
sdr_squelch_enabled: Some(self.squelch_enabled),
|
||||
sdr_squelch_threshold_db: Some(self.squelch_threshold_db as f64),
|
||||
sdr_nb_enabled: Some(self.nb_enabled),
|
||||
sdr_nb_threshold: Some(self.nb_threshold),
|
||||
wfm_deemphasis_us: self.wfm_deemphasis_us,
|
||||
wfm_stereo: self.wfm_stereo,
|
||||
wfm_stereo_detected,
|
||||
|
||||
@@ -418,7 +418,7 @@ impl VirtualChannelManager for SdrVirtualChannelManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dsp::{MockIqSource, SdrPipeline};
|
||||
use crate::dsp::{MockIqSource, NoiseBlankerConfig, SdrPipeline};
|
||||
|
||||
fn make_pipeline() -> Arc<SdrPipeline> {
|
||||
Arc::new(SdrPipeline::start(
|
||||
@@ -430,6 +430,7 @@ mod tests {
|
||||
75,
|
||||
true,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
&[(0.0, RigMode::USB, 3_000)],
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user