SoundFont and Sampled Instruments
A sampled instrument makes sound the opposite way from a synthesizer: instead of generating a waveform, it plays back short recordings of a real instrument. Record a piano playing several notes at several volumes, store those clips, and play the closest one back at the right pitch when a key is pressed — that is sampling in a nutshell. One recording is not enough because playing it faster raises its pitch but also shortens it and slides its formants up (the "chipmunk" effect), so libraries record every few semitones and stretch each sample only a little. The result can be extremely realistic, because it is a real recording.
This page explains what a SoundFont is, how it is addressed, and how libsonare guarantees that a MIDI arrangement always produces sound. It is concepts only — no code.
Sampled vs synthesized
A synthesized instrument computes its sound and can be any pitch or tone, but has to be designed. A sampled instrument replays recordings and sounds true to life, but is fixed to what was recorded and takes far more storage. Many setups use both — samples for realistic instruments, synths for sounds that do not exist acoustically.
What a SoundFont is
A SoundFont (the .sf2 file format) is a single file that bundles a whole library of recorded instrument samples, together with the rules for how to play them back (which sample covers which key range, how loops work, basic envelopes). One .sf2 can hold an entire General MIDI instrument set in a few megabytes.
Inside the file the sounds are organized into banks and programs, and any one instrument is addressed by a (bank, program) pair:
- A program is one playable instrument — "acoustic grand piano", "fingered bass".
- A bank is a numbered shelf of up to 128 programs. Bank 0 holds the main set; higher banks hold variations.
This is exactly the addressing scheme MIDI uses for Program Change and bank select, which is why MIDI and SoundFonts fit together so naturally.
General MIDI program numbers and the drum bank
Because a SoundFont is addressed by (bank, program), a General MIDI-compliant .sf2 lines its programs up with the GM map: program 0 is an acoustic grand piano, 24 a nylon-string guitar, 40 a violin, and so on across all 128 GM instruments. Percussion lives in a separate drum bank, where each note number selects a different drum or cymbal rather than a pitch — matching MIDI's channel-10 drum convention.
The principle underneath is simple: a note list is instrument-agnostic, and the address decides which instrument performs it. The piano roll below makes that tangible — the notes never change; switching the instrument points the same MIDI at a different sound.
Inside a preset: zones, generators and modulators
A preset is not one recording. It is a set of zones, each covering a key range and a velocity range and pointing at one sample (through an intermediate instrument layer, so one instrument can be reused by several presets). Each zone carries generators, fixed settings such as the sample's root key, its loop points, the filter cutoff and the envelope times. Modulators connect a live input (velocity, a CC, pitch bend) to a generator, so that playing harder lowers the attenuation or the mod wheel deepens the vibrato. The spec defines a default modulator set that every player applies even when the file lists none.
Generators are stored in logarithmic units, because hearing is logarithmic:
| Unit | Used for | Conversion |
|---|---|---|
| timecent | envelope and LFO-delay times | seconds = 2^(timecents / 1200); −12000 is about 1 ms, 0 is 1 s, 1200 is 2 s |
| centibel | attenuation, sustain level | 1 cB = 0.1 dB, so 960 cB is 96 dB down |
| absolute cent | filter cutoff, LFO rate | Hz = 8.176 × 2^(cents / 1200); 13500 cents is about 20 kHz |
With these units every doubling of a time, or every equal step in level, is the same distance on the number line, which is why a file can scale an envelope with key number or velocity by simple addition.
How libsonare resolves a note
libsonare can load a SoundFont and play MIDI through it, but it adds one important guarantee. After you supply a .sf2 file, every used (channel, bank, program) group is summarized by one backend status:
| Situation | Backend | What you hear |
|---|---|---|
Every played note in the (channel, bank, program) group is covered by the loaded SoundFont's zones | 'sf2' | SF2 samples for those notes |
| At least one played note is uncovered, or no SoundFont is loaded | 'synth' | NativeSynth General MIDI fallback for uncovered notes |
The key consequence: an uncovered MIDI note does not become silent solely because a SoundFont lacks a zone. libsonare falls back to its built-in NativeSynth GM bank for each such note. The manifest is conservative and group-level: if any played note in a (channel, bank, program) group lacks an SF2 zone, that group's status is 'synth', even when other notes in the same group use SF2.
Why the fallback matters
Different SoundFonts cover different instruments, and a small .sf2 might only include a handful of programs. The fallback means you can hand libsonare any arrangement and any SoundFont (or none) and still hear the complete piece — then swap in a richer SoundFont later to upgrade the sounds, with no change to the MIDI.
You can inspect exactly what resolved where: soundFontManifest() reports one status for each (channel, bank, program) group the arrangement plays. The backend summarizes all notes in that group and is 'synth' when at least one note uses fallback.
How libsonare implements this
On a Project, loadSoundFont(bytes) registers an .sf2 file from a byte buffer. soundFontManifest() then returns one Sf2ProgramStatus per (channel, bank, program) combination the arrangement uses, each with a backend of 'sf2' or 'synth' and the resolved presetName (drum channels report bank 128). The manifest aggregates all note-ons in each combination: any note that escapes the SoundFont's zones changes that entry to 'synth'; this does not mean every note in the group used NativeSynth. bounceWithSf2Instrument(...) applies the same GM fallback per note, so uncovered notes still render. Even with no SoundFont loaded, every note resolves to a NativeSynth voice.
Related: SoundFont Player, Built-in Synthesizer (NativeSynth), MIDI Basics