Skip to main content

Combining multiple SoundFonts

Pair a rock drum kit with an acoustic guitar pack. Layer a percussion library over a melodic-instrument SoundFont. Pull the horns file only when the loaded score has a brass track. Any of these is a case for loading several SoundFonts side by side โ€” alphaTab merges their preset banks into a single synth and you spend bandwidth only on the instruments the current score actually needs.

00:00 / 00:00
bundled Sonivox active

Start playback, then click Append โ€” the second SoundFont merges its presets on top without dropping the first. loadSoundFont(url, /* append */ true) always layers instead of replacing.

The in-repo demo layers GeneralUser GS (about 9 MB as compressed SF3, richer piano and guitar samples) on top of the bundled Sonivox. Because both banks define the same GM programs, the later file wins the collision on every program and you hear the whole score re-voiced with GeneralUser's presets. That is the point of the mechanism, not a limitation of it โ€” a real app would layer disjoint SoundFonts (guitars-only atop drums-only) so appending genuinely widens the palette instead of overriding existing presets.

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

const container = getAlphaTabContainer();
const api = new alphaTab.AlphaTabApi(container, {
player: { playerMode: alphaTab.PlayerMode.EnabledSynthesizer },
});

// First append โ€” merges into the bundled Sonivox presets already loaded.
const guitarBytes = await readScoreFile('/assets/guitars.sf2');
api.loadSoundFont(guitarBytes, /* append */ true);

// Second append โ€” layers a drums-only SoundFont on top.
// Preset conflicts resolve in load order; later wins.
const drumBytes = await readScoreFile('/assets/drums.sf2');
api.loadSoundFont(drumBytes, true);

api.soundFontLoaded.on(() => {
console.log('Layer ready.');
});

The append flagโ€‹

api.loadSoundFont(data, append) takes a second boolean argument. When it is false (the default) alphaTab replaces the currently loaded presets. When it is true, alphaTab merges the new file's presets on top of the existing ones. Preset conflicts resolve in load order โ€” the later file wins.

Layering shines when your instrument palette exceeds any single SoundFont, or when you want a per-track SoundFont strategy without shipping one giant file.

Slice the bank firstโ€‹

The biggest win from layering is bandwidth: users only download the presets they need for the current score. Slice a monolithic SoundFont into targeted files with Polyphone:

  1. Copy the SoundFont to two files, one per instrument family (for example guitars.sf2 and orchestra.sf2).
  2. Open each copy and delete the presets that do not belong. Polyphone can trim unused instruments and samples automatically.
  3. Save each file. Combined size stays close to the original; per-file size drops dramatically.

Show it in codeโ€‹

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,
},
});

const loaded = new Set();

async function ensureSoundFont(url) {
if (loaded.has(url)) return;
loaded.add(url);
const bytes = await readScoreFile(url);
api.loadSoundFont(bytes, /* append */ true);
}

// After the score arrives, inspect the tracks and pull only the SoundFonts you need.
api.scoreLoaded.on((score) => {
for (const track of score.tracks) {
const program = track.playbackInfo.program;
const isDrums = track.playbackInfo.primaryChannel === 9;
const isGuitar = program >= 24 && program <= 31;
if (isDrums || isGuitar) {
ensureSoundFont('/assets/guitars-and-drums.sf2');
} else {
ensureSoundFont('/assets/others.sf2');
}
}
});

// Drop the whole stack later to free memory before loading a different set.
function reset() {
loaded.clear();
api.resetSoundFonts();
}

getAlphaTabContainer() is a placeholder for the platform-specific step of obtaining the render target โ€” a <div> on Web, an AlphaTab control on .NET, or an AlphaTabView on Android.

readScoreFile() is a placeholder for the platform-specific step of producing a Uint8Array โ€” fs.promises.readFile on Node.js, System.IO.File.ReadAllBytes on .NET, ContentResolver.openInputStream on Android, or fetch(...).then(r => r.arrayBuffer()) in the browser. Track download progress there if the UI needs a loading bar.

Memory hygieneโ€‹

Every appended SoundFont stays resident in the synth. On long-running sessions where the user browses many scores, call api.resetSoundFonts() between loads to reclaim RAM, then re-append only what the next score needs. soundFontLoaded fires on each successful call, so hook it if you want to gate playback until every layer is ready.

Where to look nextโ€‹