How to Render Audio Offline with Tone.js
This article explains how to use the Tone.Offline
utility in Tone.js to render a musical composition directly to an audio
buffer instead of playing it through the speakers. You will learn how to
configure the offline environment, schedule synthesizers and effects,
and export the final rendered audio as a downloadable WAV file.
Understanding Tone.Offline
Normally, Tone.js synthesizes audio in real-time. However, when you
want to export a song or a sound effect to a file, you need to render it
“offline.” Tone.Offline runs the audio synthesis engine at
maximum CPU speed without hardware latency, rendering minutes of audio
in just a few seconds.
The basic syntax for Tone.Offline is:
Tone.Offline((context) => {
// Set up instruments, effects, and scheduling here
}, durationInSeconds).then((buffer) => {
// The rendered audio buffer is available here
});Step-by-Step Implementation
1. Set Up the Offline Renderer
To render audio, call Tone.Offline and pass it a
callback function and a duration (in seconds). Inside the callback, you
must define all the instruments, effects, and notes you want to
render.
const duration = 4; // Render a 4-second audio file
Tone.Offline(() => {
// Instantiate an instrument inside the offline context
const synth = new Tone.Synth().toDestination();
// Schedule notes to play
synth.triggerAttackRelease("C4", "8n", 0);
synth.triggerAttackRelease("E4", "8n", 0.5);
synth.triggerAttackRelease("G4", "8n", 1.0);
synth.triggerAttackRelease("C5", "2n", 1.5);
}, duration).then((buffer) => {
// Handle the rendered buffer
console.log("Rendering complete!");
exportToWav(buffer);
});Note: You must instantiate your instruments and effects inside the callback function so they are bound to the temporary offline audio context rather than the global online context.
2. Convert the AudioBuffer to a WAV File
The buffer returned by Tone.Offline is a
ToneAudioBuffer (which wraps a standard Web Audio
AudioBuffer). To save this as an actual audio file (like a
.wav file) that a user can download, you must encode the
buffer data.
Here is a helper function to convert an AudioBuffer into
a standard WAV file format:
function bufferToWav(buffer) {
const numOfChan = buffer.numberOfChannels;
const length = buffer.length * numOfChan * 2 + 44;
const bufferArr = new ArrayBuffer(length);
const view = new DataView(bufferArr);
const channels = [];
let pos = 0;
let offset = 0;
// Write WAV header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // chunk length
setUint16(1); // sample format (raw PCM)
setUint16(numOfChan);
setUint32(buffer.sampleRate);
setUint32(buffer.sampleRate * 2 * numOfChan); // byte rate
setUint16(numOfChan * 2); // block align
setUint16(16); // bits per sample
setUint32(0x61746164); // "data" chunk
setUint32(length - pos - 4); // chunk length
// Write interleaved audio data
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
while (pos < length) {
for (let i = 0; i < numOfChan; i++) {
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}
return new Blob([bufferArr], { type: "audio/wav" });
function setUint16(data) {
view.setUint16(pos, data, true);
pos += 2;
}
function setUint32(data) {
view.setUint32(pos, data, true);
pos += 4;
}
}3. Trigger the Download in the Browser
Once you have encoded the buffer into a WAV blob, you can generate an object URL and trigger an automatic browser download.
function exportToWav(toneBuffer) {
// Get the raw AudioBuffer from ToneAudioBuffer
const audioBuffer = toneBuffer.get();
// Convert to WAV Blob
const wavBlob = bufferToWav(audioBuffer);
// Create a download link and trigger it
const url = URL.createObjectURL(wavBlob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = "composition.wav";
anchor.click();
// Clean up the URL object
URL.revokeObjectURL(url);
}By combining these steps, your application can render complex musical compositions entirely in the background and output production-ready audio files instantly.