Beat detection
Drive visuals from beat and frequency information.
Beat detection
The BeatDetector analyses an audio signal in real time using a self-contained AudioWorkletProcessor. It detects tempo, beat positions, and bar/downbeat structure, then reports results back to the main thread as both discrete events (onBeat, onDownbeat, onBarStart, onTempoChange) and per-frame polling getters. You use the events for one-shot triggers and the getters for continuous animation — both are covered in detail in Audio-reactive visualization.
This chapter covers what the detector does and the events it fires. The next chapter covers what you do with that information.
Connecting a source
The detector taps a live audio source — an AudioBus, a Voice, an AudioNode, or a MediaStream — as a parallel branch that does not affect the source’s main signal chain. It does not accept a Sound/AudioStream descriptor; tap the bus the audio plays through (e.g. app.audio.music) or the individual Voice you got back from play():
import { AudioStream } from '@codexo/exojs';
import { BeatDetector } from '@codexo/exojs-audio-fx';
const track = loader.get(AudioStream, 'track');
this.app.audio.play(track, { loop: true });
const detector = new BeatDetector();
detector.source = this.app.audio.music; // the bus the track plays through
You can also pass the source as a constructor option, or point it at a single voice:
const voice = this.app.audio.play(track, { loop: true });
const detector = new BeatDetector({ source: voice });
The detector runs its own FFT inside the worklet processor. No AudioAnalyser is required — BeatDetector handles frequency analysis internally. The fftSize option (default 2048) and hopSize (default 512) control the worklet’s internal analysis window.
Tempo and confidence
Two read-only getters give you the detector’s current tempo estimate:
update(delta) {
const bpm = this.detector.tempo; // 0 before lock-in
const confidence = this.detector.confidence; // 0..1
}
tempo returns 0 until the detector has enough data to lock onto a stable beat. confidence rises as the beat grid stabilises. The onTempoChange signal fires when the detector settles on a new tempo:
detector.onTempoChange.add((newBpm, oldBpm) => {
console.log(`Tempo changed from ${oldBpm} to ${newBpm} BPM`);
});
Beats arrive in two trust levels via BeatInfo.status. Early, low-latency beats are tagged 'provisional' (after a short minSettlingMs warm-up, default 400) — ideal for snappy visual reactivity like a beat “blink”. Once the tempo grid is settled and trustworthy, beats promote to 'locked' (each stable segment yields exactly one provisional→locked transition). The single onBeat signal carries both; check info.status to decide how much to trust a given beat, or set emitProvisionalBeats: false to receive only locked beats. The tempo/confidence getters only report once the grid is locked.
Event signals
Four signals fire at discrete moments in the music:
| Signal | Fires when | Payload |
|---|---|---|
onBeat | A beat occurs | BeatInfo — audio time, tempo, confidence, energy, isDownbeat, beatInBar, status ('provisional' | 'locked') |
onDownbeat | The first beat of a bar | Same BeatInfo |
onBarStart | A new bar begins | BarInfo — audio time, tempo, confidence, barNumber |
onTempoChange | Tempo estimate changes | (newBpm, oldBpm) |
Use onBeat for things that should happen on every beat — a sprite pulses, a particle burst fires, a UI element flashes:
detector.onBeat.add(info => {
this.pulse = 0.3;
this.particleBurst.reset();
});
Use onDownbeat for events that should only fire on the “one” — a brighter flash, a bar-level visual change, a camera shake:
detector.onDownbeat.add(info => {
this.barFlash = 0.5;
});
BeatInfo carries isDownbeat and beatInBar so you can use the general onBeat signal and filter inside the callback:
detector.onBeat.add(info => {
if (info.isDownbeat) {
// big effect on beat 1
} else if (info.beatInBar === 3) {
// secondary emphasis on beat 3
}
});
State getters
The detector exposes read-only state you can poll every frame:
detector.tempo // BPM, or 0 before lock
detector.confidence // 0..1 locking quality
detector.beatPhase // 0..1 phase within current beat
detector.barPosition // 1..N beat within current bar
detector.barLength // beats per bar (default 4)
detector.timeSignature // { numerator, denominator }
detector.rms // 0..1 overall RMS energy
detector.onsetStrength // 0..1 onset novelty at current frame
detector.bandEnergy // { low, mid, high } band energies
detector.tempoCandidates // readonly array of { bpm, score } candidates
detector.lookahead // readonly array of upcoming beats
These are pure derivations from the detector’s most recent worklet state update — no allocation, no event dispatch.
Bar and time-signature tracking
The detector attempts 4/4 vs. 3/4 time-signature detection when enableTimeSignatureDetection is true (the default). Set it false in the constructor to lock to 4/4:
const detector = new BeatDetector({
enableTimeSignatureDetection: false,
});
barPosition (1-based) tells you which beat you’re on within the current bar. barLength reports the detected bar length. Together they let you build sequencers, rhythm-game logic, and procedural music controllers that follow the music’s structure.
Worklet processor
BeatDetector uses an AudioWorkletProcessor that runs entirely in the Web Audio rendering thread. It applies a Hann window, runs a radix-2 FFT, computes mel-band energies, detects onsets via spectral flux, builds a tempogram with autocorrelation, and tracks beat phase with a bounded phase-locked loop (PLL). All of this is internal — the public API is the events and getters listed above.
Beat/state messages are posted from the worklet and handled asynchronously on the main thread. They are not locked to your scene’s update timing, and worklet timing can be finer than requestAnimationFrame. For frame-stable visual animation, prefer polling getters in update (see Audio-reactive visualization).
Configuring the detector
Construction options with their defaults:
const detector = new BeatDetector({
minBpm: 50, // minimum detectable tempo
maxBpm: 300, // maximum detectable tempo
fftSize: 2048, // internal FFT window
hopSize: 512, // samples between successive FFTs
fastTempoWindowSec: 2.5, // short tempogram window (reacts to tempo changes)
stableTempoWindowSec: 8, // long tempogram window (holds the grid steady)
acfIntervalHops: 15, // hops between tempogram recomputations
minSettlingMs: 400, // warm-up before the first (provisional) beat
emitProvisionalBeats: true, // emit early provisional beats for visual reactivity
melBands: 24, // mel filterbank bands
enableTimeSignatureDetection: true, // auto-detect 3/4 vs 4/4
source: app.audio.music, // initial source: bus or Voice (optional)
});
minBpm and maxBpm constrain the tempo search. The tempogram runs two autocorrelation windows: stableTempoWindowSec holds a steady tempo grid, while the shorter fastTempoWindowSec reacts to tempo changes — the tracked tempo only follows the fast window when it disagrees with the stable one consistently, so a real DJ drift is tracked without making a steady tempo nervous. acfIntervalHops sets how often the tempogram is recomputed (lower reacts faster at more CPU). melBands controls the granularity of the internal onset detection; the default of 24 is sufficient for most music.
Lifecycle
Create one detector per scene (or share across scenes if the same source persists). Destroy it when you’re done:
destroy() {
this.detector.destroy();
}
destroy() disconnects the worklet node, clears the signal subscribers, and releases references. The worklet processor itself stays registered for the duration of the page — that’s a browser-level concern, not a leak.
Examples
A sprite pulses on every beat and particles burst on the onBeat signal — the canonical beat-reactive pattern.
Live BPM display with a confidence bar, demonstrating the tempo and confidence getters.
Where to go next
The next chapter, Audio-reactive visualization, covers how to bridge the detector’s output into the render pipeline — polling getters for continuous animation, spectrum data from AudioAnalyser, spectrograms with DataTexture, and building full audio-reactive scenes.