[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 <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 23:15:01 +01:00
parent ae5edabc27
commit 6d2d647511
3 changed files with 28 additions and 156 deletions
+28 -110
View File
@@ -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<Self, String> {
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<u64>,
_base_freq_hz: Option<u64>,
) -> Result<Vec<WsprDecodeResult>, 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<PathBuf, String> {
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<u64>) -> Vec<WsprDecodeResult> {
output
.lines()
.filter_map(|line| parse_wsprd_line(line, base_freq_hz))
.collect()
}
fn parse_wsprd_line(line: &str, base_freq_hz: Option<u64>) -> Option<WsprDecodeResult> {
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::<Vec<_>>().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::<f32>();
(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);
}
}
-2
View File
@@ -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;
-44
View File
@@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2026 Stanislaw Grams <stanislawgrams@gmail.com>
//
// 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<String>) -> 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<String, String> {
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())
}
}