npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gwegash/sf2-player

v0.1.0

Published

Sample-accurate SoundFont 2 (SF2) player for the Web Audio API

Readme

sf2-player

A sample-accurate SoundFont 2 player for the Web Audio API, in TypeScript.

It parses a .sf2 file, exposes the instruments inside it, and turns note events into Web Audio nodes. Notes are scheduled on the audio clock, not with setTimeout, so timing is exact to the frame.

  • No dependencies, no AudioWorklet, no eval. Plain AudioBufferSourceNodes and AudioParam automation.
  • Ships as an ES module with type declarations, bundled with esbuild.
  • Works with any BaseAudioContext, including OfflineAudioContext for rendering faster than real time.

Install

npm install @gwegash/sf2-player

Use

The whole API is one factory function and the object it returns.

import { createSF2Player } from '@gwegash/sf2-player'

const context = new AudioContext()
const player = await createSF2Player(context, '/fonts/piano.sf2')

player.output.connect(context.destination)
player.selectProgram(0)

// Schedule against the audio clock: both notes land on an exact frame.
player.noteOn(60, 100, context.currentTime + 0.5)
player.noteOff(60, context.currentTime + 1.5)

createSF2Player accepts a URL string, a URL, an ArrayBuffer, or any typed array — so a file from an <input type="file"> works directly:

const player = await createSF2Player(context, await file.arrayBuffer())

Choosing an instrument

programs lists every preset in the file, sorted by bank then program number. selectProgram takes an index into that list.

for (const p of player.programs) {
  console.log(`${p.index}: ${p.bank}:${p.program} ${p.name}`)
}
player.selectProgram(12)

Switching programs affects notes started afterwards. Notes already sounding keep the program they began with.

API

| Member | Description | | --- | --- | | output: AudioNode | Connect this into your graph. | | programs: readonly SF2Program[] | Every preset: { index, name, bank, program }. | | program: SF2Program | The program new notes will use. | | info: ReadonlyMap<string, string> | INFO metadata, e.g. INAM, ICOP. | | selectProgram(index) | Switch programs. Throws RangeError if out of range. | | noteOn(note, velocity?, time?) | Start a note. velocity defaults to 100, time to now. | | noteOff(note, time?) | Release a note. A note that is not sounding is ignored. | | allNotesOff(time?) | Release everything. | | dispose() | Silence, disconnect, and free buffers. |

time is always a value on the AudioContext clock, the same units as context.currentTime. A time in the past means "now".

Rendering offline

Because the player only needs a BaseAudioContext, you can render a passage without a sound card — which is also how this library tests itself.

const context = new OfflineAudioContext(2, 44100 * 4, 44100)
const player = await createSF2Player(context, fontBytes)
player.output.connect(context.destination)
player.noteOn(60, 100, 0)
player.noteOff(60, 2)
const rendered = await context.startRendering()

What is implemented

Sample playback with loop modes, key and velocity zone selection, layered regions, preset generators applied over instrument generators, root key and coarse/fine tuning, scale tuning, volume envelope (delay/attack/hold/decay/ sustain/release, with key-number scaling of hold and decay), initial attenuation, velocity response, pan, a low-pass filter, sample address offsets, and exclusive classes for choke groups such as hi-hats.

Plus the modulation layer:

  • Modulation envelope to filter cutoff and to pitch, with its own delay/attack/hold/decay/sustain/release.
  • Modulation LFO to pitch, cutoff and volume, and vibrato LFO to pitch, both with their own rate and delay.
  • Note-on modulators — the SF2 modulator system restricted to sources known when a voice starts (velocity, key number), with all four transfer curves, direction, polarity and the absolute-value transform.

Pitch and cutoff modulation ride on AudioBufferSourceNode.detune and BiquadFilterNode.detune, which Web Audio already expresses in cents — the same unit SF2 uses. A voice only builds the nodes for routings its region actually uses, so an unmodulated patch costs exactly what it did before.

What is not implemented, and why

  • Controller-driven modulators (mod wheel, aftertouch, pitch wheel, CC pan and effect sends). These need a live MIDI controller feed, which this player does not expose. Surveying real fonts, the ones they ship are inert at rest anyway — sitting at zero amount, or with a resting controller value that maps to zero — so notes sound correct without them.
  • Chorus and reverb sends. The spec names the sends but never defines the effect, so any implementation would be an invention. Better placed in the host graph, which is why output is a plain AudioNode.
  • sm24 24-bit sample chunks, ignored, so such files play at 16-bit depth.

Those choices came out of measuring six real sound fonts (587 presets, 3,721 regions) rather than guessing. Of 2,641 modulators found, all but 312 were inert; the 312 were a single routing, velocity to filter cutoff, which is implemented. Stereo needs no special handling either: every stereo font surveyed supplies both halves as separate regions panned hard left and right, so they already play correctly.

Two details worth knowing:

  • Velocity maps to gain as (velocity / 127)². That is not an approximation — it is the spec's default velocity-to-attenuation modulator (960 cB, concave, decreasing) written out, and the test suite checks the two agree.
  • initialFilterFc at or above its default of 13500 cents means the filter is bypassed entirely, rather than instantiated next to Nyquist where it would ring. A filter is still built when something modulates the cutoff.

Example

npm install
npm run build
npm run serve      # then open http://localhost:5173

The demo page loads a .sf2 of your choosing (or a small built-in font), lists its instruments, and gives you a keyboard. The "timed arpeggio" button schedules twelve notes in advance to show that the rhythm does not depend on the main thread staying free.

Tests

npm test          # typecheck, unit tests, then the browser suite
npm run test:unit # vitest: parsing, generator maths, zone flattening
npm run test:e2e  # playwright: real audio rendered in a real browser

The browser suite renders through an OfflineAudioContext and asserts on the samples that come out: that a note begins on exactly the frame it was scheduled for, that two notes a frame apart stay a frame apart, that pitch tracks the key, that velocity scales level, that release tails decay and stop, that the LFO swings pitch either side of the note, and that the modulation envelope sweeps the filter open.

It runs against a sound font the test suite writes itself, so the expected output is known exactly. That matters most for the modulation tests: the fixture's samples hold a constant timbre, so any change in brightness has to come from the filter. The same measurement on a real font proves nothing, because a real instrument sample dulls as it decays all by itself.

A font written by this repo cannot catch a misreading of the format shared by its reader and writer, so the parser is also checked against real files:

SF2_FONT=/path/to/font.sf2 npm run test:unit

Development

Requires Node 20 or newer (see .nvmrc).

nvm use
npm install
npx playwright install chromium
npm test

Licence

MIT