use crate::core::fft::COMPLEX_ZERO;
use crate::core::window::{generate_window, WindowType};
use rustfft::{num_complex::Complex, FftPlanner};
#[derive(Debug, Clone, Copy)]
pub struct HpssParams {
pub harmonic_width: usize,
pub percussive_width: usize,
}
impl Default for HpssParams {
fn default() -> Self {
Self {
harmonic_width: 17,
percussive_width: 17,
}
}
}
pub fn hpss(
input: &[f32],
fft_size: usize,
hop_size: usize,
params: &HpssParams,
) -> (Vec<f32>, Vec<f32>) {
if input.len() < fft_size {
return (input.to_vec(), vec![0.0; input.len()]);
}
let num_bins = fft_size / 2 + 1;
let num_frames = (input.len() - fft_size) / hop_size + 1;
if num_frames == 0 {
return (input.to_vec(), vec![0.0; input.len()]);
}
let window = generate_window(WindowType::Hann, fft_size);
let mut planner = FftPlanner::new();
let fft_fwd = planner.plan_fft_forward(fft_size);
let fft_inv = planner.plan_fft_inverse(fft_size);
let norm = 1.0 / fft_size as f32;
let mut spectrogram: Vec<Vec<Complex<f32>>> = Vec::with_capacity(num_frames);
let mut magnitudes: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
let mut fft_buf = vec![COMPLEX_ZERO; fft_size];
for frame_idx in 0..num_frames {
let pos = frame_idx * hop_size;
let frame_end = (pos + fft_size).min(input.len());
let frame_len = frame_end - pos;
for i in 0..fft_size {
fft_buf[i] = if i < frame_len {
Complex::new(input[pos + i] * window[i], 0.0)
} else {
COMPLEX_ZERO
};
}
fft_fwd.process(&mut fft_buf);
let mags: Vec<f32> = fft_buf[..num_bins].iter().map(|c| c.norm()).collect();
magnitudes.push(mags);
spectrogram.push(fft_buf[..num_bins].to_vec());
}
let harmonic_mags = median_filter_horizontal(&magnitudes, params.harmonic_width);
let percussive_mags = median_filter_vertical(&magnitudes, params.percussive_width);
let eps = 1e-10f32;
let output_len = input.len();
let mut harmonic_out = vec![0.0f32; output_len];
let mut percussive_out = vec![0.0f32; output_len];
let mut window_sum = vec![0.0f32; output_len];
let mut h_buf = vec![COMPLEX_ZERO; fft_size];
let mut p_buf = vec![COMPLEX_ZERO; fft_size];
let mut h_mask_buf = vec![0.0f32; num_bins];
let mut h_mask_smooth = vec![0.0f32; num_bins];
const MASK_SMOOTH_RADIUS_LIGHT: usize = 1; const MASK_SMOOTH_RADIUS: usize = 2; const MASK_SMOOTH_RADIUS_HEAVY: usize = 3; const PERC_FRACTION_LIGHT: f32 = 0.15;
const PERC_FRACTION_THRESHOLD: f32 = 0.3;
const PERC_FRACTION_HEAVY: f32 = 0.55;
for frame_idx in 0..num_frames {
let pos = frame_idx * hop_size;
let frame_end = (pos + fft_size).min(output_len);
let frame_len = frame_end - pos;
let mut h_energy = 0.0f32;
let mut p_energy = 0.0f32;
for bin in 0..num_bins {
let h = harmonic_mags[frame_idx][bin];
let p = percussive_mags[frame_idx][bin];
let h2 = h * h;
let p2 = p * p;
h_energy += h2;
p_energy += p2;
let denom = h2 + p2 + eps;
h_mask_buf[bin] = h2 / denom;
}
let perc_fraction = p_energy / (h_energy + p_energy + eps);
let smooth_radius = if perc_fraction > PERC_FRACTION_HEAVY {
MASK_SMOOTH_RADIUS_HEAVY
} else if perc_fraction > PERC_FRACTION_THRESHOLD {
MASK_SMOOTH_RADIUS
} else if perc_fraction > PERC_FRACTION_LIGHT {
MASK_SMOOTH_RADIUS_LIGHT
} else {
0
};
let use_smooth = smooth_radius > 0;
if use_smooth {
for (bin, smoothed) in h_mask_smooth.iter_mut().enumerate().take(num_bins) {
let start = bin.saturating_sub(smooth_radius);
let end = (bin + smooth_radius + 1).min(num_bins);
let sum: f32 = h_mask_buf[start..end].iter().sum();
*smoothed = sum / (end - start) as f32;
}
}
for bin in 0..num_bins {
let h_mask = if use_smooth {
h_mask_smooth[bin]
} else {
h_mask_buf[bin]
};
let p_mask = 1.0 - h_mask;
h_buf[bin] = spectrogram[frame_idx][bin] * h_mask;
p_buf[bin] = spectrogram[frame_idx][bin] * p_mask;
if bin > 0 && bin < num_bins - 1 {
h_buf[fft_size - bin] = h_buf[bin].conj();
p_buf[fft_size - bin] = p_buf[bin].conj();
}
}
for i in num_bins..(fft_size - num_bins + 1) {
h_buf[i] = COMPLEX_ZERO;
p_buf[i] = COMPLEX_ZERO;
}
fft_inv.process(&mut h_buf);
fft_inv.process(&mut p_buf);
for i in 0..frame_len {
let out_idx = pos + i;
harmonic_out[out_idx] += h_buf[i].re * norm * window[i];
percussive_out[out_idx] += p_buf[i].re * norm * window[i];
window_sum[out_idx] += window[i] * window[i];
}
}
let max_ws = window_sum.iter().copied().fold(0.0f32, f32::max);
let min_ws = (max_ws * 0.01).max(1e-8);
for i in 0..output_len {
let ws = window_sum[i].max(min_ws);
harmonic_out[i] /= ws;
percussive_out[i] /= ws;
}
(harmonic_out, percussive_out)
}
fn median_filter_horizontal(mags: &[Vec<f32>], width: usize) -> Vec<Vec<f32>> {
let num_frames = mags.len();
if num_frames == 0 {
return vec![];
}
let num_bins = mags[0].len();
let half = width / 2;
let mut result: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
let mut scratch = Vec::with_capacity(width);
for (frame_idx, _) in mags.iter().enumerate().take(num_frames) {
let start = frame_idx.saturating_sub(half);
let end = (frame_idx + half + 1).min(num_frames);
let mut row = Vec::with_capacity(num_bins);
for bin in 0..num_bins {
scratch.clear();
for frame_mags in &mags[start..end] {
scratch.push(frame_mags[bin]);
}
scratch.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
row.push(scratch[scratch.len() / 2]);
}
result.push(row);
}
result
}
fn median_filter_vertical(mags: &[Vec<f32>], width: usize) -> Vec<Vec<f32>> {
let num_frames = mags.len();
if num_frames == 0 {
return vec![];
}
let num_bins = mags[0].len();
let half = width / 2;
let mut result: Vec<Vec<f32>> = Vec::with_capacity(num_frames);
let mut scratch = Vec::with_capacity(width);
for frame_mags in mags.iter().take(num_frames) {
let mut row = Vec::with_capacity(num_bins);
for bin in 0..num_bins {
let start = bin.saturating_sub(half);
let end = (bin + half + 1).min(num_bins);
scratch.clear();
scratch.extend_from_slice(&frame_mags[start..end]);
scratch.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
row.push(scratch[scratch.len() / 2]);
}
result.push(row);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hpss_short_input() {
let input = vec![0.5f32; 100];
let (h, p) = hpss(&input, 4096, 1024, &HpssParams::default());
assert_eq!(h.len(), 100);
assert_eq!(p.len(), 100);
assert_eq!(h, input);
for &v in &p {
assert!(v.abs() < 1e-6);
}
}
#[test]
fn test_hpss_silence() {
let input = vec![0.0f32; 8192];
let (h, p) = hpss(&input, 4096, 1024, &HpssParams::default());
assert_eq!(h.len(), input.len());
assert_eq!(p.len(), input.len());
for &v in &h {
assert!(v.abs() < 1e-6);
}
for &v in &p {
assert!(v.abs() < 1e-6);
}
}
#[test]
fn test_hpss_sum_preserves_energy() {
let sample_rate = 44100;
let num_samples = sample_rate;
let input: Vec<f32> = (0..num_samples)
.map(|i| {
(2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin() * 0.5
})
.collect();
let (h, p) = hpss(&input, 4096, 1024, &HpssParams::default());
let sum_rms: f32 = input
.iter()
.zip(h.iter().zip(p.iter()))
.map(|(&inp, (&hi, &pi))| {
let diff = inp - hi - pi;
diff * diff
})
.sum::<f32>()
/ input.len() as f32;
let sum_rms = sum_rms.sqrt();
let input_rms: f32 = (input.iter().map(|x| x * x).sum::<f32>() / input.len() as f32).sqrt();
assert!(
sum_rms < input_rms * 0.15,
"HPSS reconstruction error too large: {:.6} vs input RMS {:.6}",
sum_rms,
input_rms
);
}
#[test]
fn test_hpss_tone_is_mostly_harmonic() {
let sample_rate = 44100;
let num_samples = sample_rate * 2;
let input: Vec<f32> = (0..num_samples)
.map(|i| {
(2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin() * 0.8
})
.collect();
let (h, p) = hpss(&input, 4096, 1024, &HpssParams::default());
let h_energy: f32 = h.iter().map(|x| x * x).sum();
let p_energy: f32 = p.iter().map(|x| x * x).sum();
assert!(
h_energy > p_energy,
"Harmonic energy ({:.2}) should exceed percussive ({:.2}) for a tone",
h_energy,
p_energy
);
}
#[test]
fn test_median_filter_horizontal_identity() {
let mags = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
let result = median_filter_horizontal(&mags, 1);
assert_eq!(result, mags);
}
#[test]
fn test_median_filter_vertical_identity() {
let mags = vec![vec![1.0, 2.0, 3.0]];
let result = median_filter_vertical(&mags, 1);
assert_eq!(result, mags);
}
}