DeadOn is a drift-less, performance.now()–based clock for precision musical timing in web applications. It emits millisecond-accurate ticks at a configurable PPQN resolution, making it ideal for synchronising Web Audio, Web MIDI, UI updates, or other precise scheduling needs.
-
Drift-less scheduling: Uses
performance.now()
for a reliable timebase. - Configurable resolution: Adjustable PPQN for any musical subdivision.
- Tunable lookahead & interval: Balance scheduling ahead of time with responsiveness.
- Lightweight: No dependencies and minimal setup.
Install via npm:
npm install dead-on
import { DeadOnClock } from "dead-on";
const audioCtx = new AudioContext(); // optional for sample-accurate audioTime
const clock = new DeadOnClock({
bpm: 120, // beats per minute (default: 120)
ppqn: 24, // pulses per quarter-note (default: 24)
lookahead: 50, // how far ahead to schedule in ms (default: 50)
interval: 20, // main loop interval in ms (default: 20)
audioContext: audioCtx, // optional AudioContext
});
clock.start();
const quarterTicks = clock.ppqn;
const barTicks = clock.ppqn * 4;
clock.on("tick", (e) => {
// e.timeMs: high-resolution wall-clock timestamp (performance.now())
// e.audioTime: AudioContext.currentTime if provided, else timeMs/1000
// e.tick: integer tick count
if (e.tick % quarterTicks === 0) {
// quarter-note event
}
if (e.tick % barTicks === 0) {
// bar event
}
});
Use clock.scheduleAt
to run a callback exactly at a future timeMs
:
clock.scheduleAt(() => {
console.log("Runs precisely on the tick!");
}, e.timeMs);
Creates a new clock.
Option | Type | Default | Description |
---|---|---|---|
bpm |
number |
120 |
Beats per minute |
ppqn |
number |
24 |
Pulses per quarter-note |
lookahead |
number (ms) |
50 |
How far ahead to schedule events |
interval |
number (ms) |
20 |
Main loop interval |
audioContext |
AudioContext |
- |
Optional for sample-accurate audioTime |
-
clock.start()
→void
Start the clock (no-op if already running). -
clock.stop()
→void
Stop the clock. -
clock.setBpm(bpm: number)
→void
Change the tempo on the fly. -
clock.setPpqn(ppqn: number)
→void
Change the resolution on the fly. -
clock.on('tick', callback)
→void
Subscribe to tick events.
callback(e: { timeMs: number; audioTime: number; tick: number })
-
clock.off('tick', callback)
→void
Unsubscribe from tick events. -
clock.scheduleAt(callback: () => void, timeMs: number)
→void
Schedule a one-off callback at a specificperformance.now()
timestamp.
A fixed-length, latency-free step sequencer built on top of DeadOnClock
.
import { DeadOnSequencer, StepAction } from "dead-on";
// Create a 16-step sequencer
const seq = new DeadOnSequencer(clock, 16);
// Define a pattern
type Note = { freq: number; durationMs: number };
const pattern: Array<StepAction<Note> | null> = Array(16).fill(null);
pattern[0] = {
payload: [{ freq: 440, durationMs: 200 }],
subdivs: 0, // play all payloads at once
offsetMs: 10, // shift by 10ms
};
seq.setSequence(pattern);
// Adjust playback speed (e.g. half-speed)
seq.setSpeedFactor(0.5);
// Play payloads on each tick
clock.on("tick", (e) => {
const payloads = seq.getPayloadsForTick(e.tick);
for (const note of payloads) {
const osc = audioCtx.createOscillator();
DeadOnSequencer.triggerAudio(osc, e.audioTime, note.durationMs);
}
});
interface StepAction<P> {
payload: P[]; // one or more items to schedule
subdivs?: number; // subdivisions per step (default: 0)
offsetMs?: number; // humanization or timing offset in ms
}
Method | Description |
---|---|
new DeadOnSequencer(clock, steps) |
Create a sequencer with given number of steps |
seq.setSequence(seq) |
Replace entire sequence at once |
seq.setStep(step, action) |
Set or clear a single step |
seq.clearSequence() |
Clear all steps |
seq.clearStep(step) |
Clear a specific step |
seq.setBpm(bpm) |
Change sequencer tempo |
seq.setPpqn(ppqn) |
Change sequencer resolution |
seq.setSpeedFactor(factor) |
Change playback speed by a continuous factor (1 = normal; <1 = slower; >1 = faster) |
seq.getPayloadsForTick(tick) |
Get payloads scheduled on a given tick |
DeadOnSequencer.triggerAudio(osc: OscillatorNode, startTimeSec: number, durationMs?: number)
DeadOnSequencer.triggerMidi(midiOut: MIDIOutput, note: number, velocity: number, startTimeMs: number, offTimeMs?: number)
Apache 2.0 © 2xAA