use crate::util::hann_window;
#[derive(Debug, Clone)]
pub struct CqtSpec {
pub sample_rate: u32,
pub f_min: f32,
pub bins_per_octave: usize,
pub bins: usize,
pub hop: usize,
pub data: Vec<f32>,
}
impl CqtSpec {
pub fn frames(&self) -> usize {
self.data.len().checked_div(self.bins).unwrap_or(0)
}
pub fn get(&self, frame: usize, bin: usize) -> f32 {
self.data[frame * self.bins + bin]
}
pub fn bin_hz(&self, bin: usize) -> f32 {
self.f_min * 2f32.powf(bin as f32 / self.bins_per_octave as f32)
}
}
pub fn cqt(signal: &[f32], sample_rate: u32, bins_per_octave: usize, f_min: f32) -> CqtSpec {
let bpo = bins_per_octave.max(1);
let sr = sample_rate as f32;
let f_min = f_min.max(1.0);
let f_max = sr * 0.5 * 0.98;
let bins = if f_max > f_min {
(bpo as f32 * (f_max / f_min).log2()).floor().max(1.0) as usize
} else {
1
};
let q = 1.0 / (2f32.powf(1.0 / bpo as f32) - 1.0);
struct Kernel {
len: usize,
cos: Vec<f32>,
sin: Vec<f32>,
}
let mut kernels = Vec::with_capacity(bins);
for k in 0..bins {
let f_k = f_min * 2f32.powf(k as f32 / bpo as f32);
let n_k = ((q * sr / f_k).round() as usize).max(4) & !1;
let win = hann_window(n_k);
let win_sum: f32 = win.iter().sum::<f32>().max(1e-9);
let mut cos = vec![0.0f32; n_k];
let mut sin = vec![0.0f32; n_k];
for i in 0..n_k {
let phase = 2.0 * std::f32::consts::PI * f_k * i as f32 / sr;
let scale = 2.0 * win[i] / win_sum;
cos[i] = scale * phase.cos();
sin[i] = scale * phase.sin();
}
kernels.push(Kernel { len: n_k, cos, sin });
}
let n_max = kernels.first().map_or(0, |k| k.len);
let hop = (n_max / 16).max(256);
let half = n_max / 2;
let mut data = Vec::new();
if signal.len() < n_max {
return CqtSpec { sample_rate, f_min, bins_per_octave: bpo, bins, hop, data };
}
let mut center = half;
while center + half <= signal.len() {
for kern in &kernels {
let kh = kern.len / 2;
let start = center - kh;
let mut re = 0.0f32;
let mut im = 0.0f32;
for i in 0..kern.len {
let x = signal[start + i];
re += x * kern.cos[i];
im -= x * kern.sin[i];
}
let mag = (re * re + im * im).sqrt();
data.push((20.0 * mag.max(1e-6).log10()).max(-120.0));
}
center += hop;
}
CqtSpec { sample_rate, f_min, bins_per_octave: bpo, bins, hop, data }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tone_peaks_at_its_bin() {
let sr = 48_000u32;
let f_min = 55.0f32; let bpo = 12;
let sig: Vec<f32> = (0..sr)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sr as f32).sin())
.collect();
let spec = cqt(&sig, sr, bpo, f_min);
assert!(spec.frames() > 0);
let f = spec.frames() / 2;
let mut peak = 0;
for b in 0..spec.bins {
if spec.get(f, b) > spec.get(f, peak) {
peak = b;
}
}
assert!(
(spec.bin_hz(peak) - 440.0).abs() < 15.0,
"peak at {} Hz (bin {peak}), want ~440",
spec.bin_hz(peak)
);
}
#[test]
fn bins_are_log_spaced() {
let spec = CqtSpec {
sample_rate: 48_000,
f_min: 55.0,
bins_per_octave: 12,
bins: 24,
hop: 512,
data: Vec::new(),
};
assert!((spec.bin_hz(12) - 110.0).abs() < 0.01);
assert!((spec.bin_hz(0) - 55.0).abs() < 0.01);
}
}