From 6d2d64751178e919badd698bde18ebe3613c910a Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Thu, 12 Feb 2026 23:15:01 +0100 Subject: [PATCH] [refactor](trx-wspr): remove external wsprd integration Drop all external wsprd wrapper/build plumbing and keep trx-wspr on an internal Rust-only decoder path. This removes wsprd process dependencies and leaves a native decoder scaffold with the same public API for incremental algorithm work. Co-authored-by: Codex Signed-off-by: Stanislaw Grams --- src/trx-wspr/src/decoder.rs | 138 ++++++------------------------ src/trx-wspr/src/lib.rs | 2 - src/trx-wspr/src/wsprd_wrapper.rs | 44 ---------- 3 files changed, 28 insertions(+), 156 deletions(-) delete mode 100644 src/trx-wspr/src/wsprd_wrapper.rs diff --git a/src/trx-wspr/src/decoder.rs b/src/trx-wspr/src/decoder.rs index 4800b60..08a0146 100644 --- a/src/trx-wspr/src/decoder.rs +++ b/src/trx-wspr/src/decoder.rs @@ -2,11 +2,6 @@ // // SPDX-License-Identifier: BSD-2-Clause -use crate::wsprd_wrapper::WsprdWrapper; -use std::fs; -use std::io::Write; -use std::path::PathBuf; - const WSPR_SAMPLE_RATE: u32 = 12_000; const SLOT_SAMPLES: usize = 120 * WSPR_SAMPLE_RATE as usize; @@ -19,16 +14,12 @@ pub struct WsprDecodeResult { } pub struct WsprDecoder { - wsprd: WsprdWrapper, + min_rms: f32, } impl WsprDecoder { pub fn new() -> Result { - let wsprd = WsprdWrapper::default_binary(); - if !wsprd.is_available() { - return Err("wsprd not found in PATH".to_string()); - } - Ok(Self { wsprd }) + Ok(Self { min_rms: 0.0005 }) } pub fn sample_rate(&self) -> u32 { @@ -42,106 +33,30 @@ impl WsprDecoder { pub fn decode_slot( &self, samples: &[f32], - base_freq_hz: Option, + _base_freq_hz: Option, ) -> Result, String> { if samples.len() < SLOT_SAMPLES { return Ok(Vec::new()); } - let wav_path = write_temp_wav(samples)?; - let output = self.wsprd.decode_wav(&wav_path); - let _ = fs::remove_file(&wav_path); - let stdout = output?; - Ok(parse_wsprd_output(&stdout, base_freq_hz)) + + // Native Rust implementation scaffold: + // keep a strict "no decode on noise-only slots" gate while protocol/DSP + // stages are implemented. + let rms = slot_rms(&samples[..SLOT_SAMPLES]); + if rms < self.min_rms { + return Ok(Vec::new()); + } + + Ok(Vec::new()) } } -fn write_temp_wav(samples: &[f32]) -> Result { - let mut path = std::env::temp_dir(); - let unique = format!( - "trx-wspr-{}-{}.wav", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| e.to_string())? - .as_millis() - ); - path.push(unique); - - let mut file = fs::File::create(&path) - .map_err(|e| format!("failed to create temp wav {}: {}", path.display(), e))?; - - let num_samples = samples.len() as u32; - let data_bytes = num_samples * 2; - let riff_size = 36 + data_bytes; - let byte_rate = WSPR_SAMPLE_RATE * 2; - - file.write_all(b"RIFF").map_err(|e| e.to_string())?; - file.write_all(&riff_size.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(b"WAVE").map_err(|e| e.to_string())?; - file.write_all(b"fmt ").map_err(|e| e.to_string())?; - file.write_all(&16u32.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&1u16.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&1u16.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&WSPR_SAMPLE_RATE.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&byte_rate.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&2u16.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(&16u16.to_le_bytes()) - .map_err(|e| e.to_string())?; - file.write_all(b"data").map_err(|e| e.to_string())?; - file.write_all(&data_bytes.to_le_bytes()) - .map_err(|e| e.to_string())?; - - for &sample in samples.iter().take(SLOT_SAMPLES) { - let clamped = sample.clamp(-1.0, 1.0); - let pcm = (clamped * i16::MAX as f32) as i16; - file.write_all(&pcm.to_le_bytes()) - .map_err(|e| e.to_string())?; +fn slot_rms(samples: &[f32]) -> f32 { + if samples.is_empty() { + return 0.0; } - - Ok(path) -} - -fn parse_wsprd_output(output: &str, base_freq_hz: Option) -> Vec { - output - .lines() - .filter_map(|line| parse_wsprd_line(line, base_freq_hz)) - .collect() -} - -fn parse_wsprd_line(line: &str, base_freq_hz: Option) -> Option { - let fields: Vec<&str> = line.split_whitespace().collect(); - if fields.len() < 6 { - return None; - } - - let snr_db: f32 = fields.get(1)?.parse().ok()?; - let dt_s: f32 = fields.get(2)?.parse().ok()?; - let decoded_freq_hz: f32 = fields.get(3)?.parse().ok()?; - - let message = fields.iter().skip(5).copied().collect::>().join(" "); - if message.is_empty() { - return None; - } - - let freq_hz = if let Some(base) = base_freq_hz { - decoded_freq_hz - base as f32 - } else { - decoded_freq_hz - }; - - Some(WsprDecodeResult { - message, - snr_db, - dt_s, - freq_hz, - }) + let sum_sq = samples.iter().map(|s| s * s).sum::(); + (sum_sq / samples.len() as f32).sqrt() } #[cfg(test)] @@ -149,12 +64,15 @@ mod tests { use super::*; #[test] - fn parse_line_basic() { - let line = "0001 -24 0.3 14097100 -1 CQ TEST FN20 37"; - let parsed = parse_wsprd_line(line, Some(14_097_000)).expect("parse"); - assert_eq!(parsed.message, "CQ TEST FN20 37"); - assert_eq!(parsed.snr_db, -24.0); - assert_eq!(parsed.dt_s, 0.3); - assert_eq!(parsed.freq_hz, 100.0); + fn short_slot_returns_empty() { + let dec = WsprDecoder::new().expect("decoder"); + let out = dec.decode_slot(&vec![0.0; dec.slot_samples() - 1], None); + assert!(out.expect("decode").is_empty()); + } + + #[test] + fn rms_is_zero_for_silence() { + let rms = slot_rms(&[0.0; 16]); + assert_eq!(rms, 0.0); } } diff --git a/src/trx-wspr/src/lib.rs b/src/trx-wspr/src/lib.rs index cb903bf..c17ef5d 100644 --- a/src/trx-wspr/src/lib.rs +++ b/src/trx-wspr/src/lib.rs @@ -3,7 +3,5 @@ // SPDX-License-Identifier: BSD-2-Clause mod decoder; -mod wsprd_wrapper; pub use decoder::{WsprDecodeResult, WsprDecoder}; -pub use wsprd_wrapper::WsprdWrapper; diff --git a/src/trx-wspr/src/wsprd_wrapper.rs b/src/trx-wspr/src/wsprd_wrapper.rs deleted file mode 100644 index ad9979b..0000000 --- a/src/trx-wspr/src/wsprd_wrapper.rs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -use std::path::Path; -use std::process::Command; - -#[derive(Debug, Clone)] -pub struct WsprdWrapper { - binary: String, -} - -impl WsprdWrapper { - pub fn new(binary: impl Into) -> Self { - Self { - binary: binary.into(), - } - } - - pub fn default_binary() -> Self { - Self::new("wsprd") - } - - pub fn is_available(&self) -> bool { - Command::new(&self.binary).output().is_ok() - } - - pub fn decode_wav(&self, wav_path: &Path) -> Result { - let output = Command::new(&self.binary) - .arg(wav_path) - .output() - .map_err(|e| format!("failed to run {}: {}", self.binary, e))?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!( - "{} failed with status {}: {}", - self.binary, - output.status, - stderr.trim() - )); - } - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } -}