Files
skills/docs/media/audio/audio-analysis-beat/src/algorithms/energy-odf.md
2026-05-29 08:20:56 +01:00

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 into blockSize-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 by length * 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 empty Float64Array when blockSize <= 0 or 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.