[chore](trx-rs): refine pending SDR frontend and backend changes
Signed-off-by: Stan Grams <sjg@haxx.space> Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
@@ -2426,6 +2426,10 @@ let _bwDragEdge = null; // "left" | "right" | null
|
|||||||
let _bwDragStartX = 0;
|
let _bwDragStartX = 0;
|
||||||
let _bwDragStartBwHz = 0;
|
let _bwDragStartBwHz = 0;
|
||||||
|
|
||||||
|
function spectrumBgColor() {
|
||||||
|
return currentTheme() === "light" ? "#eef3fb" : "#0a0f18";
|
||||||
|
}
|
||||||
|
|
||||||
// Returns { loHz, hiHz, visLoHz, visHiHz, fullSpanHz, visSpanHz } and clamps
|
// Returns { loHz, hiHz, visLoHz, visHiHz, fullSpanHz, visSpanHz } and clamps
|
||||||
// panFrac so the view never scrolls past the edges.
|
// panFrac so the view never scrolls past the edges.
|
||||||
function spectrumVisibleRange(data) {
|
function spectrumVisibleRange(data) {
|
||||||
@@ -2506,7 +2510,7 @@ function stopSpectrumStreaming() {
|
|||||||
function clearSpectrumCanvas() {
|
function clearSpectrumCanvas() {
|
||||||
if (!spectrumCanvas) return;
|
if (!spectrumCanvas) return;
|
||||||
const ctx = spectrumCanvas.getContext("2d");
|
const ctx = spectrumCanvas.getContext("2d");
|
||||||
ctx.fillStyle = "#0a0f18";
|
ctx.fillStyle = spectrumBgColor();
|
||||||
ctx.fillRect(0, 0, spectrumCanvas.width, spectrumCanvas.height);
|
ctx.fillRect(0, 0, spectrumCanvas.width, spectrumCanvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2530,7 +2534,7 @@ function drawSpectrum(data) {
|
|||||||
const n = bins.length;
|
const n = bins.length;
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
ctx.fillStyle = "#0a0f18";
|
ctx.fillStyle = spectrumBgColor();
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
if (!n) return;
|
if (!n) return;
|
||||||
@@ -2628,7 +2632,7 @@ function drawSpectrum(data) {
|
|||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
// Tab text
|
// Tab text
|
||||||
ctx.fillStyle = "#0a0f18";
|
ctx.fillStyle = spectrumBgColor();
|
||||||
ctx.textAlign = "left";
|
ctx.textAlign = "left";
|
||||||
ctx.fillText(bwText, tabX + PAD, TAB_H - 4 * dpr);
|
ctx.fillText(bwText, tabX + PAD, TAB_H - 4 * dpr);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
--filter-fg: #e7edf9;
|
--filter-fg: #e7edf9;
|
||||||
--filter-border: #385577;
|
--filter-border: #385577;
|
||||||
--wavelength-fg: #8da3be;
|
--wavelength-fg: #8da3be;
|
||||||
|
--spectrum-bg: #0a0f18;
|
||||||
--jog-wheel-size: 83.2px;
|
--jog-wheel-size: 83.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
--filter-fg: #1f2937;
|
--filter-fg: #1f2937;
|
||||||
--filter-border: #b8c5da;
|
--filter-border: #b8c5da;
|
||||||
--wavelength-fg: #6b7280;
|
--wavelength-fg: #6b7280;
|
||||||
|
--spectrum-bg: #eef3fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
body { font-family: sans-serif; margin: 0; min-height: 100vh; box-sizing: border-box; display: flex; align-items: flex-start; justify-content: center; padding-top: 2em; background: var(--bg); color: var(--text); }
|
body { font-family: sans-serif; margin: 0; min-height: 100vh; box-sizing: border-box; display: flex; align-items: flex-start; justify-content: center; padding-top: 2em; background: var(--bg); color: var(--text); }
|
||||||
@@ -193,7 +195,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
border-right: 1px solid var(--border-light);
|
border-right: 1px solid var(--border-light);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 0.8rem;
|
padding: 0 0.65rem;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -238,8 +240,9 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|||||||
.hint { color: var(--text-muted); font-size: 0.85rem; }
|
.hint { color: var(--text-muted); font-size: 0.85rem; }
|
||||||
.inline { display: flex; gap: 0.5rem; align-items: center; }
|
.inline { display: flex; gap: 0.5rem; align-items: center; }
|
||||||
.freq-inline {
|
.freq-inline {
|
||||||
|
gap: 0.35rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
.freq-field {
|
.freq-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -254,6 +257,10 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.center-frequency-col {
|
||||||
|
flex: 0 1 8.75rem;
|
||||||
|
min-width: 7.75rem;
|
||||||
|
}
|
||||||
.frequency-col input.status-input {
|
.frequency-col input.status-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3.35rem;
|
height: 3.35rem;
|
||||||
@@ -285,7 +292,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
.wavelength-display {
|
.wavelength-display {
|
||||||
min-width: 6.2rem;
|
min-width: 5.2rem;
|
||||||
height: 3.35rem;
|
height: 3.35rem;
|
||||||
padding: 0 0.7rem;
|
padding: 0 0.7rem;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
@@ -568,6 +575,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
.card { padding: 1rem; }
|
.card { padding: 1rem; }
|
||||||
button { min-height: 2.8rem; font-size: 0.95rem; }
|
button { min-height: 2.8rem; font-size: 0.95rem; }
|
||||||
input.status-input, select.status-input { font-size: 1.1rem; }
|
input.status-input, select.status-input { font-size: 1.1rem; }
|
||||||
|
.freq-inline { gap: 0.5rem; }
|
||||||
|
.freq-inline { flex-wrap: wrap; }
|
||||||
.header-text { width: auto; min-width: 0; flex: 0 1 auto; }
|
.header-text { width: auto; min-width: 0; flex: 0 1 auto; }
|
||||||
.header-signal-wrap { display: none; }
|
.header-signal-wrap { display: none; }
|
||||||
.header-right { align-items: flex-end; }
|
.header-right { align-items: flex-end; }
|
||||||
@@ -599,7 +608,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
background: #0a0f18;
|
background: var(--spectrum-bg);
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|||||||
@@ -479,9 +479,14 @@ pub fn spawn_audio_playback(
|
|||||||
let device_name = cfg.device.clone();
|
let device_name = cfg.device.clone();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Err(e) =
|
if let Err(e) = run_playback(
|
||||||
run_playback(sample_rate, channels, frame_duration_ms, device_name, rx, shutdown_rx)
|
sample_rate,
|
||||||
{
|
channels,
|
||||||
|
frame_duration_ms,
|
||||||
|
device_name,
|
||||||
|
rx,
|
||||||
|
shutdown_rx,
|
||||||
|
) {
|
||||||
error!("Audio playback thread error: {}", e);
|
error!("Audio playback thread error: {}", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -634,21 +634,13 @@ impl ServerConfig {
|
|||||||
.map(|(idx, rig)| {
|
.map(|(idx, rig)| {
|
||||||
let id = if rig.id.trim().is_empty() {
|
let id = if rig.id.trim().is_empty() {
|
||||||
// Generate ID from model name with counter.
|
// Generate ID from model name with counter.
|
||||||
let model = rig
|
let model = rig.rig.model.as_deref().unwrap_or("unknown").to_lowercase();
|
||||||
.rig
|
|
||||||
.model
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_lowercase();
|
|
||||||
format!("{}_{}", model, idx)
|
format!("{}_{}", model, idx)
|
||||||
} else {
|
} else {
|
||||||
rig.id.clone()
|
rig.id.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
RigInstanceConfig {
|
RigInstanceConfig { id, ..rig.clone() }
|
||||||
id,
|
|
||||||
..rig.clone()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -502,11 +502,8 @@ fn spawn_rig_audio_stack(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut encoder = match opus::Encoder::new(
|
let mut encoder =
|
||||||
sdr_sample_rate,
|
match opus::Encoder::new(sdr_sample_rate, opus_ch, opus::Application::Audio) {
|
||||||
opus_ch,
|
|
||||||
opus::Application::Audio,
|
|
||||||
) {
|
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("SDR audio: Opus encoder init failed: {}", e);
|
tracing::error!("SDR audio: Opus encoder init failed: {}", e);
|
||||||
|
|||||||
@@ -172,10 +172,8 @@ impl BlockFirFilter {
|
|||||||
let ifft = planner.plan_fft_inverse(fft_size);
|
let ifft = planner.plan_fft_inverse(fft_size);
|
||||||
|
|
||||||
// Pre-compute H(f) = FFT of zero-padded coefficients.
|
// Pre-compute H(f) = FFT of zero-padded coefficients.
|
||||||
let mut h_buf: Vec<FftComplex<f32>> = coeffs
|
let mut h_buf: Vec<FftComplex<f32>> =
|
||||||
.iter()
|
coeffs.iter().map(|&c| FftComplex::new(c, 0.0)).collect();
|
||||||
.map(|&c| FftComplex::new(c, 0.0))
|
|
||||||
.collect();
|
|
||||||
h_buf.resize(fft_size, FftComplex::new(0.0, 0.0));
|
h_buf.resize(fft_size, FftComplex::new(0.0, 0.0));
|
||||||
fft.process(&mut h_buf);
|
fft.process(&mut h_buf);
|
||||||
|
|
||||||
@@ -384,8 +382,7 @@ impl ChannelDsp {
|
|||||||
mixed_q[idx] = sample.re * lo_im + sample.im * lo_re;
|
mixed_q[idx] = sample.re * lo_im + sample.im * lo_re;
|
||||||
}
|
}
|
||||||
// Advance phase with wrap to avoid precision loss.
|
// Advance phase with wrap to avoid precision loss.
|
||||||
self.mixer_phase =
|
self.mixer_phase = (phase_start + n as f64 * phase_inc).rem_euclid(std::f64::consts::TAU);
|
||||||
(phase_start + n as f64 * phase_inc).rem_euclid(std::f64::consts::TAU);
|
|
||||||
|
|
||||||
// --- 2. FFT FIR (overlap-save) --------------------------------------
|
// --- 2. FFT FIR (overlap-save) --------------------------------------
|
||||||
let filtered_i = self.lpf_i.filter_block(&mixed_i);
|
let filtered_i = self.lpf_i.filter_block(&mixed_i);
|
||||||
@@ -471,8 +468,7 @@ impl SdrPipeline {
|
|||||||
let thread_dsps: Vec<Arc<Mutex<ChannelDsp>>> = channel_dsps.clone();
|
let thread_dsps: Vec<Arc<Mutex<ChannelDsp>>> = 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>>> =
|
let retune_cmd: Arc<std::sync::Mutex<Option<f64>>> = Arc::new(std::sync::Mutex::new(None));
|
||||||
Arc::new(std::sync::Mutex::new(None));
|
|
||||||
let thread_retune_cmd = retune_cmd.clone();
|
let thread_retune_cmd = retune_cmd.clone();
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
@@ -529,9 +525,7 @@ fn iq_read_loop(
|
|||||||
|
|
||||||
// Pre-compute Hann window coefficients.
|
// Pre-compute Hann window coefficients.
|
||||||
let hann_window: Vec<f32> = (0..SPECTRUM_FFT_SIZE)
|
let hann_window: Vec<f32> = (0..SPECTRUM_FFT_SIZE)
|
||||||
.map(|i| {
|
.map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos()))
|
||||||
0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos())
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut planner = FftPlanner::<f32>::new();
|
let mut planner = FftPlanner::<f32>::new();
|
||||||
@@ -683,16 +677,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn channel_dsp_processes_silence() {
|
fn channel_dsp_processes_silence() {
|
||||||
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(8);
|
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(8);
|
||||||
let mut dsp = ChannelDsp::new(
|
let mut dsp = ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 20, 3000, 31, pcm_tx);
|
||||||
0.0,
|
|
||||||
&RigMode::USB,
|
|
||||||
48_000,
|
|
||||||
8_000,
|
|
||||||
20,
|
|
||||||
3000,
|
|
||||||
31,
|
|
||||||
pcm_tx,
|
|
||||||
);
|
|
||||||
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
|
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
|
||||||
dsp.process_block(&block);
|
dsp.process_block(&block);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user