first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user