Rendering and export
You can bounce a SongDefinition to PCM faster than real time, then encode a WAV file for download or packaging. Offline rendering uses Tone.js’s Offline context via renderSong; it calls loadTone() but does not require ensureAudio() or a user gesture—offline contexts are not subject to autoplay rules.
Render to AudioBuffer
import { renderSong, type SongDefinition } from '@zylem/game-lib/audio';
const buffer = await renderSong(mySong, {
tail: 2, // seconds after the last note for reverb/release
sampleRate: 44100,
resolveUrl: (url) => `/assets/${url}`,
trackIds: ['drums', 'bass'], // optional subset; ignores mute/solo on those tracks
});
renderSong:
- Loads Tone.js on demand (same lazy import as playback).
- Schedules notes and audio clips with
scheduleSong. - Builds the instrument graph with
buildSongGraph. - Returns a Web Audio
AudioBuffer.
Encode WAV
Pair the buffer with the lightweight encoder in the same module:
import { encodeWav, encodeWavBlob } from '@zylem/game-lib/audio';
const bytes = encodeWav(buffer); // ArrayBuffer (16-bit PCM RIFF)
const file = encodeWavBlob(buffer); // Blob for download links
encodeWav accepts any PcmSource (channel count, sample rate, getChannelData), so you can wrap third-party render output without a live AudioContext.
When to use offline render vs live player
| Approach | Use when |
|---|---|
createSongPlayer | In-game music, preview in Creator, transport events |
renderSong | Export assets, CI snapshots, thumbnails, server-side tooling in the browser |
Live playback still needs ensureAudio() on a user gesture; export does not.
Pitfalls
- Tail length — default tail is 1.5 s; long reverbs may need a larger
tailor the bounce will clip early. - Subset renders — passing
trackIdsforces those tracks unmuted for the bounce even if they are muted in the definition. - URL resolution — audio clips must load successfully; unresolved URLs fail the render promise.