use std::{marker::PhantomData, num::NonZeroUsize, ops::Deref};
use crate::{SpectrogramError, SpectrogramParams, SpectrogramResult, StftPlan};
use ndarray::{Array1, Array2, Axis, Zip};
use non_empty_slice::{NonEmptySlice, NonEmptyVec};
use num_complex::Complex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ITD;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IPD;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ILD;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ILR;
#[inline]
#[must_use]
pub fn magphase(
complex_spect: &Array2<Complex<f64>>,
power: NonZeroUsize,
) -> (Array2<f64>, Array2<Complex<f64>>) {
let power_usize = power.get();
let shape = complex_spect.raw_dim();
let mut mag = Array2::zeros(shape);
let mut phase = Array2::zeros(shape);
#[inline(always)]
fn pow_mag(mag: f64, mag_sq: f64, power: usize) -> f64 {
match power {
1 => mag,
2 => mag_sq,
3 => mag_sq * mag,
4 => mag_sq * mag_sq,
_ => {
let mut base = mag;
let mut exp = power;
let mut acc = 1.0_f64;
while exp > 0 {
if (exp & 1) == 1 {
acc *= base;
}
exp >>= 1;
if exp > 0 {
base *= base;
}
}
acc
}
}
}
#[cfg(feature = "rayon")]
{
Zip::from(&mut mag)
.and(&mut phase)
.and(complex_spect)
.par_for_each(|m, p, &c| {
let mag_sq = c.re.mul_add(c.re, c.im * c.im);
if mag_sq == 0.0 {
*m = 0.0;
*p = Complex { re: 1.0, im: 0.0 };
} else {
let mag_val = mag_sq.sqrt();
*m = pow_mag(mag_val, mag_sq, power_usize);
let inv_mag = mag_val.recip();
*p = Complex {
re: c.re * inv_mag,
im: c.im * inv_mag,
};
}
});
}
#[cfg(not(feature = "rayon"))]
{
Zip::from(&mut mag)
.and(&mut phase)
.and(complex_spect)
.for_each(|m, p, &c| {
let mag_sq = c.re.mul_add(c.re, c.im * c.im);
if mag_sq == 0.0 {
*m = 0.0;
*p = Complex { re: 1.0, im: 0.0 };
} else {
let mag_val = mag_sq.sqrt();
*m = pow_mag(mag_val, mag_sq, power_usize);
let inv_mag = mag_val.recip();
*p = Complex {
re: c.re * inv_mag,
im: c.im * inv_mag,
};
}
});
}
(mag, phase)
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ItdSpectrogram {
pub data: Array2<f64>,
params: ITDSpectrogramParams,
frequencies: NonEmptyVec<f64>,
times: NonEmptyVec<f64>,
_unit: PhantomData<ITD>,
}
impl AsRef<Array2<f64>> for ItdSpectrogram {
#[inline]
fn as_ref(&self) -> &Array2<f64> {
&self.data
}
}
impl Deref for ItdSpectrogram {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl ItdSpectrogram {
#[inline]
#[must_use]
pub fn n_bins(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.nrows()) }
}
#[inline]
#[must_use]
pub fn n_frames(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.ncols()) }
}
#[inline]
#[must_use]
pub const fn params(&self) -> &ITDSpectrogramParams {
&self.params
}
#[inline]
#[must_use]
pub fn frequencies(&self) -> &NonEmptySlice<f64> {
&self.frequencies
}
#[inline]
#[must_use]
pub fn times(&self) -> &NonEmptySlice<f64> {
&self.times
}
#[inline]
#[must_use]
pub fn frequency_range(&self) -> (f64, f64) {
let freqs = &*self.frequencies;
(freqs[0], freqs[freqs.len().get() - 1])
}
#[inline]
#[must_use]
pub fn duration(&self) -> f64 {
let ts = &*self.times;
ts[ts.len().get() - 1] - ts[0]
}
#[inline]
#[must_use]
pub const fn unit_label() -> &'static str {
"ITD (seconds)"
}
#[must_use]
pub fn histogram(
&self,
num_bins: Option<NonZeroUsize>,
delay_range: Option<(f64, f64)>,
energy_weighted: bool,
normalize: bool,
) -> Array2<f64> {
let num_bins = num_bins.unwrap_or_else(|| crate::nzu!(400)).get();
let (min_delay, max_delay) = delay_range.unwrap_or((-0.00088, 0.00088));
let bin_width = (max_delay - min_delay) / num_bins as f64;
let n_frames = self.n_frames().get();
let n_freq_bins = self.n_bins().get();
let mut histogram = Array2::zeros((num_bins, n_frames));
for frame in 0..n_frames {
for freq_bin in 0..n_freq_bins {
let itd_value = self.data[(freq_bin, frame)];
if !itd_value.is_finite() || itd_value < min_delay || itd_value > max_delay {
continue;
}
let bin_idx = ((itd_value - min_delay) / bin_width).floor() as usize;
let bin_idx = bin_idx.min(num_bins - 1);
let weight = if energy_weighted {
1.0
} else {
1.0
};
histogram[(bin_idx, frame)] += weight;
}
if normalize {
let sum: f64 = histogram.column(frame).sum();
if sum > 0.0 {
for bin in 0..num_bins {
histogram[(bin, frame)] /= sum;
}
}
}
}
histogram
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ITDSpectrogramParams {
pub(crate) spectrogram_params: SpectrogramParams,
pub(crate) start_freq: f64,
pub(crate) end_freq: f64,
pub(crate) magphase_power: NonZeroUsize,
}
impl ITDSpectrogramParams {
pub fn new(
spec_params: SpectrogramParams,
start_freq: f64,
stop_freq: f64,
magphase_power: Option<NonZeroUsize>,
) -> SpectrogramResult<Self> {
let sample_rate = spec_params.sample_rate_hz;
if start_freq <= 0.0 || stop_freq <= 0.0 {
return Err(SpectrogramError::InvalidInput(
"Start and end frequencies must be positive.".into(),
));
}
if start_freq >= stop_freq {
return Err(SpectrogramError::InvalidInput(
"Start frequency must be less than end frequency.".into(),
));
}
if sample_rate <= 0.0 {
return Err(SpectrogramError::InvalidInput(
"Sample rate must be positive.".into(),
));
}
if stop_freq > sample_rate / 2.0 {
return Err(SpectrogramError::InvalidInput(
"End frequency must be less than Nyquist frequency.".into(),
));
}
Ok(Self {
spectrogram_params: spec_params,
start_freq,
end_freq: stop_freq,
magphase_power: magphase_power.unwrap_or_else(|| crate::nzu!(1)),
})
}
}
#[inline]
#[must_use]
pub fn compute_itd_spectrogram(
audio: [&NonEmptySlice<f64>; 2],
params: &ITDSpectrogramParams,
plan: &mut StftPlan, ) -> SpectrogramResult<ItdSpectrogram> {
let window_size = params.spectrogram_params.stft().n_fft();
let bin_width = params.spectrogram_params.sample_rate_hz / window_size.get() as f64;
let start_bin = (params.start_freq / bin_width).round() as usize;
let stop_bin = (params.end_freq / bin_width).round() as usize;
let left = audio[0];
let right = audio[1];
let left_spec = plan.compute(left, ¶ms.spectrogram_params)?;
let right_spec = plan.compute(right, ¶ms.spectrogram_params)?;
let (left_mag, left_phase) = magphase(&left_spec, params.magphase_power);
let (right_mag, right_phase) = magphase(&right_spec, params.magphase_power);
let num_frames = left_mag.shape()[1];
let num_bins = stop_bin - start_bin;
let left_mag_slice = left_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let right_mag_slice = right_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let left_phase_slice = left_phase.slice(ndarray::s![start_bin..stop_bin, ..]);
let right_phase_slice = right_phase.slice(ndarray::s![start_bin..stop_bin, ..]);
let mut itd_spectrogram = Array2::zeros((num_bins, num_frames));
let pi = std::f64::consts::PI;
let two_pi = 2.0 * pi;
#[inline(always)]
fn np_mod(x: f64, m: f64) -> f64 {
((x % m) + m) % m
}
#[cfg(feature = "rayon")]
Zip::indexed(itd_spectrogram.view_mut()).par_for_each(|(bin_idx, frame), o| {
let left_angle = left_phase_slice[(bin_idx, frame)].im.atan2(left_phase_slice[(bin_idx, frame)].re);
let right_angle = right_phase_slice[(bin_idx, frame)].im.atan2(right_phase_slice[(bin_idx, frame)].re);
let intensity_val = left_mag_slice[(bin_idx, frame)] + right_mag_slice[(bin_idx, frame)];
if intensity_val > 0.0 {
let diff = left_angle - right_angle;
let wrapped = np_mod(diff + pi, two_pi) - pi;
let actual_bin = (start_bin + bin_idx) as f64;
*o = wrapped / (two_pi * bin_width * actual_bin);
}
});
#[cfg(not(feature = "rayon"))]
Zip::indexed(itd_spectrogram.view_mut()).for_each(|(bin_idx, frame), o| {
let left_angle = left_phase_slice[(bin_idx, frame)].im.atan2(left_phase_slice[(bin_idx, frame)].re);
let right_angle = right_phase_slice[(bin_idx, frame)].im.atan2(right_phase_slice[(bin_idx, frame)].re);
let intensity_val = left_mag_slice[(bin_idx, frame)] + right_mag_slice[(bin_idx, frame)];
if intensity_val > 0.0 {
let diff = left_angle - right_angle;
let wrapped = np_mod(diff + pi, two_pi) - pi;
let actual_bin = (start_bin + bin_idx) as f64;
*o = wrapped / (two_pi * bin_width * actual_bin);
}
});
let frequencies: Vec<f64> = (start_bin..stop_bin)
.map(|bin| bin as f64 * bin_width)
.collect();
let frequencies = NonEmptyVec::new(frequencies)
.expect("Frequency range should have at least one bin");
let hop_size = params.spectrogram_params.stft().hop_size().get() as f64;
let sample_rate = params.spectrogram_params.sample_rate_hz;
let times: Vec<f64> = (0..num_frames)
.map(|frame| frame as f64 * hop_size / sample_rate)
.collect();
let times = NonEmptyVec::new(times)
.expect("Time axis should have at least one frame");
Ok(ItdSpectrogram {
data: itd_spectrogram,
params: params.clone(),
frequencies,
times,
_unit: PhantomData,
})
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IpdSpectrogram {
pub data: Array2<f64>,
params: IPDSpectrogramParams,
frequencies: NonEmptyVec<f64>,
times: NonEmptyVec<f64>,
_unit: PhantomData<IPD>,
}
impl AsRef<Array2<f64>> for IpdSpectrogram {
#[inline]
fn as_ref(&self) -> &Array2<f64> {
&self.data
}
}
impl Deref for IpdSpectrogram {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl IpdSpectrogram {
#[inline]
#[must_use]
pub fn n_bins(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.nrows()) }
}
#[inline]
#[must_use]
pub fn n_frames(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.ncols()) }
}
#[inline]
#[must_use]
pub const fn params(&self) -> &IPDSpectrogramParams {
&self.params
}
#[inline]
#[must_use]
pub fn frequencies(&self) -> &NonEmptySlice<f64> {
&self.frequencies
}
#[inline]
#[must_use]
pub fn times(&self) -> &NonEmptySlice<f64> {
&self.times
}
#[inline]
#[must_use]
pub fn frequency_range(&self) -> (f64, f64) {
let freqs = &*self.frequencies;
(freqs[0], freqs[freqs.len().get() - 1])
}
#[inline]
#[must_use]
pub fn duration(&self) -> f64 {
let ts = &*self.times;
ts[ts.len().get() - 1] - ts[0]
}
#[inline]
#[must_use]
pub const fn unit_label() -> &'static str {
"IPD (radians)"
}
#[must_use]
pub fn histogram(
&self,
num_bins: Option<NonZeroUsize>,
phase_range: Option<(f64, f64)>,
energy_weighted: bool,
normalize: bool,
) -> Array2<f64> {
let num_bins = num_bins.unwrap_or_else(|| crate::nzu!(400)).get();
let pi = std::f64::consts::PI;
let (min_phase, max_phase) = phase_range.unwrap_or((-pi, pi));
let bin_width = (max_phase - min_phase) / num_bins as f64;
let n_frames = self.n_frames().get();
let n_freq_bins = self.n_bins().get();
let mut histogram = Array2::zeros((num_bins, n_frames));
for frame in 0..n_frames {
for freq_bin in 0..n_freq_bins {
let ipd_value = self.data[(freq_bin, frame)];
if !ipd_value.is_finite() || ipd_value < min_phase || ipd_value > max_phase {
continue;
}
let bin_idx = ((ipd_value - min_phase) / bin_width).floor() as usize;
let bin_idx = bin_idx.min(num_bins - 1);
let weight = if energy_weighted {
1.0
} else {
1.0
};
histogram[(bin_idx, frame)] += weight;
}
if normalize {
let sum: f64 = histogram.column(frame).sum();
if sum > 0.0 {
for bin in 0..num_bins {
histogram[(bin, frame)] /= sum;
}
}
}
}
histogram
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IPDSpectrogramParams {
pub(crate) spectrogram_params: SpectrogramParams,
pub(crate) start_freq: f64,
pub(crate) end_freq: f64,
pub(crate) wrapped: bool,
}
impl IPDSpectrogramParams {
pub fn new(
spec_params: SpectrogramParams,
start_freq: f64,
stop_freq: f64,
wrapped: bool,
) -> SpectrogramResult<Self> {
let sample_rate = spec_params.sample_rate_hz;
if start_freq <= 0.0 || stop_freq <= 0.0 {
return Err(SpectrogramError::InvalidInput(
"Start and end frequencies must be positive.".into(),
));
}
if start_freq >= stop_freq {
return Err(SpectrogramError::InvalidInput(
"Start frequency must be less than end frequency.".into(),
));
}
if stop_freq > sample_rate / 2.0 {
return Err(SpectrogramError::InvalidInput(
"End frequency must be less than Nyquist frequency.".into(),
));
}
Ok(Self {
spectrogram_params: spec_params,
start_freq,
end_freq: stop_freq,
wrapped,
})
}
}
#[inline]
#[must_use]
pub fn compute_ipd_spectrogram(
audio: [&NonEmptySlice<f64>; 2],
params: &IPDSpectrogramParams,
plan: &mut StftPlan,
) -> SpectrogramResult<IpdSpectrogram> {
let window_size = params.spectrogram_params.stft().n_fft();
let bin_width = params.spectrogram_params.sample_rate_hz / window_size.get() as f64;
let start_bin = (params.start_freq / bin_width).round() as usize;
let stop_bin = (params.end_freq / bin_width).round() as usize;
let left = audio[0];
let right = audio[1];
let left_spec = plan.compute(left, ¶ms.spectrogram_params)?;
let right_spec = plan.compute(right, ¶ms.spectrogram_params)?;
let (_, left_phase) = magphase(&left_spec, crate::nzu!(1));
let (_, right_phase) = magphase(&right_spec, crate::nzu!(1));
let num_frames = left_phase.shape()[1];
let num_bins = stop_bin - start_bin;
let left_phase_slice = left_phase.slice(ndarray::s![start_bin..stop_bin, ..]);
let right_phase_slice = right_phase.slice(ndarray::s![start_bin..stop_bin, ..]);
let mut ipd_spectrogram = Array2::zeros((num_bins, num_frames));
let pi = std::f64::consts::PI;
let two_pi = 2.0 * pi;
#[inline(always)]
fn np_mod(x: f64, m: f64) -> f64 {
((x % m) + m) % m
}
#[cfg(feature = "rayon")]
Zip::indexed(ipd_spectrogram.view_mut()).par_for_each(|(bin_idx, frame), o| {
let left_angle = left_phase_slice[(bin_idx, frame)].im.atan2(left_phase_slice[(bin_idx, frame)].re);
let right_angle = right_phase_slice[(bin_idx, frame)].im.atan2(right_phase_slice[(bin_idx, frame)].re);
let diff = left_angle - right_angle;
if params.wrapped {
*o = np_mod(diff + pi, two_pi) - pi;
} else {
*o = diff;
}
});
#[cfg(not(feature = "rayon"))]
Zip::indexed(ipd_spectrogram.view_mut()).for_each(|(bin_idx, frame), o| {
let left_angle = left_phase_slice[(bin_idx, frame)].im.atan2(left_phase_slice[(bin_idx, frame)].re);
let right_angle = right_phase_slice[(bin_idx, frame)].im.atan2(right_phase_slice[(bin_idx, frame)].re);
let diff = left_angle - right_angle;
if params.wrapped {
*o = np_mod(diff + pi, two_pi) - pi;
} else {
*o = diff;
}
});
let frequencies: Vec<f64> = (start_bin..stop_bin)
.map(|bin| bin as f64 * bin_width)
.collect();
let frequencies = NonEmptyVec::new(frequencies)
.expect("Frequency range should have at least one bin");
let hop_size = params.spectrogram_params.stft().hop_size().get() as f64;
let sample_rate = params.spectrogram_params.sample_rate_hz;
let times: Vec<f64> = (0..num_frames)
.map(|frame| frame as f64 * hop_size / sample_rate)
.collect();
let times = NonEmptyVec::new(times)
.expect("Time axis should have at least one frame");
Ok(IpdSpectrogram {
data: ipd_spectrogram,
params: params.clone(),
frequencies,
times,
_unit: PhantomData,
})
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IldSpectrogram {
pub data: Array2<f64>,
params: ILDSpectrogramParams,
frequencies: NonEmptyVec<f64>,
times: NonEmptyVec<f64>,
_unit: PhantomData<ILD>,
}
impl AsRef<Array2<f64>> for IldSpectrogram {
#[inline]
fn as_ref(&self) -> &Array2<f64> {
&self.data
}
}
impl Deref for IldSpectrogram {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl IldSpectrogram {
#[inline]
#[must_use]
pub fn n_bins(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.nrows()) }
}
#[inline]
#[must_use]
pub fn n_frames(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.ncols()) }
}
#[inline]
#[must_use]
pub const fn params(&self) -> &ILDSpectrogramParams {
&self.params
}
#[inline]
#[must_use]
pub fn frequencies(&self) -> &NonEmptySlice<f64> {
&self.frequencies
}
#[inline]
#[must_use]
pub fn times(&self) -> &NonEmptySlice<f64> {
&self.times
}
#[inline]
#[must_use]
pub fn frequency_range(&self) -> (f64, f64) {
let freqs = &*self.frequencies;
(freqs[0], freqs[freqs.len().get() - 1])
}
#[inline]
#[must_use]
pub fn duration(&self) -> f64 {
let ts = &*self.times;
ts[ts.len().get() - 1] - ts[0]
}
#[inline]
#[must_use]
pub const fn unit_label() -> &'static str {
"ILD (dB)"
}
#[must_use]
pub fn histogram(
&self,
num_bins: Option<NonZeroUsize>,
db_range: Option<(f64, f64)>,
exponent: Option<i32>,
energy_weighted: bool,
normalize: bool,
) -> Array2<f64> {
let num_bins = num_bins.unwrap_or_else(|| crate::nzu!(400)).get();
let (min_db, max_db) = db_range.unwrap_or((-24.0, 24.0));
let exponent = exponent.unwrap_or(3);
let bin_width = (max_db - min_db) / num_bins as f64;
let n_frames = self.n_frames().get();
let n_freq_bins = self.n_bins().get();
let mut histogram = Array2::zeros((num_bins, n_frames));
for frame in 0..n_frames {
for freq_bin in 0..n_freq_bins {
let ild_value = self.data[(freq_bin, frame)];
if !ild_value.is_finite() || ild_value < min_db || ild_value > max_db {
continue;
}
let bin_idx = ((ild_value - min_db) / bin_width).floor() as usize;
let bin_idx = bin_idx.min(num_bins - 1);
let weight = if energy_weighted {
1.0
} else {
1.0
};
histogram[(bin_idx, frame)] += weight;
}
if exponent != 1 {
for bin in 0..num_bins {
let val: f64 = histogram[(bin, frame)];
histogram[(bin, frame)] = val.powi(exponent);
}
}
if normalize {
let sum: f64 = histogram.column(frame).sum();
if sum > 0.0 {
for bin in 0..num_bins {
histogram[(bin, frame)] /= sum;
}
}
}
}
histogram
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ILDSpectrogramParams {
pub(crate) spectrogram_params: SpectrogramParams,
pub(crate) start_freq: f64,
pub(crate) end_freq: f64,
}
impl ILDSpectrogramParams {
pub fn new(
spec_params: SpectrogramParams,
start_freq: f64,
stop_freq: f64,
) -> SpectrogramResult<Self> {
let sample_rate = spec_params.sample_rate_hz;
if start_freq <= 0.0 || stop_freq <= 0.0 {
return Err(SpectrogramError::InvalidInput(
"Start and end frequencies must be positive.".into(),
));
}
if start_freq >= stop_freq {
return Err(SpectrogramError::InvalidInput(
"Start frequency must be less than end frequency.".into(),
));
}
if stop_freq > sample_rate / 2.0 {
return Err(SpectrogramError::InvalidInput(
"End frequency must be less than Nyquist frequency.".into(),
));
}
Ok(Self {
spectrogram_params: spec_params,
start_freq,
end_freq: stop_freq,
})
}
}
#[inline]
#[must_use]
pub fn compute_ild_spectrogram(
audio: [&NonEmptySlice<f64>; 2],
params: &ILDSpectrogramParams,
plan: &mut StftPlan,
) -> SpectrogramResult<IldSpectrogram> {
let window_size = params.spectrogram_params.stft().n_fft();
let bin_width = params.spectrogram_params.sample_rate_hz / window_size.get() as f64;
let start_bin = (params.start_freq / bin_width).round() as usize;
let stop_bin = (params.end_freq / bin_width).round() as usize;
let left = audio[0];
let right = audio[1];
let left_spec = plan.compute(left, ¶ms.spectrogram_params)?;
let right_spec = plan.compute(right, ¶ms.spectrogram_params)?;
let (left_mag, _) = magphase(&left_spec, crate::nzu!(1));
let (right_mag, _) = magphase(&right_spec, crate::nzu!(1));
let num_frames = left_mag.shape()[1];
let num_bins = stop_bin - start_bin;
let left_mag_slice = left_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let right_mag_slice = right_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let mut ild_spectrogram = Array2::from_elem((num_bins, num_frames), f64::NAN);
#[cfg(feature = "rayon")]
Zip::indexed(ild_spectrogram.view_mut()).par_for_each(|(bin_idx, frame), o| {
let left_val = left_mag_slice[(bin_idx, frame)];
let right_val = right_mag_slice[(bin_idx, frame)];
let intensity = left_val + right_val;
if intensity > 0.0 && left_val > 0.0 && right_val > 0.0 {
*o = -20.0 * (right_val / left_val).log10();
}
});
#[cfg(not(feature = "rayon"))]
Zip::indexed(ild_spectrogram.view_mut()).for_each(|(bin_idx, frame), o| {
let left_val = left_mag_slice[(bin_idx, frame)];
let right_val = right_mag_slice[(bin_idx, frame)];
let intensity = left_val + right_val;
if intensity > 0.0 && left_val > 0.0 && right_val > 0.0 {
*o = -20.0 * (right_val / left_val).log10();
}
});
let frequencies: Vec<f64> = (start_bin..stop_bin)
.map(|bin| bin as f64 * bin_width)
.collect();
let frequencies = NonEmptyVec::new(frequencies)
.expect("Frequency range should have at least one bin");
let hop_size = params.spectrogram_params.stft().hop_size().get() as f64;
let sample_rate = params.spectrogram_params.sample_rate_hz;
let times: Vec<f64> = (0..num_frames)
.map(|frame| frame as f64 * hop_size / sample_rate)
.collect();
let times = NonEmptyVec::new(times)
.expect("Time axis should have at least one frame");
Ok(IldSpectrogram {
data: ild_spectrogram,
params: params.clone(),
frequencies,
times,
_unit: PhantomData,
})
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IlrSpectrogram {
pub data: Array2<f64>,
params: ILRSpectrogramParams,
frequencies: NonEmptyVec<f64>,
times: NonEmptyVec<f64>,
_unit: PhantomData<ILR>,
}
impl AsRef<Array2<f64>> for IlrSpectrogram {
#[inline]
fn as_ref(&self) -> &Array2<f64> {
&self.data
}
}
impl Deref for IlrSpectrogram {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl IlrSpectrogram {
#[inline]
#[must_use]
pub fn n_bins(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.nrows()) }
}
#[inline]
#[must_use]
pub fn n_frames(&self) -> NonZeroUsize {
unsafe { NonZeroUsize::new_unchecked(self.data.ncols()) }
}
#[inline]
#[must_use]
pub const fn params(&self) -> &ILRSpectrogramParams {
&self.params
}
#[inline]
#[must_use]
pub fn frequencies(&self) -> &NonEmptySlice<f64> {
&self.frequencies
}
#[inline]
#[must_use]
pub fn times(&self) -> &NonEmptySlice<f64> {
&self.times
}
#[inline]
#[must_use]
pub fn frequency_range(&self) -> (f64, f64) {
let freqs = &*self.frequencies;
(freqs[0], freqs[freqs.len().get() - 1])
}
#[inline]
#[must_use]
pub fn duration(&self) -> f64 {
let ts = &*self.times;
ts[ts.len().get() - 1] - ts[0]
}
#[inline]
#[must_use]
pub const fn unit_label() -> &'static str {
"ILR (normalized)"
}
#[must_use]
pub fn histogram(
&self,
num_bins: Option<NonZeroUsize>,
ratio_range: Option<(f64, f64)>,
exponent: Option<i32>,
energy_weighted: bool,
normalize: bool,
) -> Array2<f64> {
let num_bins = num_bins.unwrap_or_else(|| crate::nzu!(400)).get();
let (min_ratio, max_ratio) = ratio_range.unwrap_or((-1.0, 1.0));
let exponent = exponent.unwrap_or(3);
let bin_width = (max_ratio - min_ratio) / num_bins as f64;
let n_frames = self.n_frames().get();
let n_freq_bins = self.n_bins().get();
let mut histogram = Array2::zeros((num_bins, n_frames));
for frame in 0..n_frames {
for freq_bin in 0..n_freq_bins {
let ilr_value = self.data[(freq_bin, frame)];
if !ilr_value.is_finite() || ilr_value < min_ratio || ilr_value > max_ratio {
continue;
}
let bin_idx = ((ilr_value - min_ratio) / bin_width).floor() as usize;
let bin_idx = bin_idx.min(num_bins - 1);
let weight = if energy_weighted {
1.0
} else {
1.0
};
histogram[(bin_idx, frame)] += weight;
}
if exponent != 1 {
for bin in 0..num_bins {
let val: f64 = histogram[(bin, frame)];
histogram[(bin, frame)] = val.powi(exponent);
}
}
if normalize {
let sum: f64 = histogram.column(frame).sum();
if sum > 0.0 {
for bin in 0..num_bins {
histogram[(bin, frame)] /= sum;
}
}
}
}
histogram
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ILRSpectrogramParams {
pub(crate) spectrogram_params: SpectrogramParams,
pub(crate) start_freq: f64,
pub(crate) end_freq: f64,
}
impl ILRSpectrogramParams {
pub fn new(
spec_params: SpectrogramParams,
start_freq: f64,
stop_freq: f64,
) -> SpectrogramResult<Self> {
let sample_rate = spec_params.sample_rate_hz;
if start_freq <= 0.0 || stop_freq <= 0.0 {
return Err(SpectrogramError::InvalidInput(
"Start and end frequencies must be positive.".into(),
));
}
if start_freq >= stop_freq {
return Err(SpectrogramError::InvalidInput(
"Start frequency must be less than end frequency.".into(),
));
}
if stop_freq > sample_rate / 2.0 {
return Err(SpectrogramError::InvalidInput(
"End frequency must be less than Nyquist frequency.".into(),
));
}
Ok(Self {
spectrogram_params: spec_params,
start_freq,
end_freq: stop_freq,
})
}
}
#[inline]
#[must_use]
pub fn compute_ilr_spectrogram(
audio: [&NonEmptySlice<f64>; 2],
params: &ILRSpectrogramParams,
plan: &mut StftPlan,
) -> SpectrogramResult<IlrSpectrogram> {
let window_size = params.spectrogram_params.stft().n_fft();
let bin_width = params.spectrogram_params.sample_rate_hz / window_size.get() as f64;
let start_bin = (params.start_freq / bin_width).round() as usize;
let stop_bin = (params.end_freq / bin_width).round() as usize;
let left = audio[0];
let right = audio[1];
let left_spec = plan.compute(left, ¶ms.spectrogram_params)?;
let right_spec = plan.compute(right, ¶ms.spectrogram_params)?;
let (left_mag, _) = magphase(&left_spec, crate::nzu!(1));
let (right_mag, _) = magphase(&right_spec, crate::nzu!(1));
let num_frames = left_mag.shape()[1];
let num_bins = stop_bin - start_bin;
let left_mag_slice = left_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let right_mag_slice = right_mag.slice(ndarray::s![start_bin..stop_bin, ..]);
let mut ilr_spectrogram = Array2::from_elem((num_bins, num_frames), f64::NAN);
#[cfg(feature = "rayon")]
Zip::indexed(ilr_spectrogram.view_mut()).par_for_each(|(bin_idx, frame), o| {
let left_val = left_mag_slice[(bin_idx, frame)];
let right_val = right_mag_slice[(bin_idx, frame)];
let intensity = left_val + right_val;
if intensity > 0.0 && left_val > 0.0 && right_val > 0.0 {
let ratio = right_val / left_val;
if ratio < 1.0 {
*o = 1.0 - ratio;
} else {
*o = -(1.0 - 1.0 / ratio);
}
}
});
#[cfg(not(feature = "rayon"))]
Zip::indexed(ilr_spectrogram.view_mut()).for_each(|(bin_idx, frame), o| {
let left_val = left_mag_slice[(bin_idx, frame)];
let right_val = right_mag_slice[(bin_idx, frame)];
let intensity = left_val + right_val;
if intensity > 0.0 && left_val > 0.0 && right_val > 0.0 {
let ratio = right_val / left_val;
if ratio < 1.0 {
*o = 1.0 - ratio;
} else {
*o = -(1.0 - 1.0 / ratio);
}
}
});
let frequencies: Vec<f64> = (start_bin..stop_bin)
.map(|bin| bin as f64 * bin_width)
.collect();
let frequencies = NonEmptyVec::new(frequencies)
.expect("Frequency range should have at least one bin");
let hop_size = params.spectrogram_params.stft().hop_size().get() as f64;
let sample_rate = params.spectrogram_params.sample_rate_hz;
let times: Vec<f64> = (0..num_frames)
.map(|frame| frame as f64 * hop_size / sample_rate)
.collect();
let times = NonEmptyVec::new(times)
.expect("Time axis should have at least one frame");
Ok(IlrSpectrogram {
data: ilr_spectrogram,
params: params.clone(),
frequencies,
times,
_unit: PhantomData,
})
}
#[inline]
pub(crate) fn median(arr: &Array1<f64>) -> f64 {
let mut sorted: Vec<f64> = arr.iter().filter(|x| x.is_finite()).copied().collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = sorted.len();
if len == 0 {
return f64::NAN;
}
if len % 2 == 0 {
(sorted[len / 2 - 1] + sorted[len / 2]) / 2.0
} else {
sorted[len / 2]
}
}
#[inline]
#[must_use]
pub fn compute_itd_spectrogram_diff(
reference: [&NonEmptySlice<f64>; 2],
test: [&NonEmptySlice<f64>; 2],
params: &ITDSpectrogramParams,
plan: &mut StftPlan,
) -> SpectrogramResult<(Array1<f64>, f64, f64)> {
let ref_itd = compute_itd_spectrogram(reference, params, plan)?;
let test_itd = compute_itd_spectrogram(test, params, plan)?;
let diff = &test_itd.data - &ref_itd.data;
let col_means = diff
.mean_axis(Axis(0))
.expect("Non-empty slices produce non-zero diff array");
let mean_diff_degrees = col_means
.mapv(|x| x.abs() * (1.0 / 0.00086) * 90.0)
.mean()
.expect("Non-empty slices produce non-zero diff array");
let mean_diff_itd = median(&col_means);
Ok((col_means, mean_diff_degrees, mean_diff_itd))
}
#[inline]
#[must_use]
pub fn compute_ilr_spectrogram_diff(
reference: [&NonEmptySlice<f64>; 2],
test: [&NonEmptySlice<f64>; 2],
params: &ILRSpectrogramParams,
plan: &mut StftPlan,
) -> SpectrogramResult<(Array1<f64>, f64)> {
let ref_ilr = compute_ilr_spectrogram(reference, params, plan)?;
let test_ilr = compute_ilr_spectrogram(test, params, plan)?;
let diff = &test_ilr.data - &ref_ilr.data;
let n_frames = diff.ncols();
let col_means: Array1<f64> = (0..n_frames)
.map(|frame| {
let col = diff.column(frame);
let (sum, count) = col
.iter()
.filter(|x| !x.is_nan())
.fold((0.0_f64, 0usize), |(s, c), x| (s + x, c + 1));
if count == 0 {
f64::NAN
} else {
sum / count as f64
}
})
.collect();
let mean_diff = {
let (sum, count) = col_means
.iter()
.filter(|x| !x.is_nan())
.fold((0.0_f64, 0usize), |(s, c), x| (s + x.abs(), c + 1));
if count == 0 {
f64::NAN
} else {
sum / count as f64
}
};
Ok((col_means, mean_diff))
}