first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# `algorithms/spectral-flux`
## Purpose
Spectral-flux onset detection via a hand-rolled radix-2 real FFT. This module
provides the highest-precision ODF in the package — it catches timbral onsets
(hi-hat over a sustained pad, a fingertip pluck on a held note) that the
energy ODF cannot see, because it measures *which bins are changing* rather
than *how much total energy is moving*.
It is written entirely in terms of `Float32Array` and `Float64Array` and
never touches the browser `AnalyserNode` or any Node-only API. The same code
runs identically under Bun, Node.js, Deno, and every browser.
## Features
- `fftRadix2(re, im)` — in-place Cooley-Tukey complex FFT for power-of-two
sizes. The lowest-level primitive; both arrays are read and written.
- `magnitudeSpectrum(frame, fftSize)` — real-input single-sided magnitude
spectrum with a Hann window applied before transform; returns length
`fftSize/2 + 1`.
- `spectralFlux(frame, prevMag, fftSize)` — single-frame ODF value; sum of
positive bin-to-bin differences. `prevMag` is mutated to become the current
magnitude spectrum so the caller can chain across frames.
- `spectralFluxOdf(mono, fftSize, hop)` — convenience that splits a mono
sample stream into `hop`-sample frames of `fftSize` samples (zero-padded as
needed) and returns the full ODF.
## Usage: Basic Example
```ts
import { spectralFluxOdf } from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
const mono = new Float32Array(48_000 * 4); // 4 seconds of audio
// ... fill mono with samples ...
const odf = spectralFluxOdf(mono, 1024, 512);
console.log(odf.length, "frames"); // ceil(192000 / 512) = 375
```
## Usage: Advanced Example
```ts
import {
magnitudeSpectrum,
spectralFlux,
} from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
const fftSize = 2048;
const hop = 512;
const buf = new Float32Array(fftSize);
const prev = new Float64Array((fftSize >> 1) + 1);
const mono = new Float32Array(48_000 * 10); // 10 seconds
// Manual frame loop — lets the caller stop early on an AbortSignal,
// thread per-frame work through other code, or thin the ODF on the fly.
const odfValues: number[] = [];
for (let start = 0; start + fftSize <= mono.length; start += hop) {
buf.set(mono.subarray(start, start + fftSize));
odfValues.push(spectralFlux(buf, prev, fftSize));
}
// Verify the magnitude spectrum has the expected shape on its own.
const probe = magnitudeSpectrum(mono.subarray(0, fftSize), fftSize);
console.log(probe.length === fftSize / 2 + 1);
```
## API Reference
- `fftRadix2(re: Float64Array, im: Float64Array): void` — `re.length` must
equal `im.length` and must be a power of two; transform is in place.
- `magnitudeSpectrum(frame: Float32Array, fftSize: number): Float64Array` —
output length `fftSize/2 + 1`; Hann-windowed.
- `spectralFlux(frame: Float32Array, prevMag: Float64Array, fftSize: number):
number` — returns the flux value; `prevMag` is mutated in place to become
the current magnitude spectrum.
- `spectralFluxOdf(mono: Float32Array, fftSize: number, hop: number):
Float64Array` — returns empty array when any argument is empty or
non-positive; `odf[0]` is forced to zero so peak-picking does not flag the
artificial first-frame onset.
## Implementation Notes
The FFT is a textbook iterative Cooley-Tukey radix-2 with explicit
bit-reversal permutation followed by `log2(n)` rounds of butterflies. Twiddle
factors are computed by complex multiplication of a running unit-magnitude
phase rather than a precomputed table — for the FFT sizes used in this
package (`<= 4096`) the table cost is not worth it, and trig recurrence
avoids the alignment subtleties of typed-array tables.
Only positive bin-to-bin differences contribute to the flux. Negative
differences (bins that are quieter than in the previous frame) carry no
perceptual onset information and including them would partly cancel the
positive contributions, blurring peaks.
`prevMag` is initialised to zeros, which means the first frame's flux is the
sum of its full magnitude spectrum — an artefact, not a real onset. The
`spectralFluxOdf` wrapper therefore overwrites `odf[0]` to zero before
returning so peak-picking does not flag that boundary.