///|
/// Constant-Q Transform (CQT) feature extraction.
/// Represents audio on a logarithmic frequency scale with constant Q-factor per octave.
let cqt_pi : Double = 3.14159265358979323846264338327950288
///|
/// Configuration for Constant-Q Transform.
pub(all) struct CqtConfig {
sample_rate : Int
hop_length : Int
f_min : Double
n_bins : Int
bins_per_octave : Int
tuning : Double
power : Double
} derive(Eq, Debug)
///|
/// Default CqtConfig (16 kHz, 512 hop, C1 min frequency 32.7 Hz, 84 bins / 7 octaves, 12 bins/octave).
pub fn CqtConfig::default() -> CqtConfig {
{
sample_rate: 16000,
hop_length: 512,
f_min: 32.70319566257483, // C1
n_bins: 84, // 7 octaves * 12 semitones
bins_per_octave: 12,
tuning: 0.0,
power: 2.0,
}
}
///|
/// Validate CqtConfig parameters.
pub fn CqtConfig::validate(self : CqtConfig) -> Result[Unit, @types.MelError] {
if self.sample_rate <= 0 {
return Err(InvalidSampleRate(self.sample_rate))
}
if self.hop_length <= 0 {
return Err(InvalidHopLength(self.hop_length))
}
if self.n_bins <= 0 || self.bins_per_octave <= 0 {
return Err(DimensionMismatch("n_bins and bins_per_octave must be positive"))
}
let nyquist = self.sample_rate.to_double() / 2.0
if !@types.is_finite(self.f_min) ||
self.f_min <= 0.0 ||
self.f_min >= nyquist ||
!@types.is_finite(self.tuning) ||
!@types.is_finite(self.power) ||
self.power <= 0.0 {
return Err(InvalidFrequencyRange(self.f_min, nyquist))
}
let highest = self.f_min *
@math.pow(
2.0,
(self.n_bins.to_double() - 1.0 - self.tuning) /
self.bins_per_octave.to_double(),
)
if !@types.is_finite(highest) || highest > nyquist {
return Err(InvalidFrequencyRange(self.f_min, highest))
}
Ok(())
}
///|
/// Compute center frequencies for all CQT bins.
pub fn cqt_frequencies(config : CqtConfig) -> Array[Double] {
let freqs = Array::make(config.n_bins, 0.0)
let b = config.bins_per_octave.to_double()
for k in 0.. Array[CqtKernel] {
let b = config.bins_per_octave.to_double()
let q = 1.0 / (@math.pow(2.0, 1.0 / b) - 1.0)
let sr = config.sample_rate.to_double()
let kernels = Array::make(config.n_bins, CqtKernel::{
n_samples: 0,
kernel_re: [],
kernel_im: [],
})
for k in 0.. Result[@types.FeatureMatrix, @types.MelError] {
match config.validate() {
Ok(_) => ()
Err(e) => return Err(e)
}
if samples.is_empty() {
return Err(EmptyAudio)
}
let freqs = cqt_frequencies(config)
let kernels = build_cqt_kernels(config, freqs)
// Find max kernel length to pad signal appropriately
let mut max_len = 0
for k in 0.. max_len {
max_len = kernels[k].n_samples
}
}
let pad_width = max_len / 2
let padded = @window.pad_reflect(samples, pad_width)
let padded_len = padded.length()
let hop = config.hop_length
let n_frames = if padded_len >= max_len {
1 + (padded_len - max_len) / hop
} else {
0
}
if n_frames <= 0 {
return Ok(@types.FeatureMatrix::zeros(config.n_bins, 0))
}
let out = @types.FeatureMatrix::zeros(config.n_bins, n_frames)
let power = config.power
for frame in 0..