Skip to main content

Export audio to WAV

Bake a slowed-down study track a student can loop in any audio player. Generate audio previews for every song in a catalogue so browsers can hear before they load the notation. Render backing tracks a live performer can trigger from a phone. AlphaTabApi.exportAudio(options) runs the alphaTab synthesizer offline and hands you the raw PCM samples โ€” alphaTab produces the samples, your code encodes them into whatever container you want. This page walks the full pipeline: configure the export, pull chunks, wrap them in a WAV header, download the result.

async function generateWav() {
try {
// setup options
const options = new alphaTab.synth.AudioExportOptions();
options.masterVolume = 0.8;
options.metronomeVolume = 1;
options.trackVolume.set(0, 0.5);
options.sampleRate = 44100;

// start exporter
const exporter = await api!.exportAudio(options);

// collect all chunks in 500ms steps
const chunks: Float32Array[] = [];
try {
while (true) {
const chunk = await exporter.render(500);
if (chunk === undefined) {
break;
}

chunks.push(chunk.samples);
}

} finally {
exporter.destroy();
}

// use the samples.
// in this example: create a wav file and set it as source of the audio element

const wav = convertSamplesToWavBlobUrl(chunks, options.sampleRate);

// <audio id="wav" controls="true" />
document.querySelector<HTMLAudioElement>('#wav')!.src = wav;
}
catch (e) {
console.error('Error during export', e);
alert('Something went wrong during export, check the browser logs for details');
}
}

function convertSamplesToWavBlobUrl(chunks: Float32Array[], sampleRate: number): string {
const samples = chunks.reduce((p, c) => p + c.length, 0);
const wavHeaderSize = 44;
const fileSize = wavHeaderSize + samples * 4;
const buffer = alphaTab.io.ByteBuffer.withCapacity(fileSize);

//
// write wav header

// RIFF chunk
buffer.write(new Uint8Array([0x52, 0x49, 0x46, 0x46]), 0, 4); // RIFF
alphaTab.io.IOHelper.writeInt32LE(buffer, fileSize - 8); // file size
buffer.write(new Uint8Array([0x57, 0x41, 0x56, 0x45]), 0, 4); // WAVE

// format chunk
buffer.write(new Uint8Array([0x66, 0x6D, 0x74, 0x20]), 0, 4); // fmtโฃ
alphaTab.io.IOHelper.writeInt32LE(buffer, 16); // block size
alphaTab.io.IOHelper.writeInt16LE(buffer, 3); // audio format (1=WAVE_FORMAT_IEEE_FLOAT)
const channels = 2;
alphaTab.io.IOHelper.writeInt16LE(buffer, channels); // number of channels
alphaTab.io.IOHelper.writeInt32LE(buffer, sampleRate); // sample rate
alphaTab.io.IOHelper.writeInt32LE(buffer, Float32Array.BYTES_PER_ELEMENT * channels * sampleRate); // bytes/second
const bitsPerSample = Float32Array.BYTES_PER_ELEMENT * 8;
alphaTab.io.IOHelper.writeInt16LE(buffer, channels * Math.floor((bitsPerSample + 7) / 8)); // block align
alphaTab.io.IOHelper.writeInt16LE(buffer, bitsPerSample); // bits per sample

// data chunk
buffer.write(new Uint8Array([0x64, 0x61, 0x74, 0x61]), 0, 4); // data
alphaTab.io.IOHelper.writeInt32LE(buffer, samples * 4);
for (const c of chunks) {
const bytes = new Uint8Array(c.buffer, c.byteOffset, c.byteLength);
buffer.write(bytes, 0, bytes.length);
}

const blob: Blob = new Blob([
buffer.toArray()
], {
type: 'audio/wav'
});
return URL.createObjectURL(blob);
}

The demo synthesizes the full score, wraps the samples in a WAV file, and pipes the resulting blob URL into an <audio> element. Read the source under src/components/AudioExportSample/ for the complete implementation.

Pull-based renderingโ€‹

exportAudio returns an IAudioExporter. The exporter follows an asynchronous pull pattern โ€” you call render(milliseconds) in a loop and receive one AudioExportChunk per call. Each chunk carries a stereo interleaved Float32Array at the sample rate you configured. When the song ends, render returns undefined.

import * as alphaTab from '@coderline/alphatab';

const container = getAlphaTabContainer();
const api = new alphaTab.AlphaTabApi(container, {
core: { file: '/scores/song.gp' },
player: { playerMode: alphaTab.PlayerMode.EnabledSynthesizer },
});

api.scoreLoaded.on(async () => {
const options = new alphaTab.synth.AudioExportOptions();
options.sampleRate = 44100;
options.masterVolume = 1;

const exporter = await api.exportAudio(options);
const chunks = [];
try {
while (true) {
const chunk = await exporter.render(500);
if (chunk === undefined) break;
chunks.push(chunk.samples);
}
} finally {
exporter.destroy();
}

const wavUrl = encodeWav(chunks, options.sampleRate);
triggerDownload(wavUrl, `${api.score.title || 'Untitled'}.wav`);
});

getAlphaTabContainer() is the platform-specific render target. encodeWav and triggerDownload are your code โ€” the samples are format-agnostic.

Options that shape the exportโ€‹

AudioExportOptions mirrors the runtime mixer, but with export-specific defaults:

  • soundFonts โ€” SF2 buffers to load into the offline synth. If omitted, the exporter reuses whatever the current synthesizer already has loaded.
  • sampleRate โ€” output rate in Hz. Defaults to 44100.
  • masterVolume, metronomeVolume โ€” mix knobs applied to the offline synth. Metronome defaults to 0; enable it to bake the click into the render.
  • playbackRange โ€” limit the export to a tick range. Omit for the whole song.
  • trackVolume, trackTranspositionPitches โ€” per-track mix and pitch shift, keyed by track index. Applied on top of the score's own settings.
  • useSyncPoints โ€” when true, sync-point timing is respected. Turn off for the score's original tempo map.

Encode the chunks to WAVโ€‹

WAV wants a 44-byte header followed by the raw samples. alphaTab emits IEEE-float samples in stereo interleaved order, so the format tag is 3 (WAVE_FORMAT_IEEE_FLOAT) and each sample stays 32 bits. The <AudioExportSample> component above lists the full encoder โ€” copy the createWavSampleUrl function into your project.

Hand the chunks to a codec library (LAME, OggEncoder, Opus) instead when you want MP3, OGG, or Opus output. A 4-minute stereo song at 44.1 kHz is roughly 84 MB of float PCM, so encode as chunks arrive rather than buffering the whole song.

Where to look nextโ€‹