///|
/// 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..