use crate::error::AnalysisError;
use rustfft::FftPlanner;
use rustfft::num_complex::Complex;
use std::cmp::Ordering;
const EPSILON: f32 = 1e-10;
const A4_FREQ: f32 = 440.0;
const SEMITONE_OFFSET: f32 = 57.0;
const DEFAULT_CHROMA_FMIN_HZ: f32 = 100.0;
const DEFAULT_CHROMA_FMAX_HZ: f32 = 5000.0;
pub fn estimate_tuning_offset_semitones_from_spectrogram(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
fmin_hz: f32,
fmax_hz: f32,
frame_step: usize,
peak_rel_threshold: f32,
) -> Result<f32, AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok(0.0);
}
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins,
i,
f.len()
)));
}
}
if sample_rate == 0 || fft_size == 0 {
return Ok(0.0);
}
let freq_resolution = sample_rate as f32 / fft_size as f32;
let fmin = fmin_hz.max(20.0);
let fmax = fmax_hz.max(fmin + 1.0).min(sample_rate as f32 / 2.0);
let step = frame_step.max(1);
let thr = peak_rel_threshold.clamp(0.0, 1.0);
let mut sum_sin = 0.0f32;
let mut sum_cos = 0.0f32;
let mut sum_w = 0.0f32;
for (t, frame) in magnitude_spec_frames.iter().enumerate().step_by(step) {
let mut peak = 0.0f32;
for (bin_idx, &mag) in frame.iter().enumerate() {
let freq = bin_idx as f32 * freq_resolution;
if freq < fmin {
continue;
}
if freq > fmax {
break;
}
peak = peak.max(mag);
}
if peak <= 1e-12 {
continue;
}
let abs_thr = peak * thr;
for (bin_idx, &mag) in frame.iter().enumerate() {
if mag < abs_thr {
continue;
}
let freq = bin_idx as f32 * freq_resolution;
if freq < fmin {
continue;
}
if freq > fmax {
break;
}
let semitone = 12.0 * (freq / A4_FREQ).log2() + SEMITONE_OFFSET;
let residual = semitone - semitone.round();
let w = mag.max(0.0).powf(0.5);
if w <= 0.0 {
continue;
}
let angle = 2.0 * std::f32::consts::PI * residual;
sum_sin += w * angle.sin();
sum_cos += w * angle.cos();
sum_w += w;
}
let _ = t;
}
if sum_w <= 1e-6 {
return Ok(0.0);
}
let r = (sum_sin * sum_sin + sum_cos * sum_cos).sqrt() / sum_w;
if r < 0.05 {
return Ok(0.0);
}
let mean_angle = sum_sin.atan2(sum_cos);
let delta = mean_angle / (2.0 * std::f32::consts::PI); Ok(delta)
}
pub fn extract_chroma(
samples: &[f32],
sample_rate: u32,
frame_size: usize,
hop_size: usize,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
extract_chroma_with_options(samples, sample_rate, frame_size, hop_size, true, 0.5)
}
pub fn extract_chroma_with_options(
samples: &[f32],
sample_rate: u32,
frame_size: usize,
hop_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
log::debug!("Extracting chroma: {} samples at {} Hz, frame_size={}, hop_size={}, soft_mapping={}",
samples.len(), sample_rate, frame_size, hop_size, soft_mapping);
if samples.is_empty() {
return Err(AnalysisError::InvalidInput("Empty audio samples".to_string()));
}
if frame_size == 0 {
return Err(AnalysisError::InvalidInput("Frame size must be > 0".to_string()));
}
if hop_size == 0 {
return Err(AnalysisError::InvalidInput("Hop size must be > 0".to_string()));
}
if sample_rate == 0 {
return Err(AnalysisError::InvalidInput("Sample rate must be > 0".to_string()));
}
let stft_magnitudes = compute_stft(samples, frame_size, hop_size)?;
if stft_magnitudes.is_empty() {
return Ok(vec![]);
}
let mut chroma_vectors = Vec::with_capacity(stft_magnitudes.len());
for frame in &stft_magnitudes {
let chroma = frame_to_chroma(frame, sample_rate, frame_size, soft_mapping, soft_mapping_sigma)?;
chroma_vectors.push(chroma);
}
log::debug!("Extracted {} chroma vectors", chroma_vectors.len());
Ok(chroma_vectors)
}
pub fn compute_stft(
samples: &[f32],
frame_size: usize,
hop_size: usize,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
let n_samples = samples.len();
if n_samples < frame_size {
return Ok(vec![]);
}
let n_frames = (n_samples - frame_size) / hop_size + 1;
let mut magnitudes = Vec::with_capacity(n_frames);
let window: Vec<f32> = (0..frame_size)
.map(|i| {
let x = 2.0 * std::f32::consts::PI * i as f32 / (frame_size - 1) as f32;
0.5 * (1.0 - x.cos())
})
.collect();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(frame_size);
for frame_idx in 0..n_frames {
let start = frame_idx * hop_size;
let end = start + frame_size;
if end > n_samples {
break;
}
let mut fft_input: Vec<Complex<f32>> = samples[start..end]
.iter()
.zip(window.iter())
.map(|(&s, &w)| Complex::new(s * w, 0.0))
.collect();
fft.process(&mut fft_input);
let n_bins = frame_size / 2 + 1;
let magnitude: Vec<f32> = fft_input[..n_bins]
.iter()
.map(|x| (x.re * x.re + x.im * x.im).sqrt())
.collect();
magnitudes.push(magnitude);
}
Ok(magnitudes)
}
fn frame_to_chroma(
magnitude_frame: &[f32],
sample_rate: u32,
fft_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
) -> Result<Vec<f32>, AnalysisError> {
frame_to_chroma_tuned(
magnitude_frame,
sample_rate,
fft_size,
soft_mapping,
soft_mapping_sigma,
0.0,
)
}
fn frame_to_chroma_tuned(
magnitude_frame: &[f32],
sample_rate: u32,
fft_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
) -> Result<Vec<f32>, AnalysisError> {
let mut chroma = vec![0.0f32; 12];
let freq_resolution = sample_rate as f32 / fft_size as f32;
for (bin_idx, &magnitude) in magnitude_frame.iter().enumerate() {
let freq = bin_idx as f32 * freq_resolution;
if freq < DEFAULT_CHROMA_FMIN_HZ {
continue;
}
if freq > DEFAULT_CHROMA_FMAX_HZ.min(sample_rate as f32 / 2.0) {
break;
}
if freq >= sample_rate as f32 / 2.0 {
break;
}
let semitone = 12.0 * (freq / A4_FREQ).log2() + SEMITONE_OFFSET - tuning_offset_semitones;
let magnitude = magnitude.max(0.0).powf(0.6);
let contrib = magnitude;
if soft_mapping {
let semitone_pc = semitone.rem_euclid(12.0); let primary_pc = semitone_pc.round().rem_euclid(12.0); let primary_class = primary_pc as i32;
for offset in -1..=1 {
let target_class = (primary_class + offset).rem_euclid(12);
let target_pc = target_class as f32; let mut distance = (semitone_pc - target_pc).abs();
distance = distance.min(12.0 - distance);
let sigma = soft_mapping_sigma.max(1e-6);
let weight = (-distance * distance / (2.0 * sigma * sigma)).exp();
chroma[target_class as usize] += contrib * weight;
}
} else {
let semitone_class = (semitone.round() as i32) % 12;
let semitone_class = if semitone_class < 0 {
semitone_class + 12
} else {
semitone_class
} as usize;
chroma[semitone_class] += contrib;
}
}
let norm: f32 = chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > EPSILON {
for x in &mut chroma {
*x /= norm;
}
}
Ok(chroma)
}
fn frame_to_hpcp_tuned(
magnitude_frame: &[f32],
sample_rate: u32,
fft_size: usize,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
peaks_per_frame: usize,
num_harmonics: usize,
harmonic_decay: f32,
mag_power: f32,
enable_whitening: bool,
whitening_smooth_bins: usize,
) -> Result<Vec<f32>, AnalysisError> {
frame_to_hpcp_tuned_band(
magnitude_frame,
sample_rate,
fft_size,
soft_mapping_sigma,
tuning_offset_semitones,
peaks_per_frame,
num_harmonics,
harmonic_decay,
mag_power,
enable_whitening,
whitening_smooth_bins,
DEFAULT_CHROMA_FMIN_HZ,
DEFAULT_CHROMA_FMAX_HZ,
)
}
fn frame_to_hpcp_tuned_band(
magnitude_frame: &[f32],
sample_rate: u32,
fft_size: usize,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
peaks_per_frame: usize,
num_harmonics: usize,
harmonic_decay: f32,
mag_power: f32,
enable_whitening: bool,
whitening_smooth_bins: usize,
fmin_hz: f32,
fmax_hz: f32,
) -> Result<Vec<f32>, AnalysisError> {
let mut pc = vec![0.0f32; 12];
if magnitude_frame.is_empty() || sample_rate == 0 || fft_size == 0 {
return Ok(pc);
}
let freq_resolution = sample_rate as f32 / fft_size as f32;
let fmin = fmin_hz.max(20.0);
let fmax = fmax_hz.min(sample_rate as f32 / 2.0);
if fmax <= fmin {
return Ok(pc);
}
let mut whitened: Vec<f32> = Vec::new();
let use_whitening = enable_whitening && whitening_smooth_bins >= 3;
if use_whitening {
let win = whitening_smooth_bins.max(3) | 1; let half = win / 2;
let mut prefix = vec![0.0f32; magnitude_frame.len() + 1];
for (i, &x) in magnitude_frame.iter().enumerate() {
prefix[i + 1] = prefix[i] + x.max(0.0);
}
whitened.resize(magnitude_frame.len(), 0.0);
let eps = 1e-12f32;
for i in 0..magnitude_frame.len() {
let l = i.saturating_sub(half);
let r = (i + half).min(magnitude_frame.len().saturating_sub(1));
let denom = (r + 1 - l) as f32;
let mean = (prefix[r + 1] - prefix[l]) / denom.max(1.0);
let v = magnitude_frame[i].max(0.0) / (mean + eps);
whitened[i] = v.min(20.0);
}
}
let mut peaks: Vec<(usize, f32)> = Vec::new();
for bin in 1..magnitude_frame.len().saturating_sub(1) {
let freq = bin as f32 * freq_resolution;
if freq < fmin {
continue;
}
if freq > fmax {
break;
}
let m = if use_whitening { whitened[bin] } else { magnitude_frame[bin] };
let m_prev = if use_whitening { whitened[bin - 1] } else { magnitude_frame[bin - 1] };
let m_next = if use_whitening { whitened[bin + 1] } else { magnitude_frame[bin + 1] };
if m <= m_prev || m < m_next {
continue;
}
peaks.push((bin, m));
}
if peaks.is_empty() {
return Ok(pc);
}
let k = peaks_per_frame.max(1).min(peaks.len());
peaks.select_nth_unstable_by(k - 1, |a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
peaks.truncate(k);
let sigma = soft_mapping_sigma.max(1e-6);
let hmax = num_harmonics.max(1);
let decay = harmonic_decay.clamp(0.0, 1.0);
let p = mag_power.clamp(0.05, 1.0);
for (bin, _score) in peaks {
let f0 = bin as f32 * freq_resolution;
if f0 <= 0.0 {
continue;
}
let w0 = magnitude_frame[bin].max(0.0).powf(p);
if w0 <= 0.0 {
continue;
}
for h in 1..=hmax {
let fh = f0 * (h as f32);
if fh > fmax {
break;
}
if fh < fmin {
continue;
}
let semitone = 12.0 * (fh / A4_FREQ).log2() + SEMITONE_OFFSET - tuning_offset_semitones;
let semitone_pc = semitone.rem_euclid(12.0);
let primary_pc = semitone_pc.round().rem_euclid(12.0);
let primary_class = primary_pc as i32;
let hw = (decay.powi((h as i32) - 1)) / (h as f32);
let contrib = w0 * hw;
for offset in -1..=1 {
let target_class = (primary_class + offset).rem_euclid(12);
let target_pc = target_class as f32;
let mut distance = (semitone_pc - target_pc).abs();
distance = distance.min(12.0 - distance);
let weight = (-distance * distance / (2.0 * sigma * sigma)).exp();
pc[target_class as usize] += contrib * weight;
}
}
}
let norm: f32 = pc.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > EPSILON {
for x in pc.iter_mut() {
*x /= norm;
}
}
Ok(pc)
}
pub fn convert_linear_to_log_frequency_spectrogram(
linear_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
fmin_hz: f32,
fmax_hz: f32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if linear_spec_frames.is_empty() {
return Ok(vec![]);
}
let n_frames = linear_spec_frames.len();
let n_linear_bins = linear_spec_frames[0].len();
for (i, frame) in linear_spec_frames.iter().enumerate() {
if frame.len() != n_linear_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_linear_bins, i, frame.len()
)));
}
}
if sample_rate == 0 || fft_size == 0 {
return Err(AnalysisError::InvalidInput(
"Sample rate and FFT size must be > 0".to_string()
));
}
let freq_resolution = sample_rate as f32 / fft_size as f32;
let nyquist = sample_rate as f32 / 2.0;
let fmin = fmin_hz.max(20.0);
let fmax = fmax_hz.min(nyquist - 1.0);
let semitone_min = 12.0 * (fmin / A4_FREQ).log2() + SEMITONE_OFFSET;
let semitone_max = 12.0 * (fmax / A4_FREQ).log2() + SEMITONE_OFFSET;
let semitone_bin_min = semitone_min.floor() as i32;
let semitone_bin_max = semitone_max.ceil() as i32;
let n_semitone_bins = (semitone_bin_max - semitone_bin_min + 1) as usize;
if n_semitone_bins == 0 {
return Err(AnalysisError::InvalidInput(
"Invalid semitone range: fmin >= fmax".to_string()
));
}
let _semitone_freqs: Vec<(f32, f32)> = (0..n_semitone_bins)
.map(|bin_idx| {
let semitone = (semitone_bin_min + bin_idx as i32) as f32;
let freq_low = A4_FREQ * 2.0_f32.powf((semitone - 0.5 - SEMITONE_OFFSET) / 12.0);
let freq_high = A4_FREQ * 2.0_f32.powf((semitone + 0.5 - SEMITONE_OFFSET) / 12.0);
(freq_low.max(fmin), freq_high.min(fmax))
})
.collect();
let mut log_freq_spec = Vec::with_capacity(n_frames);
for frame_idx in 0..n_frames {
let linear_frame = &linear_spec_frames[frame_idx];
let mut log_frame = vec![0.0f32; n_semitone_bins];
for (linear_bin, &magnitude) in linear_frame.iter().enumerate() {
if magnitude <= 0.0 {
continue;
}
let freq = linear_bin as f32 * freq_resolution;
if freq < fmin || freq >= fmax || freq >= nyquist {
continue;
}
let semitone = 12.0 * (freq / A4_FREQ).log2() + SEMITONE_OFFSET;
let semitone_float = semitone - semitone_bin_min as f32;
let bin_idx_float = semitone_float;
let bin_idx_low = bin_idx_float.floor() as usize;
let bin_idx_high = (bin_idx_float.ceil() as usize).min(n_semitone_bins - 1);
if bin_idx_low < n_semitone_bins {
let weight_high = bin_idx_float - bin_idx_low as f32;
let weight_low = 1.0 - weight_high;
log_frame[bin_idx_low] += magnitude * weight_low;
if bin_idx_high != bin_idx_low && bin_idx_high < n_semitone_bins {
log_frame[bin_idx_high] += magnitude * weight_high;
}
}
}
log_freq_spec.push(log_frame);
}
Ok(log_freq_spec)
}
pub fn extract_beat_synchronous_chroma(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
hop_size: usize,
beat_times: &[f32],
soft_mapping: bool,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
) -> Result<(Vec<Vec<f32>>, Vec<f32>), AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
if beat_times.is_empty() {
return Err(AnalysisError::InvalidInput(
"Beat-synchronous chroma requires at least one beat time".to_string()
));
}
let frame_duration = hop_size as f32 / sample_rate as f32;
let frame_times: Vec<f32> = (0..magnitude_spec_frames.len())
.map(|i| i as f32 * frame_duration)
.collect();
let mut beat_chroma_vectors = Vec::with_capacity(beat_times.len().saturating_sub(1));
let mut beat_energies = Vec::with_capacity(beat_times.len().saturating_sub(1));
for i in 0..beat_times.len().saturating_sub(1) {
let beat_start = beat_times[i];
let beat_end = beat_times[i + 1];
let mut interval_chroma = Vec::new();
let mut interval_energy = 0.0f32;
for (frame_idx, &frame_time) in frame_times.iter().enumerate() {
if frame_time >= beat_start && frame_time < beat_end {
let chroma = frame_to_chroma_tuned(
&magnitude_spec_frames[frame_idx],
sample_rate,
fft_size,
soft_mapping,
soft_mapping_sigma,
tuning_offset_semitones,
)?;
let energy: f32 = magnitude_spec_frames[frame_idx]
.iter()
.map(|&x| x * x)
.sum();
interval_chroma.push(chroma);
interval_energy += energy;
}
}
if !interval_chroma.is_empty() {
let mut avg_chroma = vec![0.0f32; 12];
for chroma in &interval_chroma {
for (j, &val) in chroma.iter().enumerate() {
avg_chroma[j] += val;
}
}
let n = interval_chroma.len() as f32;
for val in &mut avg_chroma {
*val /= n;
}
let norm: f32 = avg_chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > EPSILON {
for val in &mut avg_chroma {
*val /= norm;
}
}
beat_chroma_vectors.push(avg_chroma);
beat_energies.push(interval_energy);
} else {
beat_chroma_vectors.push(vec![0.0f32; 12]);
beat_energies.push(0.0);
}
}
Ok((beat_chroma_vectors, beat_energies))
}
pub fn extract_chroma_from_log_frequency_spectrogram(
log_freq_spec_frames: &[Vec<f32>],
semitone_offset: i32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if log_freq_spec_frames.is_empty() {
return Ok(Vec::new());
}
let mut chroma_vectors = Vec::with_capacity(log_freq_spec_frames.len());
for frame in log_freq_spec_frames {
let mut chroma = vec![0.0f32; 12];
for (bin_idx, &magnitude) in frame.iter().enumerate() {
if magnitude <= 0.0 {
continue;
}
let semitone = semitone_offset + bin_idx as i32;
let pitch_class = semitone.rem_euclid(12);
let pc_idx = if pitch_class < 0 {
pitch_class + 12
} else {
pitch_class
} as usize;
chroma[pc_idx] += magnitude;
}
let norm: f32 = chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > EPSILON {
for x in &mut chroma {
*x /= norm;
}
}
chroma_vectors.push(chroma);
}
Ok(chroma_vectors)
}
pub fn extract_chroma_from_spectrogram_with_options(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok(Vec::new());
}
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins,
i,
f.len()
)));
}
}
let mut chroma_vectors = Vec::with_capacity(magnitude_spec_frames.len());
for frame in magnitude_spec_frames {
chroma_vectors.push(frame_to_chroma(
frame,
sample_rate,
fft_size,
soft_mapping,
soft_mapping_sigma,
)?);
}
Ok(chroma_vectors)
}
pub fn extract_chroma_from_spectrogram_with_options_and_energy(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
) -> Result<(Vec<Vec<f32>>, Vec<f32>), AnalysisError> {
extract_chroma_from_spectrogram_with_options_and_energy_tuned(
magnitude_spec_frames,
sample_rate,
fft_size,
soft_mapping,
soft_mapping_sigma,
0.0,
)
}
pub fn extract_chroma_from_spectrogram_with_options_and_energy_tuned(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
soft_mapping: bool,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
) -> Result<(Vec<Vec<f32>>, Vec<f32>), AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins,
i,
f.len()
)));
}
}
let mut chroma_vectors = Vec::with_capacity(magnitude_spec_frames.len());
let mut energies = Vec::with_capacity(magnitude_spec_frames.len());
for frame in magnitude_spec_frames {
let e: f32 = frame.iter().map(|&x| x * x).sum();
energies.push(e);
chroma_vectors.push(frame_to_chroma_tuned(
frame,
sample_rate,
fft_size,
soft_mapping,
soft_mapping_sigma,
tuning_offset_semitones,
)?);
}
Ok((chroma_vectors, energies))
}
pub fn extract_hpcp_from_spectrogram_with_options_and_energy_tuned(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
peaks_per_frame: usize,
num_harmonics: usize,
harmonic_decay: f32,
mag_power: f32,
enable_whitening: bool,
whitening_smooth_bins: usize,
) -> Result<(Vec<Vec<f32>>, Vec<f32>), AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins, i, f.len()
)));
}
}
let mut pcs = Vec::with_capacity(magnitude_spec_frames.len());
let mut energies = Vec::with_capacity(magnitude_spec_frames.len());
for frame in magnitude_spec_frames {
let e: f32 = frame.iter().map(|&x| x * x).sum();
energies.push(e);
pcs.push(frame_to_hpcp_tuned(
frame,
sample_rate,
fft_size,
soft_mapping_sigma,
tuning_offset_semitones,
peaks_per_frame,
num_harmonics,
harmonic_decay,
mag_power,
enable_whitening,
whitening_smooth_bins,
)?);
}
Ok((pcs, energies))
}
pub fn extract_hpcp_bass_blend_from_spectrogram_with_options_and_energy_tuned(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
soft_mapping_sigma: f32,
tuning_offset_semitones: f32,
peaks_per_frame: usize,
num_harmonics: usize,
harmonic_decay: f32,
mag_power: f32,
enable_whitening: bool,
whitening_smooth_bins: usize,
bass_fmin_hz: f32,
bass_fmax_hz: f32,
bass_weight: f32,
) -> Result<(Vec<Vec<f32>>, Vec<f32>), AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins, i, f.len()
)));
}
}
let w = bass_weight.clamp(0.0, 1.0);
let mut pcs = Vec::with_capacity(magnitude_spec_frames.len());
let mut energies = Vec::with_capacity(magnitude_spec_frames.len());
for frame in magnitude_spec_frames {
let e: f32 = frame.iter().map(|&x| x * x).sum();
energies.push(e);
let full = frame_to_hpcp_tuned(
frame,
sample_rate,
fft_size,
soft_mapping_sigma,
tuning_offset_semitones,
peaks_per_frame,
num_harmonics,
harmonic_decay,
mag_power,
enable_whitening,
whitening_smooth_bins,
)?;
let bass = frame_to_hpcp_tuned_band(
frame,
sample_rate,
fft_size,
soft_mapping_sigma,
tuning_offset_semitones,
peaks_per_frame.min(12).max(1),
num_harmonics,
harmonic_decay,
mag_power,
enable_whitening,
whitening_smooth_bins,
bass_fmin_hz,
bass_fmax_hz,
)?;
let mut blend = vec![0.0f32; 12];
for i in 0..12 {
blend[i] = (1.0 - w) * full[i] + w * bass[i];
}
let norm: f32 = blend.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
for x in blend.iter_mut() {
*x /= norm;
}
}
pcs.push(blend);
}
Ok((pcs, energies))
}
pub fn smooth_spectrogram_time(
magnitude_spec_frames: &[Vec<f32>],
margin: usize,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if magnitude_spec_frames.is_empty() || margin == 0 {
return Ok(magnitude_spec_frames.to_vec());
}
let n_frames = magnitude_spec_frames.len();
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins,
i,
f.len()
)));
}
}
let mut out = vec![vec![0.0f32; n_bins]; n_frames];
for bin in 0..n_bins {
let mut prefix = vec![0.0f32; n_frames + 1];
for t in 0..n_frames {
prefix[t + 1] = prefix[t] + magnitude_spec_frames[t][bin];
}
for t in 0..n_frames {
let start = t.saturating_sub(margin);
let end = (t + margin + 1).min(n_frames);
let sum = prefix[end] - prefix[start];
let denom = (end - start).max(1) as f32;
out[t][bin] = sum / denom;
}
}
Ok(out)
}
pub fn harmonic_spectrogram_time_mask(
magnitude_spec_frames: &[Vec<f32>],
smooth_margin: usize,
mask_power: f32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok(Vec::new());
}
let n_frames = magnitude_spec_frames.len();
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins,
i,
f.len()
)));
}
}
let h_est = smooth_spectrogram_time(magnitude_spec_frames, smooth_margin)?;
let p = mask_power.max(1.0);
let eps = 1e-12f32;
let mut out = vec![vec![0.0f32; n_bins]; n_frames];
for t in 0..n_frames {
for b in 0..n_bins {
let x = magnitude_spec_frames[t][b].max(0.0);
let h = h_est[t][b].max(0.0);
let r = (x - h).max(0.0);
let hp = h.powf(p);
let rp = r.powf(p);
let m = hp / (hp + rp + eps);
out[t][b] = x * m;
}
}
Ok(out)
}
pub fn harmonic_spectrogram_hpss_median_mask(
magnitude_spec_frames: &[Vec<f32>],
sample_rate: u32,
fft_size: usize,
fmin_hz: f32,
fmax_hz: f32,
frame_step: usize,
time_margin: usize,
freq_margin: usize,
mask_power: f32,
) -> Result<Vec<Vec<f32>>, AnalysisError> {
if magnitude_spec_frames.is_empty() {
return Ok(Vec::new());
}
let n_frames = magnitude_spec_frames.len();
let n_bins = magnitude_spec_frames[0].len();
if n_bins == 0 {
return Err(AnalysisError::InvalidInput("Empty spectrogram frames".to_string()));
}
for (i, f) in magnitude_spec_frames.iter().enumerate() {
if f.len() != n_bins {
return Err(AnalysisError::InvalidInput(format!(
"Inconsistent spectrogram frame lengths: frame 0 has {} bins, frame {} has {} bins",
n_bins, i, f.len()
)));
}
}
if sample_rate == 0 || fft_size == 0 {
return Ok(magnitude_spec_frames.to_vec());
}
let freq_resolution = sample_rate as f32 / fft_size as f32;
let fmin = fmin_hz.max(20.0);
let fmax = fmax_hz.max(fmin + 1.0).min(sample_rate as f32 / 2.0);
let mut bin_start = (fmin / freq_resolution).floor() as isize;
let mut bin_end = (fmax / freq_resolution).ceil() as isize;
bin_start = bin_start.clamp(0, n_bins as isize);
bin_end = bin_end.clamp(0, n_bins as isize);
if bin_end <= bin_start {
return Ok(magnitude_spec_frames.to_vec());
}
let bin_start = bin_start as usize;
let bin_end = bin_end as usize;
let band_bins = bin_end - bin_start;
let step = frame_step.max(1);
let ds_indices: Vec<usize> = (0..n_frames).step_by(step).collect();
let n_ds = ds_indices.len().max(1);
let mut band_ds = vec![vec![0.0f32; band_bins]; n_ds];
for (k, &t) in ds_indices.iter().enumerate() {
let src = &magnitude_spec_frames[t][bin_start..bin_end];
band_ds[k].copy_from_slice(src);
}
#[inline]
fn median_in_place(v: &mut [f32]) -> f32 {
if v.is_empty() {
return 0.0;
}
let mid = v.len() / 2;
let (_, m, _) = v.select_nth_unstable_by(mid, |a, b| {
a.partial_cmp(b).unwrap_or(Ordering::Equal)
});
*m
}
let mut h_est = vec![vec![0.0f32; band_bins]; n_ds];
let mut scratch: Vec<f32> = Vec::with_capacity(2 * time_margin + 1);
for b in 0..band_bins {
for t in 0..n_ds {
scratch.clear();
let start = t.saturating_sub(time_margin);
let end = (t + time_margin + 1).min(n_ds);
for tt in start..end {
let x = band_ds[tt][b];
scratch.push(if x.is_finite() { x.max(0.0) } else { 0.0 });
}
h_est[t][b] = median_in_place(&mut scratch);
}
}
let mut p_est = vec![vec![0.0f32; band_bins]; n_ds];
scratch = Vec::with_capacity(2 * freq_margin + 1);
for t in 0..n_ds {
for b in 0..band_bins {
scratch.clear();
let start = b.saturating_sub(freq_margin);
let end = (b + freq_margin + 1).min(band_bins);
for bb in start..end {
let x = band_ds[t][bb];
scratch.push(if x.is_finite() { x.max(0.0) } else { 0.0 });
}
p_est[t][b] = median_in_place(&mut scratch);
}
}
let p = mask_power.max(1.0);
let eps = 1e-12f32;
let mut mask_ds = vec![vec![0.0f32; band_bins]; n_ds];
for t in 0..n_ds {
for b in 0..band_bins {
let h = h_est[t][b].max(0.0);
let per = p_est[t][b].max(0.0);
let hp = h.powf(p);
let pp = per.powf(p);
mask_ds[t][b] = hp / (hp + pp + eps);
}
}
let mut out = vec![vec![0.0f32; n_bins]; n_frames];
for t in 0..n_frames {
let k = (t / step).min(n_ds - 1);
for b in 0..band_bins {
let bin = bin_start + b;
let x = magnitude_spec_frames[t][bin];
let x = if x.is_finite() { x.max(0.0) } else { 0.0 };
out[t][bin] = x * mask_ds[k][b];
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_chroma_empty() {
let result = extract_chroma(&[], 44100, 2048, 512);
assert!(result.is_err());
}
#[test]
fn test_extract_chroma_short() {
let samples = vec![0.0f32; 1000];
let result = extract_chroma(&samples, 44100, 2048, 512);
assert!(result.is_ok());
let chroma_vectors = result.unwrap();
assert_eq!(chroma_vectors.len(), 0);
}
#[test]
fn test_extract_chroma_basic() {
let sample_rate = 44100;
let duration_samples = sample_rate * 2; let mut samples = Vec::with_capacity(duration_samples);
for i in 0..duration_samples {
let t = i as f32 / sample_rate as f32;
samples.push((2.0 * std::f32::consts::PI * 440.0 * t).sin());
}
let result = extract_chroma(&samples, sample_rate as u32, 2048, 512);
assert!(result.is_ok());
let chroma_vectors = result.unwrap();
assert!(!chroma_vectors.is_empty());
for chroma in &chroma_vectors {
assert_eq!(chroma.len(), 12);
let norm: f32 = chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01 || norm < EPSILON);
}
let avg_chroma: Vec<f32> = (0..12)
.map(|i| {
chroma_vectors.iter().map(|v| v[i]).sum::<f32>() / chroma_vectors.len() as f32
})
.collect();
assert!(avg_chroma[9] > 0.1, "A semitone class should be prominent for A4 tone");
}
#[test]
fn test_frame_to_chroma() {
let sample_rate = 44100;
let fft_size = 2048;
let mut magnitude = vec![0.0f32; fft_size / 2 + 1];
let bin_a4 = (440.0 * fft_size as f32 / sample_rate as f32) as usize;
if bin_a4 < magnitude.len() {
magnitude[bin_a4] = 1.0;
}
let chroma = frame_to_chroma(&magnitude, sample_rate, fft_size, false, 0.5).unwrap();
assert_eq!(chroma.len(), 12);
let norm: f32 = chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 0.01 || norm < EPSILON);
}
#[test]
fn test_invalid_parameters() {
let samples = vec![0.0f32; 10000];
assert!(extract_chroma(&samples, 44100, 0, 512).is_err());
assert!(extract_chroma(&samples, 44100, 2048, 0).is_err());
assert!(extract_chroma(&samples, 0, 2048, 512).is_err());
}
#[test]
fn test_soft_chroma_mapping() {
let sample_rate = 44100;
let duration_samples = sample_rate * 2; let mut samples = Vec::with_capacity(duration_samples);
for i in 0..duration_samples {
let t = i as f32 / sample_rate as f32;
samples.push((2.0 * std::f32::consts::PI * 440.0 * t).sin());
}
let result_soft = extract_chroma_with_options(&samples, sample_rate as u32, 2048, 512, true, 0.5);
assert!(result_soft.is_ok());
let result_hard = extract_chroma_with_options(&samples, sample_rate as u32, 2048, 512, false, 0.5);
assert!(result_hard.is_ok());
let chroma_soft = result_soft.unwrap();
let chroma_hard = result_hard.unwrap();
assert_eq!(chroma_soft.len(), chroma_hard.len());
assert!(!chroma_soft.is_empty());
}
}