use crate::error::{Error, Result};
use boostr::model::audio::stft::{CenterPad, IStftOptions, IStftPadding, StftOptions, istft, stft};
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use numr::tensor::Tensor;
#[derive(Debug, Clone, Copy)]
pub struct DenoiseOptions {
pub n_fft: usize,
pub hop_length: usize,
pub noise_frame_fraction: f64,
pub min_dynamic_range_db: f64,
pub over_subtraction_db: f64,
pub gain_floor_db: f64,
pub freq_smooth_bins: usize,
pub time_smooth_frames: usize,
}
impl Default for DenoiseOptions {
fn default() -> Self {
Self {
n_fft: 1024,
hop_length: 256,
noise_frame_fraction: 0.10,
min_dynamic_range_db: 6.0,
over_subtraction_db: 6.0,
gain_floor_db: -30.0,
freq_smooth_bins: 0,
time_smooth_frames: 2,
}
}
}
fn hann(n: usize) -> Vec<f32> {
(0..n)
.map(|i| (0.5 - 0.5 * (std::f64::consts::TAU * i as f64 / n as f64).cos()) as f32)
.collect()
}
pub fn noise_floor_dbfs(samples: &[f32], rate: u32) -> f64 {
if rate == 0 {
return f64::NEG_INFINITY;
}
let frame = (rate as usize / 50).max(1);
if samples.len() < frame {
return f64::NEG_INFINITY;
}
let mut rms: Vec<f64> = samples
.chunks_exact(frame)
.map(|c| (c.iter().map(|&s| (s as f64) * (s as f64)).sum::<f64>() / frame as f64).sqrt())
.collect();
rms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((rms.len() as f64 * 0.10) as usize).min(rms.len() - 1);
let v = rms[idx];
if v > 0.0 {
20.0 * v.log10()
} else {
f64::NEG_INFINITY
}
}
pub fn denoise(samples: &[f32], opts: DenoiseOptions) -> Result<Vec<f32>> {
run(samples, None, opts)
}
pub fn denoise_with_profile(
samples: &[f32],
noise_clip: &[f32],
opts: DenoiseOptions,
) -> Result<Vec<f32>> {
if noise_clip.len() < opts.n_fft {
return Err(Error::InvalidArgument {
arg: "noise_clip",
reason: format!(
"need at least n_fft ({}) samples of silence, got {}",
opts.n_fft,
noise_clip.len()
),
});
}
run(samples, Some(noise_clip), opts)
}
struct Analysis {
mag: Vec<f32>,
phase: Tensor<CpuRuntime>,
shape: Vec<usize>,
f_bins: usize,
t_frames: usize,
}
fn run(samples: &[f32], noise_clip: Option<&[f32]>, opts: DenoiseOptions) -> Result<Vec<f32>> {
validate(&opts)?;
if samples.len() < opts.n_fft {
return Ok(samples.to_vec());
}
let device = CpuDevice::default();
let client = CpuClient::new(device.clone());
let window = Tensor::<CpuRuntime>::from_slice(&hann(opts.n_fft), &[opts.n_fft], &device)?;
let st = StftOptions {
n_fft: opts.n_fft,
hop_length: opts.hop_length,
center: true,
pad: CenterPad::Zeros,
};
let analyze = |sig: &[f32]| -> Result<Analysis> {
let wave = Tensor::<CpuRuntime>::from_slice(sig, &[1, sig.len()], &device)?;
let (mag, phase) = stft(&client, &wave, &window, st)?;
let shape = mag.shape().to_vec();
Ok(Analysis {
f_bins: shape[1],
t_frames: shape[2],
mag: mag.contiguous()?.to_vec(),
phase,
shape,
})
};
let signal = analyze(samples)?;
let (f_bins, t_frames) = (signal.f_bins, signal.t_frames);
let noise = match noise_clip {
Some(clip) => {
let n = analyze(clip)?;
debug_assert_eq!(n.f_bins, f_bins);
let all: Vec<usize> = (0..n.t_frames).collect();
mean_spectrum(&n.mag, n.f_bins, n.t_frames, &all)
}
None => match quiet_frame_profile(&signal.mag, f_bins, t_frames, &opts) {
Some(profile) => profile,
None => return Ok(samples.to_vec()),
},
};
let mask = build_mask(&signal.mag, &noise, f_bins, t_frames, &opts);
let masked: Vec<f32> = signal
.mag
.iter()
.zip(mask.iter())
.map(|(&m, &g)| m * g)
.collect();
let masked = Tensor::<CpuRuntime>::from_slice(&masked, &signal.shape, &device)?;
let back = istft(
&client,
&masked,
&signal.phase,
&window,
IStftOptions {
hop_length: opts.hop_length,
padding: IStftPadding::Center,
eps: 1e-8,
},
)?;
let mut out: Vec<f32> = back.to_vec();
out.resize(samples.len(), 0.0);
Ok(out)
}
fn validate(opts: &DenoiseOptions) -> Result<()> {
if opts.n_fft == 0 || opts.hop_length == 0 {
return Err(Error::InvalidArgument {
arg: "opts",
reason: "n_fft and hop_length must be > 0".into(),
});
}
if !(0.0..=1.0).contains(&opts.noise_frame_fraction) || opts.noise_frame_fraction == 0.0 {
return Err(Error::InvalidArgument {
arg: "noise_frame_fraction",
reason: format!("must be in (0, 1], got {}", opts.noise_frame_fraction),
});
}
Ok(())
}
fn mean_spectrum(mag: &[f32], f_bins: usize, t_frames: usize, frames: &[usize]) -> Vec<f32> {
let mut out = vec![0.0f32; f_bins];
if frames.is_empty() {
return out;
}
for k in 0..f_bins {
let row = &mag[k * t_frames..(k + 1) * t_frames];
let sum: f64 = frames.iter().map(|&t| row[t] as f64).sum();
out[k] = (sum / frames.len() as f64) as f32;
}
out
}
fn quiet_frame_profile(
mag: &[f32],
f_bins: usize,
t_frames: usize,
opts: &DenoiseOptions,
) -> Option<Vec<f32>> {
let mut energy: Vec<(f64, usize)> = (0..t_frames)
.map(|t| {
let e: f64 = (0..f_bins)
.map(|k| {
let m = mag[k * t_frames + t] as f64;
m * m
})
.sum();
(e, t)
})
.collect();
energy.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let take = ((t_frames as f64 * opts.noise_frame_fraction) as usize).clamp(1, t_frames);
let quiet_mean: f64 = energy[..take].iter().map(|e| e.0).sum::<f64>() / take as f64;
let loud = energy[((t_frames as f64 * 0.9) as usize).min(t_frames - 1)].0;
if quiet_mean <= 0.0 || loud <= 0.0 {
return None;
}
let range_db = 10.0 * (loud / quiet_mean).log10();
if range_db < opts.min_dynamic_range_db {
return None;
}
let frames: Vec<usize> = energy[..take].iter().map(|e| e.1).collect();
Some(mean_spectrum(mag, f_bins, t_frames, &frames))
}
fn build_mask(
mag: &[f32],
noise: &[f32],
f_bins: usize,
t_frames: usize,
opts: &DenoiseOptions,
) -> Vec<f32> {
let threshold_scale = 10f64.powf(opts.over_subtraction_db / 20.0);
let floor = 10f32.powf(opts.gain_floor_db as f32 / 20.0);
let mut raw = vec![0.0f32; f_bins * t_frames];
for k in 0..f_bins {
let row = &mag[k * t_frames..(k + 1) * t_frames];
let threshold = noise[k] as f64 * threshold_scale;
for (t, &m) in row.iter().enumerate() {
let g = if m as f64 > 0.0 {
((m as f64 - threshold) / m as f64).clamp(0.0, 1.0)
} else {
0.0
};
raw[k * t_frames + t] = g as f32;
}
}
box_smooth(
&raw,
f_bins,
t_frames,
opts.freq_smooth_bins,
opts.time_smooth_frames,
)
.into_iter()
.map(|g| g.max(floor))
.collect()
}
fn box_smooth(src: &[f32], f_bins: usize, t_frames: usize, fw: usize, tw: usize) -> Vec<f32> {
let mut tmp = vec![0.0f32; src.len()];
for k in 0..f_bins {
let row = &src[k * t_frames..(k + 1) * t_frames];
for t in 0..t_frames {
let lo = t.saturating_sub(tw);
let hi = (t + tw + 1).min(t_frames);
let span = &row[lo..hi];
tmp[k * t_frames + t] = span.iter().sum::<f32>() / span.len() as f32;
}
}
let mut out = vec![0.0f32; src.len()];
for t in 0..t_frames {
for k in 0..f_bins {
let lo = k.saturating_sub(fw);
let hi = (k + fw + 1).min(f_bins);
let mut sum = 0.0f32;
for kk in lo..hi {
sum += tmp[kk * t_frames + t];
}
out[k * t_frames + t] = sum / (hi - lo) as f32;
}
}
out
}