[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::Low => 1.0 - (1.0 - broadband_gain) * 0.35,
|
||||||
WfmDenoiseLevel::Medium => 1.0 - (1.0 - broadband_gain) * 0.65,
|
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)
|
diff_i * effective_gain.clamp(0.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use num_complex::Complex;
|
|||||||
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, VirtualSquelchConfig};
|
||||||
pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter};
|
pub use self::filter::{BlockFirFilter, BlockFirFilterPair, FirFilter};
|
||||||
use self::spectrum::SpectrumSnapshotter;
|
use self::spectrum::SpectrumSnapshotter;
|
||||||
|
|
||||||
@@ -107,6 +107,7 @@ impl SdrPipeline {
|
|||||||
frame_duration_ms: u16,
|
frame_duration_ms: u16,
|
||||||
wfm_deemphasis_us: u32,
|
wfm_deemphasis_us: u32,
|
||||||
wfm_stereo: bool,
|
wfm_stereo: bool,
|
||||||
|
squelch_cfg: VirtualSquelchConfig,
|
||||||
channels: &[(f64, RigMode, u32, usize)],
|
channels: &[(f64, RigMode, u32, usize)],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
const IQ_BROADCAST_CAPACITY: usize = 64;
|
const IQ_BROADCAST_CAPACITY: usize = 64;
|
||||||
@@ -118,9 +119,16 @@ impl SdrPipeline {
|
|||||||
let mut iq_senders = Vec::with_capacity(channels.len());
|
let mut iq_senders = Vec::with_capacity(channels.len());
|
||||||
let mut channel_dsps: Vec<Arc<Mutex<ChannelDsp>>> = 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 (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 (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(
|
let dsp = ChannelDsp::new(
|
||||||
channel_if_hz,
|
channel_if_hz,
|
||||||
mode,
|
mode,
|
||||||
@@ -132,6 +140,7 @@ impl SdrPipeline {
|
|||||||
wfm_deemphasis_us,
|
wfm_deemphasis_us,
|
||||||
wfm_stereo,
|
wfm_stereo,
|
||||||
fir_taps,
|
fir_taps,
|
||||||
|
channel_squelch_cfg,
|
||||||
pcm_tx.clone(),
|
pcm_tx.clone(),
|
||||||
iq_tx.clone(),
|
iq_tx.clone(),
|
||||||
);
|
);
|
||||||
@@ -405,6 +414,7 @@ mod tests {
|
|||||||
20,
|
20,
|
||||||
75,
|
75,
|
||||||
true,
|
true,
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
&[(200_000.0, RigMode::USB, 3000, 64)],
|
&[(200_000.0, RigMode::USB, 3000, 64)],
|
||||||
);
|
);
|
||||||
assert_eq!(pipeline.pcm_senders.len(), 1);
|
assert_eq!(pipeline.pcm_senders.len(), 1);
|
||||||
@@ -421,6 +431,7 @@ mod tests {
|
|||||||
20,
|
20,
|
||||||
75,
|
75,
|
||||||
true,
|
true,
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
assert_eq!(pipeline.pcm_senders.len(), 0);
|
assert_eq!(pipeline.pcm_senders.len(), 0);
|
||||||
|
|||||||
@@ -10,6 +10,88 @@ use crate::demod::{DcBlocker, Demodulator, SoftAgc, WfmStereoDecoder};
|
|||||||
|
|
||||||
use super::{BlockFirFilterPair, IQ_BLOCK_SIZE};
|
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 {
|
fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc {
|
||||||
let sr = audio_sample_rate.max(1) as f32;
|
let sr = audio_sample_rate.max(1) as f32;
|
||||||
match mode {
|
match mode {
|
||||||
@@ -87,6 +169,7 @@ pub struct ChannelDsp {
|
|||||||
audio_agc: SoftAgc,
|
audio_agc: SoftAgc,
|
||||||
audio_dc: Option<DcBlocker>,
|
audio_dc: Option<DcBlocker>,
|
||||||
processing_enabled: bool,
|
processing_enabled: bool,
|
||||||
|
squelch: VirtualSquelch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelDsp {
|
impl ChannelDsp {
|
||||||
@@ -186,6 +269,7 @@ impl ChannelDsp {
|
|||||||
wfm_deemphasis_us: u32,
|
wfm_deemphasis_us: u32,
|
||||||
wfm_stereo: bool,
|
wfm_stereo: bool,
|
||||||
fir_taps: usize,
|
fir_taps: usize,
|
||||||
|
squelch_cfg: VirtualSquelchConfig,
|
||||||
pcm_tx: broadcast::Sender<Vec<f32>>,
|
pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||||
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -264,6 +348,7 @@ impl ChannelDsp {
|
|||||||
audio_agc: agc_for_mode(mode, audio_sample_rate),
|
audio_agc: agc_for_mode(mode, audio_sample_rate),
|
||||||
audio_dc: dc_for_mode(mode),
|
audio_dc: dc_for_mode(mode),
|
||||||
processing_enabled: true,
|
processing_enabled: true,
|
||||||
|
squelch: VirtualSquelch::new(squelch_cfg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,12 +356,18 @@ impl ChannelDsp {
|
|||||||
self.processing_enabled = enabled;
|
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) {
|
pub fn set_mode(&mut self, mode: &RigMode) {
|
||||||
self.mode = mode.clone();
|
self.mode = mode.clone();
|
||||||
if *mode != RigMode::WFM {
|
if *mode != RigMode::WFM {
|
||||||
self.audio_bandwidth_hz = default_bandwidth_for_mode(mode);
|
self.audio_bandwidth_hz = default_bandwidth_for_mode(mode);
|
||||||
}
|
}
|
||||||
self.demodulator = Demodulator::for_mode(mode);
|
self.demodulator = Demodulator::for_mode(mode);
|
||||||
|
self.squelch.reset();
|
||||||
self.rebuild_filters(true);
|
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() {
|
if self.wfm_decoder.is_some() {
|
||||||
for sample in decimated.iter_mut() {
|
for sample in decimated.iter_mut() {
|
||||||
let mag = (sample.re * sample.re + sample.im * sample.im).sqrt();
|
let mag = (sample.re * sample.re + sample.im * sample.im).sqrt();
|
||||||
@@ -437,7 +534,7 @@ impl ChannelDsp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WFM_OUTPUT_GAIN: f32 = 0.50;
|
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);
|
let mut out = decoder.process_iq(decimated);
|
||||||
for sample in &mut out {
|
for sample in &mut out {
|
||||||
*sample = (*sample * WFM_OUTPUT_GAIN).clamp(-1.0, 1.0);
|
*sample = (*sample * WFM_OUTPUT_GAIN).clamp(-1.0, 1.0);
|
||||||
@@ -462,6 +559,9 @@ impl ChannelDsp {
|
|||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if !self.squelch.update(&self.mode, signal_db) {
|
||||||
|
audio.fill(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
self.frame_buf.extend_from_slice(&audio);
|
self.frame_buf.extend_from_slice(&audio);
|
||||||
while self.frame_buf.len().saturating_sub(self.frame_buf_offset) >= self.frame_size {
|
while self.frame_buf.len().saturating_sub(self.frame_buf_offset) >= self.frame_size {
|
||||||
@@ -499,6 +599,7 @@ mod tests {
|
|||||||
75,
|
75,
|
||||||
true,
|
true,
|
||||||
31,
|
31,
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
pcm_tx,
|
pcm_tx,
|
||||||
iq_tx,
|
iq_tx,
|
||||||
);
|
);
|
||||||
@@ -521,6 +622,7 @@ mod tests {
|
|||||||
75,
|
75,
|
||||||
true,
|
true,
|
||||||
31,
|
31,
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
pcm_tx,
|
pcm_tx,
|
||||||
iq_tx,
|
iq_tx,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ pub struct SoapySdrRig {
|
|||||||
gain_db: f64,
|
gain_db: f64,
|
||||||
/// Optional hard ceiling for the applied hardware gain in dB.
|
/// Optional hard ceiling for the applied hardware gain in dB.
|
||||||
max_gain_db: Option<f64>,
|
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.
|
/// Hidden AIS decoder channels (A and B) when available.
|
||||||
ais_channel_indices: Option<(usize, usize)>,
|
ais_channel_indices: Option<(usize, usize)>,
|
||||||
}
|
}
|
||||||
@@ -90,6 +94,10 @@ impl SoapySdrRig {
|
|||||||
/// - `center_offset_hz`: the hardware is tuned this many Hz *below* the
|
/// - `center_offset_hz`: the hardware is tuned this many Hz *below* the
|
||||||
/// dial frequency so the desired signal lands off-DC. The DSP mixer
|
/// dial frequency so the desired signal lands off-DC. The DSP mixer
|
||||||
/// shifts it back. Pass 0 to tune exactly to the dial frequency.
|
/// 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new_with_config(
|
pub fn new_with_config(
|
||||||
args: &str,
|
args: &str,
|
||||||
@@ -106,6 +114,10 @@ impl SoapySdrRig {
|
|||||||
sdr_sample_rate: u32,
|
sdr_sample_rate: u32,
|
||||||
bandwidth_hz: u32,
|
bandwidth_hz: u32,
|
||||||
center_offset_hz: i64,
|
center_offset_hz: i64,
|
||||||
|
squelch_enabled: bool,
|
||||||
|
squelch_threshold_db: f32,
|
||||||
|
squelch_hysteresis_db: f32,
|
||||||
|
squelch_tail_ms: u32,
|
||||||
) -> DynResult<Self> {
|
) -> DynResult<Self> {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
|
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
|
||||||
@@ -161,6 +173,16 @@ impl SoapySdrRig {
|
|||||||
25_000,
|
25_000,
|
||||||
96,
|
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(
|
let pipeline = dsp::SdrPipeline::start(
|
||||||
iq_source,
|
iq_source,
|
||||||
@@ -170,6 +192,12 @@ impl SoapySdrRig {
|
|||||||
frame_duration_ms,
|
frame_duration_ms,
|
||||||
wfm_deemphasis_us,
|
wfm_deemphasis_us,
|
||||||
true, // wfm_stereo: enabled by default
|
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,
|
&all_channels,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -244,6 +272,8 @@ impl SoapySdrRig {
|
|||||||
wfm_denoise: WfmDenoiseLevel::Auto,
|
wfm_denoise: WfmDenoiseLevel::Auto,
|
||||||
gain_db,
|
gain_db,
|
||||||
max_gain_db,
|
max_gain_db,
|
||||||
|
squelch_enabled,
|
||||||
|
squelch_threshold_db,
|
||||||
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
||||||
};
|
};
|
||||||
rig.apply_ais_channel_activity();
|
rig.apply_ais_channel_activity();
|
||||||
@@ -269,6 +299,10 @@ impl SoapySdrRig {
|
|||||||
1_920_000,
|
1_920_000,
|
||||||
1_500_000, // bandwidth_hz
|
1_500_000, // bandwidth_hz
|
||||||
0, // center_offset_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>(
|
fn get_signal_strength<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
|
) -> 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))
|
.map(|max_gain| self.gain_db.min(max_gain))
|
||||||
.unwrap_or(self.gain_db),
|
.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_deemphasis_us: self.wfm_deemphasis_us,
|
||||||
wfm_stereo: self.wfm_stereo,
|
wfm_stereo: self.wfm_stereo,
|
||||||
wfm_stereo_detected,
|
wfm_stereo_detected,
|
||||||
|
|||||||
Reference in New Issue
Block a user