use alloc::vec;
use alloc::vec::Vec;
use core::f32::consts::PI;
#[cfg(not(feature = "std"))]
use num_traits::Float;
#[derive(Clone, Copy, Debug)]
pub struct SubtractCfg {
pub sample_rate: f32,
pub tone_spacing_hz: f32,
pub samples_per_symbol: usize,
pub base_offset_s: f32,
pub gfsk: Option<GfskParams>,
}
#[derive(Clone, Copy, Debug)]
pub struct GfskParams {
pub bt: f32,
pub hmod: f32,
pub ramp_samples: usize,
}
fn generate_iq(tones: &[u8], freq_hz: f32, cfg: &SubtractCfg) -> (Vec<f32>, Vec<f32>) {
let n = tones.len() * cfg.samples_per_symbol;
if let Some(g) = cfg.gfsk {
let gfsk_cfg = crate::core::dsp::gfsk::GfskCfg {
sample_rate: cfg.sample_rate,
samples_per_symbol: cfg.samples_per_symbol,
bt: g.bt,
hmod: g.hmod,
ramp_samples: g.ramp_samples,
};
let mut w_cos = vec![0.0f32; n];
let mut w_sin = vec![0.0f32; n];
crate::core::dsp::gfsk::synth_complex_f32_into(
&mut w_cos, &mut w_sin, tones, freq_hz, 1.0, &gfsk_cfg,
);
return (w_cos, w_sin);
}
let mut w_cos = vec![0.0f32; n];
let mut w_sin = vec![0.0f32; n];
let mut phase = 0.0f32;
for (sym, &tone) in tones.iter().enumerate() {
let freq = freq_hz + tone as f32 * cfg.tone_spacing_hz;
let dphi = 2.0 * PI * freq / cfg.sample_rate;
let base = sym * cfg.samples_per_symbol;
for j in 0..cfg.samples_per_symbol {
w_cos[base + j] = phase.cos();
w_sin[base + j] = phase.sin();
phase += dphi;
if phase > PI {
phase -= 2.0 * PI;
}
}
}
(w_cos, w_sin)
}
fn ls_amp_mag(audio: &[i16], tones: &[u8], freq_hz: f32, dt_sec: f32, cfg: &SubtractCfg) -> f32 {
let (w_cos, w_sin) = generate_iq(tones, freq_hz, cfg);
let signed_start = ((cfg.base_offset_s + dt_sec) * cfg.sample_rate).round() as i64;
let (audio_off, ref_off) = if signed_start < 0 {
(0usize, (-signed_start) as usize)
} else {
(signed_start as usize, 0usize)
};
if ref_off >= w_cos.len() {
return 0.0;
}
let len = (w_cos.len() - ref_off).min(audio.len().saturating_sub(audio_off));
if len == 0 {
return 0.0;
}
let (mut na, mut nb, mut da, mut db) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for i in 0..len {
let rx = audio[audio_off + i] as f64;
let c = w_cos[ref_off + i] as f64;
let s = w_sin[ref_off + i] as f64;
na += rx * c;
nb += rx * s;
da += c * c;
db += s * s;
}
let a = if da > 0.0 { na / da } else { 0.0 };
let b = if db > 0.0 { nb / db } else { 0.0 };
((a * a + b * b) as f32).sqrt()
}
pub fn refine_freq(
audio: &[i16],
tones: &[u8],
freq_hz_init: f32,
dt_sec: f32,
cfg: &SubtractCfg,
radius_hz: f32,
step_hz: f32,
) -> f32 {
debug_assert!(cfg.gfsk.is_some(), "refine_freq requires GFSK shaping");
let mut best_freq = freq_hz_init;
let mut best_amp = ls_amp_mag(audio, tones, freq_hz_init, dt_sec, cfg);
let mut df = -radius_hz;
while df <= radius_hz {
if df.abs() > f32::EPSILON {
let a = ls_amp_mag(audio, tones, freq_hz_init + df, dt_sec, cfg);
if a > best_amp {
best_amp = a;
best_freq = freq_hz_init + df;
}
}
df += step_hz;
}
best_freq
}
pub fn subtract_tones_lpf(
audio: &mut [i16],
tones: &[u8],
freq_hz: f32,
dt_sec: f32,
cfg: &SubtractCfg,
lpf_half: usize,
) {
let nframe = tones.len() * cfg.samples_per_symbol;
let (cref_re, cref_im) = generate_iq(tones, freq_hz, cfg);
let signed_start = ((cfg.base_offset_s + dt_sec) * cfg.sample_rate).round() as i64;
let (audio_off, ref_off) = if signed_start < 0 {
(0usize, (-signed_start) as usize)
} else {
(signed_start as usize, 0usize)
};
if ref_off >= nframe {
return;
}
let len = (nframe - ref_off).min(audio.len().saturating_sub(audio_off));
if len == 0 {
return;
}
let mut camp_re = vec![0.0f32; len];
let mut camp_im = vec![0.0f32; len];
for i in 0..len {
let rx = audio[audio_off + i] as f32;
camp_re[i] = rx * cref_re[ref_off + i];
camp_im[i] = -rx * cref_im[ref_off + i];
}
let nk = 2 * lpf_half + 1;
let mut kern = vec![0.0f32; nk];
let mut sumw = 0.0f32;
for j in 0..nk {
let x = (j as f32 - lpf_half as f32) * PI / lpf_half as f32;
let w = x.cos().powi(2);
kern[j] = w;
sumw += w;
}
for w in kern.iter_mut() {
*w /= sumw;
}
let mut cfilt_re = vec![0.0f32; len];
let mut cfilt_im = vec![0.0f32; len];
for i in 0..len {
let (mut sr, mut si, mut sw) = (0.0f32, 0.0f32, 0.0f32);
let lo = (i as i64 - lpf_half as i64).max(0) as usize;
let hi = (i + lpf_half + 1).min(len);
for j in lo..hi {
let k = j as i64 - i as i64 + lpf_half as i64;
if k < 0 || k as usize >= nk {
continue;
}
let w = kern[k as usize];
sr += w * camp_re[j];
si += w * camp_im[j];
sw += w;
}
if sw > f32::EPSILON {
cfilt_re[i] = sr / sw;
cfilt_im[i] = si / sw;
}
}
for i in 0..len {
let cr = cref_re[ref_off + i];
let ci = cref_im[ref_off + i];
let sub = 2.0 * (cfilt_re[i] * cr - cfilt_im[i] * ci);
let v = audio[audio_off + i] as f32 - sub;
audio[audio_off + i] = v.clamp(-32_768.0, 32_767.0) as i16;
}
}
#[inline]
pub fn subtract_tones(
audio: &mut [i16],
tones: &[u8],
freq_hz: f32,
dt_sec: f32,
gain: f32,
cfg: &SubtractCfg,
) {
let (w_cos, w_sin) = generate_iq(tones, freq_hz, cfg);
let signed_start = ((cfg.base_offset_s + dt_sec) * cfg.sample_rate).round() as i64;
let (audio_off, ref_off) = if signed_start < 0 {
(0usize, (-signed_start) as usize)
} else {
(signed_start as usize, 0usize)
};
if ref_off >= w_cos.len() {
return;
}
let len = (w_cos.len() - ref_off).min(audio.len().saturating_sub(audio_off));
if len == 0 {
return;
}
let (num_a, num_b, den_a, den_b) =
(0..len).fold((0.0f32, 0.0f32, 0.0f32, 0.0f32), |(na, nb, da, db), i| {
let rx = audio[audio_off + i] as f32;
let wc = w_cos[ref_off + i];
let ws = w_sin[ref_off + i];
(na + rx * wc, nb + rx * ws, da + wc * wc, db + ws * ws)
});
let a = if den_a > f32::EPSILON {
num_a / den_a
} else {
0.0
};
let b = if den_b > f32::EPSILON {
num_b / den_b
} else {
0.0
};
for i in 0..len {
let sub = gain * (a * w_cos[ref_off + i] + b * w_sin[ref_off + i]);
let new_val = audio[audio_off + i] as f32 - sub;
audio[audio_off + i] = new_val.clamp(-32_768.0, 32_767.0) as i16;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subtract_tones_negative_dt_aligns_via_ref_offset() {
let cfg = SubtractCfg {
sample_rate: 12_000.0,
tone_spacing_hz: 6.25,
samples_per_symbol: 1920,
base_offset_s: 0.5,
gfsk: None,
};
let tones: Vec<u8> = (0..79).map(|k| (k % 8) as u8).collect();
let (w_cos, _w_sin) = generate_iq(&tones, 1500.0, &cfg);
let shift = 3360usize;
let amp = 5000.0f32;
let mut audio: Vec<i16> = vec![0i16; 180_000];
let n = w_cos.len() - shift;
for i in 0..n.min(audio.len()) {
audio[i] = (amp * w_cos[shift + i]).clamp(-32_768.0, 32_767.0) as i16;
}
let pre_energy: f64 = audio.iter().map(|&s| (s as f64) * (s as f64)).sum();
let dt_sec = -(shift as f32) / cfg.sample_rate - cfg.base_offset_s;
subtract_tones(&mut audio, &tones, 1500.0, dt_sec, 1.0, &cfg);
let post_energy: f64 = audio.iter().map(|&s| (s as f64) * (s as f64)).sum();
let drop_db = 10.0 * (post_energy / pre_energy).log10();
assert!(
drop_db < -30.0,
"subtract_tones failed to remove signal at dt_sec={dt_sec:.3} \
(drop only {drop_db:.1} dB; expected < -30 dB). \
Pre-fix bug: `start as usize` saturated negative to 0."
);
}
#[test]
fn subtract_tones_positive_dt_works() {
let cfg = SubtractCfg {
sample_rate: 12_000.0,
tone_spacing_hz: 6.25,
samples_per_symbol: 1920,
base_offset_s: 0.5,
gfsk: None,
};
let tones: Vec<u8> = (0..79).map(|k| (k % 8) as u8).collect();
let (w_cos, _) = generate_iq(&tones, 1500.0, &cfg);
let dt_sec: f32 = 0.2;
let start = ((cfg.base_offset_s + dt_sec) * cfg.sample_rate).round() as usize;
let amp = 5000.0f32;
let mut audio: Vec<i16> = vec![0i16; 180_000];
for i in 0..w_cos.len() {
audio[start + i] = (amp * w_cos[i]).clamp(-32_768.0, 32_767.0) as i16;
}
let pre: f64 = audio.iter().map(|&s| (s as f64) * (s as f64)).sum();
subtract_tones(&mut audio, &tones, 1500.0, dt_sec, 1.0, &cfg);
let post: f64 = audio.iter().map(|&s| (s as f64) * (s as f64)).sum();
let drop_db = 10.0 * (post / pre).log10();
assert!(
drop_db < -30.0,
"positive-DT subtract drop only {drop_db:.1} dB"
);
}
}