[feat](trx-backend-soapysdr): dynamic virtual channel DSP slots
Replace the fixed channel_dsps Vec (thread-owned) with an Arc<RwLock<Vec<...>>> shared between the IQ read thread and the async side, enabling live add/remove of virtual DSP channels. Cache audio construction params in SdrPipeline so add_virtual_channel() can build ChannelDsp instances without being re-passed them. Add: - SdrPipeline::add_virtual_channel() / remove_virtual_channel() - SoapySdrRig::virtual_channel_add/remove/set_freq/set_mode() - SoapySdrRig::center_hz() / half_span_hz() accessors The IQ read loop holds a brief read lock (~2 ms per block) while processing all channels; write lock for add/remove waits at most one block. All 27 existing tests continue to pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -15,7 +15,7 @@ mod channel;
|
|||||||
mod filter;
|
mod filter;
|
||||||
mod spectrum;
|
mod spectrum;
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use num_complex::Complex;
|
use num_complex::Complex;
|
||||||
@@ -84,7 +84,10 @@ impl IqSource for MockIqSource {
|
|||||||
pub struct SdrPipeline {
|
pub struct SdrPipeline {
|
||||||
pub pcm_senders: Vec<broadcast::Sender<Vec<f32>>>,
|
pub pcm_senders: Vec<broadcast::Sender<Vec<f32>>>,
|
||||||
pub iq_senders: Vec<broadcast::Sender<Vec<Complex<f32>>>>,
|
pub iq_senders: Vec<broadcast::Sender<Vec<Complex<f32>>>>,
|
||||||
pub channel_dsps: Vec<Arc<Mutex<ChannelDsp>>>,
|
/// All DSP channel slots, including fixed (primary, AIS) and dynamic
|
||||||
|
/// (user virtual) channels. Shared with the IQ read thread via RwLock.
|
||||||
|
/// Virtual channels are appended beyond the fixed slots.
|
||||||
|
pub channel_dsps: Arc<RwLock<Vec<Arc<Mutex<ChannelDsp>>>>>,
|
||||||
/// Latest FFT magnitude bins (dBFS, FFT-shifted), updated ~10 Hz.
|
/// Latest FFT magnitude bins (dBFS, FFT-shifted), updated ~10 Hz.
|
||||||
pub spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
|
pub spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
|
||||||
/// SDR capture sample rate, needed by `SoapySdrRig::get_spectrum`.
|
/// SDR capture sample rate, needed by `SoapySdrRig::get_spectrum`.
|
||||||
@@ -95,6 +98,13 @@ pub struct SdrPipeline {
|
|||||||
/// Write `Some(gain_db)` here to adjust the hardware RX gain.
|
/// Write `Some(gain_db)` here to adjust the hardware RX gain.
|
||||||
/// The IQ read loop picks it up on the next iteration.
|
/// The IQ read loop picks it up on the next iteration.
|
||||||
pub gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
|
pub gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
|
||||||
|
// Parameters cached here so `add_virtual_channel` can construct new
|
||||||
|
// `ChannelDsp` instances without needing to be passed them again.
|
||||||
|
audio_sample_rate: u32,
|
||||||
|
audio_channels: usize,
|
||||||
|
frame_duration_ms: u16,
|
||||||
|
wfm_deemphasis_us: u32,
|
||||||
|
wfm_stereo: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SdrPipeline {
|
impl SdrPipeline {
|
||||||
@@ -117,7 +127,7 @@ impl SdrPipeline {
|
|||||||
|
|
||||||
let mut pcm_senders = Vec::with_capacity(channels.len());
|
let mut pcm_senders = Vec::with_capacity(channels.len());
|
||||||
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: Vec<Arc<Mutex<ChannelDsp>>> = Vec::with_capacity(channels.len());
|
||||||
|
|
||||||
for (channel_idx, &(channel_if_hz, ref mode, audio_bandwidth_hz, fir_taps)) in
|
for (channel_idx, &(channel_if_hz, ref mode, audio_bandwidth_hz, fir_taps)) in
|
||||||
channels.iter().enumerate()
|
channels.iter().enumerate()
|
||||||
@@ -146,10 +156,12 @@ impl SdrPipeline {
|
|||||||
);
|
);
|
||||||
pcm_senders.push(pcm_tx);
|
pcm_senders.push(pcm_tx);
|
||||||
iq_senders.push(iq_tx);
|
iq_senders.push(iq_tx);
|
||||||
channel_dsps.push(Arc::new(Mutex::new(dsp)));
|
channel_dsps_vec.push(Arc::new(Mutex::new(dsp)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let thread_dsps: Vec<Arc<Mutex<ChannelDsp>>> = channel_dsps.clone();
|
let channel_dsps: Arc<RwLock<Vec<Arc<Mutex<ChannelDsp>>>>> =
|
||||||
|
Arc::new(RwLock::new(channel_dsps_vec));
|
||||||
|
let thread_dsps = channel_dsps.clone();
|
||||||
let spectrum_buf: Arc<Mutex<Option<Vec<f32>>>> = Arc::new(Mutex::new(None));
|
let spectrum_buf: Arc<Mutex<Option<Vec<f32>>>> = Arc::new(Mutex::new(None));
|
||||||
let thread_spectrum_buf = spectrum_buf.clone();
|
let thread_spectrum_buf = spectrum_buf.clone();
|
||||||
let retune_cmd: Arc<std::sync::Mutex<Option<f64>>> = Arc::new(std::sync::Mutex::new(None));
|
let retune_cmd: Arc<std::sync::Mutex<Option<f64>>> = Arc::new(std::sync::Mutex::new(None));
|
||||||
@@ -180,8 +192,67 @@ impl SdrPipeline {
|
|||||||
sdr_sample_rate,
|
sdr_sample_rate,
|
||||||
retune_cmd,
|
retune_cmd,
|
||||||
gain_cmd,
|
gain_cmd,
|
||||||
|
audio_sample_rate,
|
||||||
|
audio_channels: output_channels,
|
||||||
|
frame_duration_ms,
|
||||||
|
wfm_deemphasis_us,
|
||||||
|
wfm_stereo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocate a new virtual DSP channel.
|
||||||
|
///
|
||||||
|
/// Returns the PCM and IQ broadcast senders so the caller can subscribe to
|
||||||
|
/// audio frames and raw decimated IQ respectively. The channel is appended
|
||||||
|
/// beyond all fixed slots and is immediately visible to the IQ read thread.
|
||||||
|
pub fn add_virtual_channel(
|
||||||
|
&self,
|
||||||
|
channel_if_hz: f64,
|
||||||
|
mode: &RigMode,
|
||||||
|
bandwidth_hz: u32,
|
||||||
|
fir_taps: usize,
|
||||||
|
) -> (broadcast::Sender<Vec<f32>>, broadcast::Sender<Vec<Complex<f32>>>) {
|
||||||
|
const PCM_BROADCAST_CAPACITY: usize = 32;
|
||||||
|
const IQ_BROADCAST_CAPACITY: usize = 64;
|
||||||
|
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(PCM_BROADCAST_CAPACITY);
|
||||||
|
let (iq_tx, _) = broadcast::channel::<Vec<Complex<f32>>>(IQ_BROADCAST_CAPACITY);
|
||||||
|
let dsp = ChannelDsp::new(
|
||||||
|
channel_if_hz,
|
||||||
|
mode,
|
||||||
|
self.sdr_sample_rate,
|
||||||
|
self.audio_sample_rate,
|
||||||
|
self.audio_channels,
|
||||||
|
self.frame_duration_ms,
|
||||||
|
bandwidth_hz,
|
||||||
|
self.wfm_deemphasis_us,
|
||||||
|
self.wfm_stereo,
|
||||||
|
fir_taps.max(1),
|
||||||
|
VirtualSquelchConfig::default(),
|
||||||
|
pcm_tx.clone(),
|
||||||
|
iq_tx.clone(),
|
||||||
|
);
|
||||||
|
self.channel_dsps
|
||||||
|
.write()
|
||||||
|
.expect("channel_dsps RwLock poisoned")
|
||||||
|
.push(Arc::new(Mutex::new(dsp)));
|
||||||
|
(pcm_tx, iq_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a DSP channel slot by its index in the full `channel_dsps` list.
|
||||||
|
///
|
||||||
|
/// Returns `false` when the index is out of range. Callers are responsible
|
||||||
|
/// for not removing fixed slots (primary, AIS).
|
||||||
|
pub fn remove_virtual_channel(&self, idx: usize) -> bool {
|
||||||
|
let mut dsps = self
|
||||||
|
.channel_dsps
|
||||||
|
.write()
|
||||||
|
.expect("channel_dsps RwLock poisoned");
|
||||||
|
if idx >= dsps.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dsps.remove(idx);
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -193,7 +264,7 @@ pub const IQ_BLOCK_SIZE: usize = 4096;
|
|||||||
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,
|
||||||
channel_dsps: Vec<Arc<Mutex<ChannelDsp>>>,
|
channel_dsps: Arc<RwLock<Vec<Arc<Mutex<ChannelDsp>>>>>,
|
||||||
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||||
spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
|
spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
|
||||||
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
|
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
|
||||||
@@ -326,11 +397,18 @@ fn iq_read_loop(
|
|||||||
let _ = iq_tx.send(samples.to_vec());
|
let _ = iq_tx.send(samples.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
for dsp_arc in &channel_dsps {
|
// Hold a read lock only for the duration of this block's DSP pass.
|
||||||
match dsp_arc.lock() {
|
// Write lock (add/remove channel) waits at most one block (~2 ms).
|
||||||
Ok(mut dsp) => dsp.process_block(samples),
|
{
|
||||||
Err(e) => {
|
let dsps = channel_dsps
|
||||||
tracing::error!("channel DSP mutex poisoned: {}", e);
|
.read()
|
||||||
|
.expect("channel_dsps RwLock poisoned");
|
||||||
|
for dsp_arc in dsps.iter() {
|
||||||
|
match dsp_arc.lock() {
|
||||||
|
Ok(mut dsp) => dsp.process_block(samples),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("channel DSP mutex poisoned: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +503,7 @@ mod tests {
|
|||||||
&[(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);
|
||||||
assert_eq!(pipeline.channel_dsps.len(), 1);
|
assert_eq!(pipeline.channel_dsps.read().unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -442,6 +520,6 @@ mod tests {
|
|||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
assert_eq!(pipeline.pcm_senders.len(), 0);
|
assert_eq!(pipeline.pcm_senders.len(), 0);
|
||||||
assert_eq!(pipeline.channel_dsps.len(), 0);
|
assert_eq!(pipeline.channel_dsps.read().unwrap().len(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,12 +310,12 @@ impl SoapySdrRig {
|
|||||||
let Some((ais_a_idx, ais_b_idx)) = self.ais_channel_indices else {
|
let Some((ais_a_idx, ais_b_idx)) = self.ais_channel_indices else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(ais_a_idx) {
|
if let Some(dsp_arc) = dsps.get(ais_a_idx) {
|
||||||
let if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
let if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
||||||
dsp_arc.lock().unwrap().set_channel_if_hz(if_hz);
|
dsp_arc.lock().unwrap().set_channel_if_hz(if_hz);
|
||||||
}
|
}
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(ais_b_idx) {
|
if let Some(dsp_arc) = dsps.get(ais_b_idx) {
|
||||||
let if_hz = (self.freq.hz as i64 + AIS_CHANNEL_SPACING_HZ - self.center_hz) as f64;
|
let if_hz = (self.freq.hz as i64 + AIS_CHANNEL_SPACING_HZ - self.center_hz) as f64;
|
||||||
dsp_arc.lock().unwrap().set_channel_if_hz(if_hz);
|
dsp_arc.lock().unwrap().set_channel_if_hz(if_hz);
|
||||||
}
|
}
|
||||||
@@ -325,9 +325,9 @@ impl SoapySdrRig {
|
|||||||
let Some((ais_a_idx, ais_b_idx)) = self.ais_channel_indices else {
|
let Some((ais_a_idx, ais_b_idx)) = self.ais_channel_indices else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
for idx in [ais_a_idx, ais_b_idx] {
|
for idx in [ais_a_idx, ais_b_idx] {
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(idx) {
|
if let Some(dsp_arc) = dsps.get(idx) {
|
||||||
dsp_arc
|
dsp_arc
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -341,17 +341,103 @@ impl SoapySdrRig {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let enabled = matches!(self.mode, RigMode::AIS | RigMode::MARINE);
|
let enabled = matches!(self.mode, RigMode::AIS | RigMode::MARINE);
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
for idx in [ais_a_idx, ais_b_idx] {
|
for idx in [ais_a_idx, ais_b_idx] {
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(idx) {
|
if let Some(dsp_arc) = dsps.get(idx) {
|
||||||
dsp_arc.lock().unwrap().set_processing_enabled(enabled);
|
dsp_arc.lock().unwrap().set_processing_enabled(enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Current hardware center frequency (Hz).
|
||||||
|
pub fn center_hz(&self) -> i64 {
|
||||||
|
self.center_hz
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Half of the SDR capture bandwidth (Hz). A virtual channel's dial
|
||||||
|
/// frequency must stay within `center_hz ± half_span_hz`.
|
||||||
|
pub fn half_span_hz(&self) -> i64 {
|
||||||
|
i64::from(self.pipeline.sdr_sample_rate) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a new virtual DSP channel within the current SDR capture
|
||||||
|
/// bandwidth. Returns `None` if `freq_hz` is outside the capture span.
|
||||||
|
///
|
||||||
|
/// The returned senders can be subscribed to for PCM audio frames. The
|
||||||
|
/// `pipeline_slot` index identifies the slot for future
|
||||||
|
/// `virtual_channel_set_freq`, `virtual_channel_set_mode`, and
|
||||||
|
/// `virtual_channel_remove` calls.
|
||||||
|
pub fn virtual_channel_add(
|
||||||
|
&self,
|
||||||
|
freq_hz: u64,
|
||||||
|
mode: &RigMode,
|
||||||
|
bandwidth_hz: u32,
|
||||||
|
fir_taps: usize,
|
||||||
|
) -> Option<(
|
||||||
|
tokio::sync::broadcast::Sender<Vec<f32>>,
|
||||||
|
tokio::sync::broadcast::Sender<Vec<num_complex::Complex<f32>>>,
|
||||||
|
usize,
|
||||||
|
)> {
|
||||||
|
let channel_if_hz = freq_hz as i64 - self.center_hz;
|
||||||
|
if channel_if_hz.unsigned_abs() as i64 > self.half_span_hz() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (pcm_tx, iq_tx) =
|
||||||
|
self.pipeline
|
||||||
|
.add_virtual_channel(channel_if_hz as f64, mode, bandwidth_hz, fir_taps);
|
||||||
|
let slot = self
|
||||||
|
.pipeline
|
||||||
|
.channel_dsps
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.len()
|
||||||
|
.saturating_sub(1);
|
||||||
|
Some((pcm_tx, iq_tx, slot))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a virtual channel at the given pipeline slot index.
|
||||||
|
/// Returns `false` if the slot is out of range.
|
||||||
|
pub fn virtual_channel_remove(&self, pipeline_slot: usize) -> bool {
|
||||||
|
self.pipeline.remove_virtual_channel(pipeline_slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the dial frequency of a virtual channel.
|
||||||
|
/// Returns `false` if the slot is out of range or the freq is outside
|
||||||
|
/// the current capture bandwidth.
|
||||||
|
pub fn virtual_channel_set_freq(&self, pipeline_slot: usize, freq_hz: u64) -> bool {
|
||||||
|
let channel_if_hz = freq_hz as i64 - self.center_hz;
|
||||||
|
if channel_if_hz.unsigned_abs() as i64 > self.half_span_hz() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
if let Some(dsp_arc) = dsps.get(pipeline_slot) {
|
||||||
|
dsp_arc
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_channel_if_hz(channel_if_hz as f64);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the demodulation mode of a virtual channel.
|
||||||
|
/// Returns `false` if the slot is out of range.
|
||||||
|
pub fn virtual_channel_set_mode(&self, pipeline_slot: usize, mode: &RigMode) -> bool {
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
if let Some(dsp_arc) = dsps.get(pipeline_slot) {
|
||||||
|
dsp_arc.lock().unwrap().set_mode(mode);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn subscribe_iq_channel(
|
pub fn subscribe_iq_channel(
|
||||||
&self,
|
&self,
|
||||||
channel_idx: usize,
|
channel_idx: usize,
|
||||||
) -> tokio::sync::broadcast::Receiver<Vec<num_complex::Complex<f32>>> {
|
) -> tokio::sync::broadcast::Receiver<Vec<num_complex::Complex<f32>>> {
|
||||||
|
// iq_senders covers fixed channels only (primary + AIS).
|
||||||
if let Some(sender) = self.pipeline.iq_senders.get(channel_idx) {
|
if let Some(sender) = self.pipeline.iq_senders.get(channel_idx) {
|
||||||
sender.subscribe()
|
sender.subscribe()
|
||||||
} else {
|
} else {
|
||||||
@@ -437,12 +523,15 @@ impl RigCat for SoapySdrRig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
let channel_if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
let mut dsp = dsp_arc.lock().unwrap();
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
dsp.set_channel_if_hz(channel_if_hz);
|
let channel_if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
||||||
if freq_changed {
|
let mut dsp = dsp_arc.lock().unwrap();
|
||||||
dsp.reset_wfm_state();
|
dsp.set_channel_if_hz(channel_if_hz);
|
||||||
|
if freq_changed {
|
||||||
|
dsp.reset_wfm_state();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.update_ais_channel_offsets();
|
self.update_ais_channel_offsets();
|
||||||
@@ -460,9 +549,12 @@ impl RigCat for SoapySdrRig {
|
|||||||
if let Ok(mut cmd) = self.retune_cmd.lock() {
|
if let Ok(mut cmd) = self.retune_cmd.lock() {
|
||||||
*cmd = Some(self.center_hz as f64);
|
*cmd = Some(self.center_hz as f64);
|
||||||
}
|
}
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
let channel_if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
dsp_arc.lock().unwrap().set_channel_if_hz(channel_if_hz);
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
|
let channel_if_hz = (self.freq.hz as i64 - self.center_hz) as f64;
|
||||||
|
dsp_arc.lock().unwrap().set_channel_if_hz(channel_if_hz);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.update_ais_channel_offsets();
|
self.update_ais_channel_offsets();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -478,10 +570,13 @@ impl RigCat for SoapySdrRig {
|
|||||||
self.mode = mode.clone();
|
self.mode = mode.clone();
|
||||||
self.bandwidth_hz = Self::default_bandwidth_for_mode(&mode);
|
self.bandwidth_hz = Self::default_bandwidth_for_mode(&mode);
|
||||||
// Update the primary channel's demodulator in the live pipeline.
|
// Update the primary channel's demodulator in the live pipeline.
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
let mut dsp = dsp_arc.lock().unwrap();
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
dsp.set_mode(&mode);
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
dsp.set_filter(self.bandwidth_hz, self.fir_taps as usize);
|
let mut dsp = dsp_arc.lock().unwrap();
|
||||||
|
dsp.set_mode(&mode);
|
||||||
|
dsp.set_filter(self.bandwidth_hz, self.fir_taps as usize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.apply_ais_channel_activity();
|
self.apply_ais_channel_activity();
|
||||||
self.apply_ais_channel_filters();
|
self.apply_ais_channel_filters();
|
||||||
@@ -501,8 +596,11 @@ impl RigCat for SoapySdrRig {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.wfm_deemphasis_us = deemphasis_us;
|
self.wfm_deemphasis_us = deemphasis_us;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc.lock().unwrap().set_wfm_deemphasis(deemphasis_us);
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
|
dsp_arc.lock().unwrap().set_wfm_deemphasis(deemphasis_us);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -545,11 +643,14 @@ impl RigCat for SoapySdrRig {
|
|||||||
}
|
}
|
||||||
self.squelch_enabled = enabled;
|
self.squelch_enabled = enabled;
|
||||||
self.squelch_threshold_db = threshold_db as f32;
|
self.squelch_threshold_db = threshold_db as f32;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
.lock()
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
.unwrap()
|
dsp_arc
|
||||||
.set_squelch(enabled, self.squelch_threshold_db);
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_squelch(enabled, self.squelch_threshold_db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -654,11 +755,14 @@ impl RigCat for SoapySdrRig {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
tracing::debug!("SoapySdrRig: set_bandwidth -> {} Hz", bandwidth_hz);
|
tracing::debug!("SoapySdrRig: set_bandwidth -> {} Hz", bandwidth_hz);
|
||||||
self.bandwidth_hz = bandwidth_hz;
|
self.bandwidth_hz = bandwidth_hz;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
.lock()
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
.unwrap()
|
dsp_arc
|
||||||
.set_filter(bandwidth_hz, self.fir_taps as usize);
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_filter(bandwidth_hz, self.fir_taps as usize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.apply_ais_channel_filters();
|
self.apply_ais_channel_filters();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -672,11 +776,14 @@ impl RigCat for SoapySdrRig {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
tracing::debug!("SoapySdrRig: set_fir_taps -> {}", taps);
|
tracing::debug!("SoapySdrRig: set_fir_taps -> {}", taps);
|
||||||
self.fir_taps = taps;
|
self.fir_taps = taps;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
.lock()
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
.unwrap()
|
dsp_arc
|
||||||
.set_filter(self.bandwidth_hz, taps as usize);
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_filter(self.bandwidth_hz, taps as usize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.apply_ais_channel_filters();
|
self.apply_ais_channel_filters();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -689,8 +796,11 @@ impl RigCat for SoapySdrRig {
|
|||||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.wfm_stereo = enabled;
|
self.wfm_stereo = enabled;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc.lock().unwrap().set_wfm_stereo(enabled);
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
|
dsp_arc.lock().unwrap().set_wfm_stereo(enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -702,8 +812,11 @@ impl RigCat for SoapySdrRig {
|
|||||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.wfm_denoise = level;
|
self.wfm_denoise = level;
|
||||||
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
|
{
|
||||||
dsp_arc.lock().unwrap().set_wfm_denoise(level);
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||||
|
dsp_arc.lock().unwrap().set_wfm_denoise(level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -713,6 +826,8 @@ impl RigCat for SoapySdrRig {
|
|||||||
let wfm_stereo_detected = self
|
let wfm_stereo_detected = self
|
||||||
.pipeline
|
.pipeline
|
||||||
.channel_dsps
|
.channel_dsps
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
.get(self.primary_channel_idx)
|
.get(self.primary_channel_idx)
|
||||||
.and_then(|dsp| dsp.lock().ok().map(|d| d.wfm_stereo_detected()))
|
.and_then(|dsp| dsp.lock().ok().map(|d| d.wfm_stereo_detected()))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -739,6 +854,8 @@ impl RigCat for SoapySdrRig {
|
|||||||
let rds = self
|
let rds = self
|
||||||
.pipeline
|
.pipeline
|
||||||
.channel_dsps
|
.channel_dsps
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
.get(self.primary_channel_idx)
|
.get(self.primary_channel_idx)
|
||||||
.and_then(|dsp| dsp.lock().ok().and_then(|d| d.rds_data()));
|
.and_then(|dsp| dsp.lock().ok().and_then(|d| d.rds_data()));
|
||||||
Some(SpectrumData {
|
Some(SpectrumData {
|
||||||
|
|||||||
Reference in New Issue
Block a user