///|
/// Onset strength computation method.
pub(all) enum OnsetMethod {
SpectralFlux
SuperFlux(Int) // Frequency filter window size (radius = size / 2)
Energy
} derive(Eq, Debug)
///|
/// Configuration for onset strength envelope extraction.
pub(all) struct OnsetConfig {
mode : OnsetMethod
normalize : Bool // Normalize envelope to [0.0, 1.0]
} derive(Eq, Debug)
///|
/// Default configuration: Spectral flux with normalization.
pub fn OnsetConfig::default() -> OnsetConfig {
{ mode: SpectralFlux, normalize: true, }
}
///|
/// Standard spectral flux: half-wave rectified difference across successive frames.
pub fn spectral_flux(spec : @types.FeatureMatrix) -> Array[Double] {
let n_rows = spec.n_rows
let n_cols = spec.n_cols
let flux : Array[Double] = Array::make(n_cols, 0.0)
if n_cols <= 1 {
return flux
}
for c in 1.. 0.0 {
sum = sum + diff
}
}
flux[c] = sum
}
flux
}
///|
/// SuperFlux: spectral difference relative to maximum-filtered previous frame (Boeck & Widmer 2013).
/// Filter sizes <= 1 disable frequency spreading and use ordinary spectral flux.
pub fn super_flux(
spec : @types.FeatureMatrix,
filter_size : Int,
) -> Array[Double] {
if filter_size <= 1 {
return spectral_flux(spec)
}
let n_rows = spec.n_rows
let n_cols = spec.n_cols
let flux : Array[Double] = Array::make(n_cols, 0.0)
if n_cols <= 1 {
return flux
}
let radius = filter_size / 2
for c in 1.. 0 { r - radius } else { 0 }
let r_max = if r + radius < n_rows - 1 { r + radius } else { n_rows - 1 }
let mut max_prev = spec.at(r_min, c - 1)
for k in (r_min + 1)..<=r_max {
let v = spec.at(k, c - 1)
if v > max_prev {
max_prev = v
}
}
let diff = spec.at(r, c) - max_prev
if diff > 0.0 {
sum = sum + diff
}
}
flux[c] = sum
}
flux
}
///|
/// Compute onset strength envelope from spectrogram.
pub fn onset_strength(
spec : @types.FeatureMatrix,
config : OnsetConfig,
) -> Array[Double] {
let raw = match config.mode {
SpectralFlux => spectral_flux(spec)
SuperFlux(size) => super_flux(spec, size)
Energy => {
let n_cols = spec.n_cols
let n_rows = spec.n_rows
let out : Array[Double] = Array::make(n_cols, 0.0)
if n_cols > 1 {
let mut prev_energy = 0.0
for r in 0.. 0.0 { diff } else { 0.0 }
prev_energy = curr_energy
}
}
out
}
}
if config.normalize {
let mut max_val = 0.0
for v in raw {
if v > max_val {
max_val = v
}
}
if max_val > 1.0e-12 {
let inv = 1.0 / max_val
for i in 0.. Array[Int] {
let len = envelope.length()
let peaks : Array[Int] = []
if len == 0 {
return peaks
}
let mut last_onset = -wait - 1
for i in 0.. 0 { i - pre_max } else { 0 }
let end_max = if i + post_max < len - 1 { i + post_max } else { len - 1 }
let mut is_max = true
for j in start_max..<=end_max {
if j != i && envelope[j] >= val {
is_max = false
break
}
}
if !is_max {
continue
}
// 2. Check local average threshold in [i - pre_avg, i + post_avg]
let start_avg = if i - pre_avg > 0 { i - pre_avg } else { 0 }
let end_avg = if i + post_avg < len - 1 { i + post_avg } else { len - 1 }
let count = end_avg - start_avg + 1
let mut sum = 0.0
for j in start_avg..<=end_avg {
sum = sum + envelope[j]
}
let local_mean = sum / count.to_double()
if val < local_mean + delta {
continue
}
// 3. Check wait interval
if i - last_onset >= wait {
peaks.push(i)
last_onset = i
}
}
peaks
}
///|
/// Extract onset strength envelope directly from AudioBuffer.
pub fn onset_strength_audio(
audio : @types.AudioBuffer,
config : @types.FeatureConfig,
onset_config? : OnsetConfig = OnsetConfig::default(),
) -> Result[Array[Double], @types.MelError] {
match @stft.stft_spectrogram(audio.samples, config) {
Ok(spec) => Ok(onset_strength(spec, onset_config))
Err(e) => Err(e)
}
}
///|
/// Detect onset event frame indices directly from AudioBuffer.
pub fn onset_detect_audio(
audio : @types.AudioBuffer,
config : @types.FeatureConfig,
onset_config? : OnsetConfig = OnsetConfig::default(),
delta? : Double = 0.05,
wait? : Int = 3,
) -> Result[Array[Int], @types.MelError] {
match onset_strength_audio(audio, config, onset_config~) {
Ok(env) => Ok(peak_pick(env, delta~, wait~))
Err(e) => Err(e)
}
}
///|
pub extend OnsetConfig with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend OnsetConfig with Eq::{not_equal, equal}
///|
pub extend OnsetMethod with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend OnsetMethod with Eq::{not_equal, equal}