use super::ComplexOnsetConfig;
use ndarray::{Array2, ArrayView2, Axis, s};
use non_empty_slice::NonEmptyVec;
use num_complex::Complex;
#[cfg(feature = "simd")]
use wide::f64x4;
#[inline]
#[must_use]
pub fn magnitude_difference(mag: ArrayView2<f64>) -> Array2<f64> {
let (bins, frames) = mag.dim();
let mut out = Array2::zeros((bins, frames));
let mag = mag.as_standard_layout();
for (mut out_row, mag_row) in out.axis_iter_mut(Axis(0)).zip(mag.axis_iter(Axis(0))) {
let curr = mag_row.slice(s![1..]);
let prev = mag_row.slice(s![..frames - 1]);
let mut out_slice = out_row.slice_mut(s![1..]);
for ((o, &c), &p) in out_slice.iter_mut().zip(curr).zip(prev) {
*o = c - p;
}
}
out
}
#[inline]
#[allow(dead_code)] fn wrapped_phase_diff(a: f64, b: f64) -> f64 {
let mut d = a - b;
if d > std::f64::consts::PI {
d -= std::f64::consts::TAU;
} else if d < -std::f64::consts::PI {
d += std::f64::consts::TAU;
}
d
}
#[inline]
fn princarg(angle: f64) -> f64 {
use std::f64::consts::{PI, TAU};
let wrapped = (angle + PI).rem_euclid(TAU) - PI;
if wrapped <= -PI {
wrapped + TAU
} else {
wrapped
}
}
#[inline]
#[must_use]
pub fn phase_deviation(
spec: ArrayView2<Complex<f64>>,
config: &ComplexOnsetConfig,
sample_rate: f64,
) -> Array2<f64> {
let (bins, frames) = spec.dim();
let mut out = Array2::zeros((bins, frames));
let hop = config.hop_size.get() as f64;
let tau = std::f64::consts::TAU;
for (b, (mut out_row, spec_row)) in out
.axis_iter_mut(Axis(0))
.zip(spec.axis_iter(Axis(0)))
.enumerate()
{
let f = config.cqt_config.bin_frequency(b);
let expected = tau * f * hop / sample_rate;
let mut prev_phase = spec_row[0].arg();
for t in 1..frames {
let phase = spec_row[t].arg();
let diff = phase - prev_phase;
out_row[t] = princarg(diff - expected).abs();
prev_phase = phase;
}
}
out
}
#[inline]
#[must_use]
pub fn combine_complex_odf(
mag_diff: &Array2<f64>,
phase_dev: &Array2<f64>,
config: &ComplexOnsetConfig,
) -> NonEmptyVec<f64> {
let (bins, frames) = mag_diff.dim();
let mut odf = vec![0.0; frames];
let mag_rect = config.magnitude_rectify;
let phase_rect = config.phase_rectify;
let mag_diff = mag_diff.as_standard_layout();
let phase_dev = phase_dev.as_standard_layout();
for (t, odf_val) in odf.iter_mut().enumerate().take(frames) {
let mag_col = mag_diff.index_axis(Axis(1), t);
let phase_col = phase_dev.index_axis(Axis(1), t);
let mut b = 0;
#[cfg(feature = "simd")]
let mut mag_acc = f64x4::ZERO;
#[cfg(feature = "simd")]
let mut phase_acc = f64x4::ZERO;
#[cfg(not(feature = "simd"))]
let mut mag_acc = 0.0_f64;
#[cfg(not(feature = "simd"))]
let mut phase_acc = 0.0_f64;
if mag_rect && phase_rect {
while b + 4 <= bins {
let m_view = mag_col.slice(s![b..b + 4]);
let p_view = phase_col.slice(s![b..b + 4]);
let m_slice = m_view.as_slice().unwrap_or_else(|| unreachable!("We made sure mag_diff array is contiguous, therefore any 1d slice from it should also be contiguous"));
let p_slice = p_view.as_slice().unwrap_or_else(|| unreachable!("We made sure phase_dev array is contiguous, therefore any 1d slice from it should also be contiguous"));
#[cfg(feature = "simd")]
{
let m = f64x4::new([m_slice[0], m_slice[1], m_slice[2], m_slice[3]])
.max(f64x4::ZERO);
let p = f64x4::new([p_slice[0], p_slice[1], p_slice[2], p_slice[3]])
.max(f64x4::ZERO);
mag_acc += m;
phase_acc += p;
}
#[cfg(not(feature = "simd"))]
{
for i in 0..4 {
mag_acc += m_slice[i].max(0.0);
phase_acc += p_slice[i].max(0.0);
}
}
b += 4;
}
} else {
while b + 4 <= bins {
let m_view = mag_col.slice(s![b..b + 4]);
let p_view = phase_col.slice(s![b..b + 4]);
let m_slice = m_view.as_slice().unwrap_or_else(|| unreachable!("We made sure mag_diff array is contiguous, therefore any 1d slice from it should also be contiguous"));
let p_slice = p_view.as_slice().unwrap_or_else(|| unreachable!("We made sure phase_dev array is contiguous, therefore any 1d slice from it should also be contiguous"));
#[cfg(feature = "simd")]
{
let m = f64x4::new([m_slice[0], m_slice[1], m_slice[2], m_slice[3]]).abs();
let p = f64x4::new([p_slice[0], p_slice[1], p_slice[2], p_slice[3]]).abs();
mag_acc += m;
phase_acc += p;
}
#[cfg(not(feature = "simd"))]
{
for i in 0..4 {
mag_acc += m_slice[i].abs();
phase_acc += p_slice[i].abs();
}
}
b += 4;
}
}
#[cfg(feature = "simd")]
let mut mag_sum = mag_acc.reduce_add();
#[cfg(feature = "simd")]
let mut phase_sum = phase_acc.reduce_add();
#[cfg(not(feature = "simd"))]
let mut mag_sum = mag_acc;
#[cfg(not(feature = "simd"))]
let mut phase_sum = phase_acc;
for i in b..bins {
mag_sum += if mag_rect {
mag_col[i].max(0.0)
} else {
mag_col[i].abs()
};
phase_sum += if phase_rect {
phase_col[i].max(0.0)
} else {
phase_col[i].abs()
};
}
let mut v = config
.magnitude_weight
.mul_add(mag_sum, config.phase_weight * phase_sum);
if config.log_compression > 0.0 {
v = config.log_compression.mul_add(v, 1.0).ln();
}
*odf_val = v;
}
unsafe { NonEmptyVec::new_unchecked(odf) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bug1_nonrectified_magnitude_comes_from_magnitude() {
let mut config = ComplexOnsetConfig::speech();
assert!(!config.phase_rectify, "test relies on non-rectified phase");
config.magnitude_weight = 1.0;
config.phase_weight = 0.0;
config.log_compression = 0.0;
config.magnitude_rectify = false;
let bins = 8;
let mag_val = 2.0;
let phase_val = 100.0; let mag_diff = Array2::from_elem((bins, 1), mag_val);
let phase_dev = Array2::from_elem((bins, 1), phase_val);
let odf = combine_complex_odf(&mag_diff, &phase_dev, &config);
let expected = bins as f64 * mag_val;
assert!(
(odf[0] - expected).abs() < 1e-9,
"expected magnitude-derived sum {expected}, got {} (would be {} if read from phase)",
odf[0],
bins as f64 * phase_val
);
}
#[test]
fn bug2_phase_deviation_zero_for_expected_advance() {
let config = ComplexOnsetConfig::speech();
let sample_rate = 44100.0;
let hop = config.hop_size.get() as f64;
let tau = std::f64::consts::TAU;
let bins = 64usize;
let frames = 6usize;
let mut high_bin = 0usize;
for b in 0..bins {
let f = config.cqt_config.bin_frequency(b);
let expected = tau * f * hop / sample_rate;
if expected > std::f64::consts::PI {
high_bin = b;
break;
}
}
assert!(high_bin > 0, "expected to find a bin with advance > π");
let mut spec = Array2::from_elem((bins, frames), Complex::new(0.0, 0.0));
for b in 0..bins {
let f = config.cqt_config.bin_frequency(b);
let expected = tau * f * hop / sample_rate;
for t in 0..frames {
let ph = expected * t as f64;
spec[[b, t]] = Complex::from_polar(1.0, ph);
}
}
let dev = phase_deviation(spec.view(), &config, sample_rate);
for t in 1..frames {
assert!(
dev[[high_bin, t]].abs() < 1e-9,
"high bin {high_bin} deviation at frame {t} should be ~0, got {}",
dev[[high_bin, t]]
);
}
}
}