How Tone.js Schedules Events on the Transport Timeline
Tone.js manages the scheduling of audio events through its global
Transport timeline, which acts as a master musical clock
for synchronizing synthesizers, samplers, and effects. By combining the
high-precision clock of the Web Audio API with JavaScript’s event loop,
Tone.js allows developers to schedule notes, loops, and coordinate
tempo-relative playback with sample-accurate precision. This article
explores how the Transport works, how time is represented,
and the different scheduling methods available for creating interactive
web audio applications.
The Role of Tone.Transport
At the core of Tone.js scheduling is Tone.Transport.
Unlike the default Web Audio API AudioContext which starts
at zero when the page loads and runs continuously, the
Transport can be started, stopped, paused, and looped. It
maintains its own internal timeline and handles tempo (BPM), time
signature, and swing adjustments.
When you schedule an event on the Transport, its
execution time is defined relative to this timeline rather than the
absolute real-world time. If the tempo changes, the scheduled events
automatically speed up or slow down to maintain their correct musical
positions.
Time Formats in Tone.js
To schedule events relative to the transport timeline, Tone.js supports various flexible time formats. These formats are automatically parsed into seconds under the hood:
- Bars:Beats:Sixteenths (BB:B:S): A musical notation
format (e.g.,
"0:2:0"represents 2 beats into the timeline). - Notation: Subdivisions of a bar (e.g.,
"4n"for a quarter note,"8t"for an eighth-note triplet). - Seconds: Numeric values (e.g.,
1.5) represent absolute time in seconds. - Relative Time: Adding a plus sign (e.g.,
"+4n") schedules an event relative to the current transport time.
Low-Level Scheduling Methods
Tone.js provides direct methods on the Transport object
to schedule callbacks at specific times.
Tone.Transport.schedule()
The schedule method triggers a callback at a specific
point on the timeline. It returns a unique ID that can be used to cancel
the event later.
// Schedule a note to play at the start of the second bar
Tone.Transport.schedule((time) => {
synth.triggerAttackRelease("C4", "8n", time);
}, "1:0:0");Tone.Transport.scheduleRepeat()
To create repeating events, such as a metronome click, use
scheduleRepeat. This repeats a callback at a specified
interval.
// Repeat a hi-hat sound every quarter note
Tone.Transport.scheduleRepeat((time) => {
hat.triggerAttackRelease(time);
}, "4n");High-Level Event Classes
For complex musical structures, Tone.js offers specialized classes that wrap the low-level transport timeline. These classes make it easier to manage groups of notes and patterns.
- Tone.Event: A flexible wrapper for a single event or a custom repeating loop. It allows you to adjust playback rate, loop points, and humanization.
- Tone.Part: A collection of events that can be scheduled, started, and stopped as a single unit. It is ideal for rendering entire musical scores or MIDI file data.
- Tone.Sequence: Specifically designed for step sequencers. It takes an array of values and iterates through them at a specified musical interval.
// Example of a Tone.Sequence playing a bassline
const sequence = new Tone.Sequence((time, note) => {
synth.triggerAttackRelease(note, "8n", time);
}, ["C2", "E2", "G2", "B2"], "4n").start(0);The Look-Ahead Scheduler
To achieve perfect musical timing, Tone.js addresses a fundamental limitation of web browsers: JavaScript is single-threaded and prone to UI-induced lag, while the Web Audio API runs on a separate, high-priority system thread.
If Tone.js attempted to play an audio event at the exact millisecond JavaScript executed the callback, UI rendering or user interaction could cause audible stuttering and jitter.
Tone.js solves this by using a look-ahead scheduler.
It looks slightly ahead into the future (typically around 100
milliseconds) and schedules the Web Audio API nodes in advance. The
time parameter passed into your transport callbacks
represents the precise audio clock time when the sound should
play, not the CPU time when the JavaScript function runs. By passing
this time argument directly into your instrument triggers
(e.g., synth.triggerAttackRelease("C4", "8n", time)), you
ensure jitter-free, sample-accurate audio playback regardless of
main-thread performance.