3.5 KiB
algorithms/energy-odf
Purpose
Energy onset-detection function (ODF) — the cheapest of the three algorithms
shipped by @suaveplan/audio-analysis-beat. It consumes a per-block energy
sequence and emits a 1-D curve whose peaks mark sample positions where the
signal's energy rises sharply, i.e. note onsets.
Energy ODF works extremely well on percussive material (kick, snare, claps) and is the only path that adds essentially no cost over a plain document scan — per-block energies can be reused from the host's existing block statistics pipeline rather than recomputed.
Features
blockEnergy(block)— mean per-sample energy of a single multi-channel block. Defensive against empty blocks, ragged channel arrays, and zero-length data.energyOdf(energies)— half-wave-rectified first difference of an energy sequence. Same length as the input; entry 0 is always zero.energyOdfFromSamples(channels, blockSize)— convenience that splits raw channel data intoblockSize-sample blocks, computes block energies, and returns the ODF in one call.
Usage: Basic Example
import { energyOdf } from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
const energies = new Float64Array([0, 0, 0.5, 0.6, 0.1, 0.05, 0.4]);
const odf = energyOdf(energies);
// odf === Float64Array([0, 0, 0.5, 0.1, 0, 0, 0.35])
Usage: Advanced Example
import {
blockEnergy,
energyOdfFromSamples,
} from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
const sampleRate = 48_000;
const blockSize = 1024;
const left = new Float32Array(sampleRate * 4); // 4 seconds mono-ish
const right = new Float32Array(sampleRate * 4);
// ... fill ...
const odf = energyOdfFromSamples([left, right], blockSize);
const hopSeconds = blockSize / sampleRate;
// Pick the loudest block in the first second:
let bestIdx = 0;
let best = 0;
for (let i = 0; i < Math.floor(1 / hopSeconds); i++) {
if (odf[i] > best) {
best = odf[i];
bestIdx = i;
}
}
console.log("onset at", bestIdx * hopSeconds, "seconds");
console.log("avg first-block energy", blockEnergy([left.subarray(0, blockSize)]));
API Reference
blockEnergy(block: readonly Float32Array[]): number— mean per-sample energy summed across channels then divided bylength * channels.energyOdf(energies: Float64Array): Float64Array— same-length output;out[i] = max(0, energies[i] - energies[i-1]),out[0] === 0.energyOdfFromSamples(channels: readonly Float32Array[], blockSize: number): Float64Array— combined pipeline; returns an emptyFloat64ArraywhenblockSize <= 0or the first channel is empty.
Implementation Notes
The half-wave rectification (d > 0 ? d : 0) is the entire reason this works
as an onset detector rather than a generic energy-change detector — falling
edges (notes ending) carry no perceptual onset and zeroing them out keeps the
ODF interpretable as a peak-picking problem.
Mean-per-sample energy rather than raw sum is used because the final block of
a document is often shorter than blockSize; without normalisation the last
block would appear artificially quiet and the ODF would spike negative
(rectified to zero), masking a legitimate onset at the end of the track.
Defensive ?? 0 and ?? undefined-shape guards thread through every loop so
malformed input — an empty channels array, a ragged channels array, a block
whose first channel is undefined — yields a defined zero rather than throwing.
This module is the leaf any of the three algorithm paths can fall back to and
must never propagate an exception of its own.