smplris a collection of sampled instruments for Web Audio API ready to be used with no setup required.
Play a note from a General MIDI soundfont:
import { Soundfont } from "smplr";
const context = new AudioContext();
const marimba = Soundfont(context, { instrument: "marimba" });
marimba.start({ note: 60, velocity: 80 });Sequence a beat with a drum machine and a piano on the same clock:
import { Sequencer, SplendidGrandPiano, DrumMachine } from "smplr";
const context = new AudioContext();
const piano = SplendidGrandPiano(context);
const drums = DrumMachine(context, { instrument: "TR-808" });
const seq = Sequencer(context, { bpm: 110, loop: true });
seq.addTrack(piano, [
{ note: "C4", at: "1:1", duration: "4n" },
{ note: "E4", at: "1:2", duration: "4n" },
{ note: "G4", at: "1:3", duration: "4n" },
]);
seq.addTrack(drums, [
{ note: "kick", at: "1:1" },
{ note: "snare", at: "1:2" },
{ note: "kick", at: "1:3" },
{ note: "snare", at: "1:4" },
]);
seq.start();Render an arpeggio with reverb to a WAV file — offline, no speakers needed:
import { SplendidGrandPiano, Reverb, renderOffline } from "smplr";
const wav = await renderOffline(async (context) => {
const piano = await SplendidGrandPiano(context).load;
piano.output.addEffect("reverb", Reverb(context), 0.3);
["C4", "E4", "G4", "C5"].forEach((note, i) => {
piano.start({ note, time: i * 0.4, duration: 0.4 });
});
});
wav.downloadWav("arpeggio.wav");See demo: https://danigb.github.io/smplr/
- No setup: specifically, all samples are online, so no need for a server.
- Easy to use: everything should be intuitive for non-experienced developers
- Decent sounding: uses high quality open source samples. For better or worse, it is sample based 🤷
You can install the library with a package manager or use it directly by importing from the browser.
Samples are stored at https://github.com/smpldsnds and there is no need to download them. Kudos to all samplerist 🙌
Install with npm or your favourite package manager:
npm i smplr
You can import directly from the browser. For example:
<html>
<body>
<button id="btn">play</button>
</body>
<script type="module">
import { SplendidGrandPiano } from "https://unpkg.com/smplr/dist/index.mjs"; // needs to be a url
const context = new AudioContext(); // create the audio context
const piano = SplendidGrandPiano(context); // create and load the instrument
document.getElementById("btn").onclick = () => {
context.resume(); // enable audio context after a user interaction
piano.start({ note: 60, velocity: 80 }); // play the note
};
</script>
</html>The package needs to be served as a URL from a service like unpkg or similar.
smplr ships eleven instruments out of the box. Pick one and jump to its section in the Instrument reference for setup details.
| Instrument | Description | Names helper |
|---|---|---|
Sampler |
Your own buffers or SFZ-style preset | — |
Soundfont |
General MIDI soundfonts | getSoundfontNames() |
SplendidGrandPiano |
Sampled Steinway grand, 4 velocity layers | — |
ElectricPiano |
CP80, PianetT, Wurlitzer, TX81Z | getElectricPianoNames() |
DrumMachine |
Classic drum machines (TR-808, …) | getDrumMachineNames() |
DrumAbuse |
~210 machines (Synthabuse collection) | getDrumAbuseMachineNames() |
Mallet |
VCSL mallets | getMalletNames() |
Mellotron |
Mellotron archive samples | getMellotronNames() |
Smolken |
Smolken double bass (Arco/Pizz/Switched) | getSmolkenNames() |
Versilian |
VCSL multi-instrument (partial support) | getVersilianInstruments() |
Soundfont2 |
Reads .sf2 files directly | — |
Each names helper returns strings to pass as the factory's instrument option. getVersilianInstruments is async (the catalog is fetched once and cached).
To build your own instrument, see Defining your own instrument.
The shared API below applies to every instrument. Instrument-specific options live in the Instrument reference.
Every smplr instrument is a factory function: call it with an AudioContext and an options object to get back an instance.
import { SplendidGrandPiano, Soundfont } from "smplr";
const context = new AudioContext();
const piano = SplendidGrandPiano(context, { decayTime: 0.5 });
const marimba = Soundfont(context, { instrument: "marimba" });You can start playing notes as soon as one sample is loaded. To wait for all of them, await either:
piano.ready— resolves tovoid(preferred for new code).piano.load— resolves to the instrument itself, so you can create and await in one line:
const piano = await SplendidGrandPiano(context).load;Upgrading from older versions? See MIGRATE.md.
Track how many samples have loaded via the onLoadProgress option or the loadProgress getter:
const piano = SplendidGrandPiano(context, {
onLoadProgress: ({ loaded, total }) => {
console.log(`${loaded} / ${total} samples loaded`);
},
});
// Or poll at any time:
console.log(piano.loadProgress); // { loaded: 12, total: 48 }total is known before loading starts, so you can display a determinate progress bar.
All instruments share some configuration options, passed as the second argument to the factory. Every field is optional:
volume: a number from 0 to 127 representing the instrument's global volume. 100 by default.velocity: default note velocity (0–127) when not specified per note. 100 by default.pan: stereo pan, -1 (full left) to +1 (full right). 0 by default.destination: theAudioNodethe instrument writes to.AudioContext.destinationby default.volumeToGain: a function to map MIDI volume to a linear gain. Uses the MIDI standard curve by default.storage: a storage backend used to fetch sample buffers.HttpStorageby default.loader: a sharedSampleLoaderinstance. Pass the same loader to multiple instruments to cache buffers across them (see Buffer reuse).scheduler: a sharedSchedulerinstance. Construct your own to tune scheduling — for example,Scheduler(context, { lookaheadMs: 100, intervalMs: 25 })— or omit to get a per-instrument default.onLoadProgress: a function called after each sample buffer is decoded. Receives{ loaded, total }wheretotalis the full count known before loading starts.onStart: called when a note is dispatched to the audio engine. Receives the started note. See⚠️ note under Events on timing precision.onEnded: called when each voice's audio node ends. Receives the started note.
The start function accepts these options:
piano.start({ note: "C4", velocity: 80, time: 5, duration: 1 });velocity (0–127) represents how hard the key is pressed: louder at higher values, and on some instruments it also changes timbre.
The start function returns a stop function for the given note:
const stopNote = piano.start({ note: 60 });
stopNote({ time: 10 });Bear in mind that you may need to call context.resume() before playing a note
Instruments have a global stop function that can be used to stop all notes:
// This will stop all notes
piano.stop();Or stop the specified one. The argument is a stopId — by default the same value you passed as note, but you can override it via start({ note, stopId }):
piano.stop("C4"); // stop the note(s) started with `note: "C4"`
piano.stop(60); // stop the note(s) started with `note: 60`Schedule notes via the time and duration properties (both in seconds). time is measured against audioContext.currentTime.
This plays a C major arpeggio, one note per second:
const now = context.currentTime;
["C4", "E4", "G4", "C5"].forEach((note, i) => {
piano.start({ note, time: now + i, duration: 0.5 });
});You can loop a note by using loop, loopStart and loopEnd:
const context = new AudioContext();
const sampler = Sampler(context, {
buffers: { duh: "https://example.com/duh-duh-ah.mp3" },
});
sampler.start({
note: "duh",
loop: true,
loopStart: 1.0,
loopEnd: 9.0,
});If loop is true but loopStart or loopEnd are not specified, 0 and total duration will be used by default, respectively.
Instrument output attribute represents the main output of the instrument. The output.volume getter/setter accepts a number where 0 means no volume, and 127 is max volume without amplification:
piano.output.volume = 80;
piano.output.volume; // => 80volume is global to the instrument, but velocity is specific for each note.
Every instrument accepts a pan option at construction (-1 = full left, +1 = full right):
const drums = DrumMachine(context, { instrument: "TR-808", pan: -0.5 });Two universal setters mutate the playback defaults in place. They apply to notes scheduled after the call; in-flight notes are unaffected.
sampler.setDetune(100); // semitone up (100 cents) for all future notes
sampler.setReverse(true); // play samples reversed for all future notes
sampler.setReverse(false); // back to forward playbackSet and read MIDI Control Change values on the instrument:
piano.setCC(64, 127); // sustain pedal on
piano.getCC(64); // => 127
piano.setCC(64, 0); // sustain pedal offUnset CCs default to 0 (matches MIDI's "undefined controller defaults to 0" convention).
A packaged version of the DattorroReverbNode algorithmic reverb is included.
Use output.addEffect(name, effect, mix) to connect an effect using a send bus:
import { Reverb, SplendidGrandPiano } from "smplr";
const reverb = Reverb(context);
const piano = SplendidGrandPiano(context, { volume });
piano.output.addEffect("reverb", reverb, 0.2);To change the mix level, use output.setEffectMix(name, mix):
piano.output.setEffectMix("reverb", 0.5);Two events are available: onStart and onEnded. Both callbacks receive the started note as a parameter, and can be configured globally:
const context = new AudioContext();
const piano = SplendidGrandPiano(context, {
onStart: (note) => {
console.log(note.time, context.currentTime);
},
});or per note basis:
piano.start({
note: "C4",
duration: 1,
onEnded: () => {
// will be called after 1 second
},
});Global callbacks will be invoked regardless of whether local events are defined.
onStart is not exact: it fires slightly before the audio actually starts, by up to the scheduler's lookahead window (200ms by default; configurable via the scheduler option — see Shared configuration options).
When you're done with an instrument, call dispose() to stop all voices, tear down the audio graph, and stop the scheduler. The instance must not be used after this call.
useEffect(() => {
const piano = SplendidGrandPiano(context);
return () => piano.dispose();
}, []);The default sample sets are hosted on GitHub Pages, which rate-limits requests per second. That can be a problem, especially in a development environment with hot reload (most React frameworks).
To cache samples in the browser, use a CacheStorage object:
import { SplendidGrandPiano, CacheStorage } from "smplr";
const context = new AudioContext();
const storage = CacheStorage();
// First time the instrument loads, will fetch the samples from http. Subsequent times from cache.
const piano = SplendidGrandPiano(context, { storage });CacheStorage is based on the Cache API and only works in secure environments that run over https. Check your framework's documentation for local-HTTPS setup — for example next-dev-https for Next.js or vite-plugin-mkcert for Vite.
This package should be compatible with standardized-audio-context:
import { AudioContext } from "standardized-audio-context";
const context = new AudioContext();
const piano = SplendidGrandPiano(context);However, if you are using Typescript, you might need to "force cast" the types:
import { Soundfont } from "smplr";
import { AudioContext as StandardizedAudioContext } from "standardized-audio-context";
const context = new StandardizedAudioContext() as unknown as AudioContext;
const marimba = Soundfont(context, { instrument: "marimba" });If you use Reverb (or anything else that needs AudioWorkletNode), force the standardized-audio-context version:
import {
AudioWorkletNode,
IAudioContext,
AudioContext as StandardizedAudioContext,
} from "standardized-audio-context";
window.AudioWorkletNode = AudioWorkletNode as any;
const context = new StandardizedAudioContext() as unknown as AudioContext;
// ... rest of the codeSee standardized-audio-context issue #897 for background on why the cast is required.
Sequencer schedules notes from one or more tracks against any smplr instrument with sample-accurate timing.
import { Sequencer, SplendidGrandPiano, DrumMachine } from "smplr";
const context = new AudioContext();
const piano = SplendidGrandPiano(context);
const drums = DrumMachine(context, { instrument: "TR-808" });
const seq = Sequencer(context, { bpm: 120, loop: true });
seq.addTrack(piano, [
{ note: "C4", at: "1:1", duration: "4n" },
{ note: "E4", at: "1:2", duration: "4n" },
{ note: "G4", at: "1:3", duration: "4n" },
{ note: "C5", at: "1:4", duration: "2n" },
]);
seq.addTrack(drums, [
{ note: "kick", at: "1:1" },
{ note: "snare", at: "1:2" },
{ note: "kick", at: "1:3" },
{ note: "snare", at: "1:4" },
]);
seq.loopEnd = "2:1"; // 1 bar
seq.start();Note positions and durations accept several formats:
| Format | Meaning |
|---|---|
"4n" |
quarter note |
"8n" |
eighth note |
"4n." |
dotted quarter (1.5×) |
"1m" |
one measure |
"2:1" |
bar 2, beat 1 (1-indexed) |
"2:3:48" |
bar 2, beat 3, +48 ticks |
96 |
raw ticks (number passthrough) |
const seq = Sequencer(context, {
bpm: 120, // default 120
ppq: 480, // pulses per quarter note, default 480
timeSignature: 4, // accepts `4` (→ 4/4) or `{ numerator, denominator }`
loop: false, // default false
loopStart: 0, // loop start position (ticks or string)
loopEnd: "2:1", // loop end position; defaults to end of longest track
lookaheadMs: 200, // scheduling lookahead, default 200
intervalMs: 50, // flush interval, default 50
humanize: { timingMs: 10, velocity: 8 }, // optional randomisation
stepSize: "16n", // optional: emit "step" events at this interval
});timeSignature accepts a plain number (interpreted as { numerator: n, denominator: 4 }) or a full object such as { numerator: 7, denominator: 8 } for 7/8 time. The seq.timeSignature getter always returns the { numerator, denominator } form.
seq.addTrack(piano, notes); // append a track
seq.addTrack(drums, notes, { id: "drums", volume: 0.8 }); // with options
seq.removeTrack(piano); // remove by instrument reference
seq.clearTracks(); // remove every trackaddTrack's third argument accepts:
| Field | Type | Description |
|---|---|---|
id |
string |
Stable id for setTrackVolume / muteTrack / soloTrack. |
humanize |
{ timingMs?: number; velocity?: number } |
Per-track humanize. Overrides the sequencer-level setting when set. |
volume |
number |
Multiplicative velocity scalar (default 1). 0.5 halves velocities. |
muted |
boolean |
When true, this track does not dispatch notes. |
solo |
boolean |
When true, only soloed tracks play. |
After setPatterns is called (see Pattern chain), addTrack / removeTrack / clearTracks throw — the chain is owned by the patterns array.
seq.setTrackVolume("drums", 0.6);
seq.muteTrack("drums");
seq.unmuteTrack("drums");
seq.soloTrack("lead");
seq.unsoloTrack("lead");Mixer methods operate on the currently-playing pattern (so per-pattern mute/solo state is automatic when using a pattern chain). Calls with an unknown id are no-ops.
seq.start(); // start from beginning (or resume from pause if no offset given)
seq.pause(); // freeze position
seq.stop(); // stop and reset to 0
seq.togglePlayPause(); // pause if playing, start/resume otherwise
seq.state; // "stopped" | "playing" | "paused"Individual sequenced notes can be stopped by their id:
seq.stopNote("intro-c"); // stop immediately
seq.stopNote("intro-c", time); // stop at a scheduled timeseq.bpm = 140; // change BPM live, no glitch
seq.timeSignature = 3; // 3/4 (number → { numerator: 3, denominator: 4 })
seq.timeSignature = { numerator: 7, denominator: 8 }; // 7/8
seq.timeSignature; // → { numerator: 7, denominator: 8 }
seq.position; // current position as "bar:beat:tick" string
seq.position = "3:1"; // seek while playing or stoppedThe "beat" event fires once per denominator-defined note: 4/4 → 4 beats per bar, 6/8 → 6 beats per bar, etc.
seq.loop = true;
seq.loopStart = "1:1"; // ticks or string notation
seq.loopEnd = "3:1"; // ticks or string notation
seq.progress; // 0..1 within the loop rangescheduleRepeat fires a callback at a regular musical interval, passing the exact AudioContext time:
const cancel = seq.scheduleRepeat((time) => {
piano.start({ note: "C4", time, duration: 0.1 });
}, "8n"); // every eighth note
cancel(); // stop repeatingAn optional third argument sets the start position:
seq.scheduleRepeat(callback, "4n", "2:1"); // start at bar 2seq.on("statechange", (state) => {
// state: "playing" | "paused" | "stopped"
setSeqState(state);
});
seq.on("beat", (beat, time) => {
const delay = (time - context.currentTime) * 1000;
setTimeout(() => metronome.flash(), delay);
});
seq.on("bar", (bar, time) => {
ui.updateBar(bar);
});
seq.on("step", (stepIndex, time) => {
ui.flashStep(stepIndex); // only fires when `stepSize` is set in options
});
seq.on("loop", () => {
console.log("looped");
});
seq.on("end", () => {
console.log("done");
});
seq.on("patternChange", (patternIndex, time) => {
ui.highlightPattern(patternIndex); // fires when the chain advances
});
seq.on("start", () => {});
seq.on("stop", () => {});
seq.on("pause", () => {});
seq.off("beat", handler); // remove a listenerThe "step" event only fires when the sequencer was constructed with stepSize (e.g. "16n").
The "patternChange" event only fires when more than one pattern is in the chain.
noteOn and noteOff events fire when the instrument's onStart / onEnded callbacks are called, so they are driven by the actual audio playback — not by the scheduling lookahead.
seq.on("noteOn", (event) => {
console.log(event.noteId, event.trackIndex, event.noteIndex);
highlight(event.noteId);
});
seq.on("noteOff", (event) => {
unhighlight(event.noteId);
});The event object (NoteEvent) contains:
| Field | Type | Description |
|---|---|---|
noteId |
string | number |
The note's id if provided, otherwise its array index |
trackIndex |
number |
Index of the track in the order it was added |
noteIndex |
number |
Index of the note within its track's notes array |
note |
SequencerNote |
The original note object |
You can set a custom id on any SequencerNote to use as noteId:
seq.addTrack(piano, [
{ id: "intro-c", note: "C4", at: "1:1", duration: "4n" },
{ id: "intro-e", note: "E4", at: "1:2", duration: "4n" },
]);Add subtle randomisation to timing and velocity for a more natural feel:
const seq = Sequencer(context, {
bpm: 90,
humanize: { timingMs: 12, velocity: 8 },
});timingMs: maximum random offset in milliseconds (±). Default 0.velocity: maximum random offset in MIDI velocity units (±). Default 0.
Per-track humanize (passed to addTrack) overrides the global setting:
seq.addTrack(piano, notes, { humanize: { timingMs: 0, velocity: 0 } });| Field | Type | Description |
|---|---|---|
note |
string | number |
Note name or MIDI number. |
at |
string | number |
Musical position (ticks or "bar:beat[.frac][:ticks]" / "4n" / "1m"). |
duration |
string | number? |
Duration; omit for a one-shot trigger. |
velocity |
number? |
Velocity 0–127. Default 100. |
id |
string | number? |
Used as noteId in noteOn / noteOff events. Default: array index. |
chance |
number? |
Probability 0–100 that this note fires on each pass. Re-rolled on every loop. |
ratchet |
number? |
Expand into N sub-notes over duration (requires duration). |
ratchetVelocityDecay |
number? |
Per-step velocity decay; each sub-note scaled by (1 - decay)^i. |
Example:
seq.addTrack(drums, [
{
note: "hat",
at: "1:4",
duration: "8n",
ratchet: 4,
ratchetVelocityDecay: 0.2,
},
{ note: "snare", at: "1:2", chance: 50 }, // fires 50% of the time
]);When ratchet > 1, each sub-note's noteId is suffixed with #0, #1, etc., so you can stop an individual sub-voice via seq.stopNote("id#0").
For multi-pattern arrangements (intro → verse → chorus), use setPatterns:
seq.setPatterns([
{ tracks: [{ instrument: drums, notes: introNotes }], loopEnd: "1m" },
{ tracks: [{ instrument: drums, notes: verseNotes }], loopEnd: "2m" },
{ tracks: [{ instrument: drums, notes: chorusNotes }], loopEnd: "2m" },
]);
seq.chainOrder = [0, 1, 2, 1, 2]; // intro, verse, chorus, verse, chorus
seq.loop = true; // loop the whole chain
seq.start();
seq.on("patternChange", (idx) => ui.highlightPattern(idx));- Each pattern's
tracksentries accept the sameAddTrackOptions(id,humanize,volume,muted,solo) asaddTrack. loopEndis per-pattern and defaults to the longest track in that pattern.chainOrderdefaults to[0, 1, …, n-1]. Setting it lets you repeat or reorder patterns without duplicating data.- With
loop: falsethe chain plays once and emits"end"; withloop: trueit cycles indefinitely and emits"loop"each time it wraps. - Track mixer methods (
muteTrack,setTrackVolume, etc.) operate on the currently-playing pattern —muteTrack("lead")only affects the pattern that owns the"lead"track.
Render audio offline (faster than real-time) and export it as a WAV file. Uses OfflineAudioContext under the hood.
import { renderOffline } from "smplr";
const result = await renderOffline(async (context) => {
const piano = await SplendidGrandPiano(context).load;
piano.start({ note: "C4", time: 0, duration: 1 });
piano.start({ note: "E4", time: 0.5, duration: 1 });
});
result.downloadWav("export.wav");const result = await renderOffline(callback, {
duration: 10, // Total duration in seconds (auto-detected if omitted)
sampleRate: 48000, // Sample rate (default: 48000)
channels: 2, // Number of channels (default: 2)
});When duration is omitted, a 60-second buffer is used and trailing silence is automatically trimmed. Pass an explicit duration for longer renders or to preserve trailing silence.
renderOffline returns a RenderResult object:
result.audioBuffer— the rawAudioBufferresult.toWav()— encode as 32-bit float WAVBlob(lossless)result.toWav16()— encode as 16-bit integer WAVBlob(smaller file)result.downloadWav(filename?)— download as 32-bit WAVresult.downloadWav16(filename?)— download as 16-bit WAVresult.duration— actual duration in secondsresult.sampleRate— sample rate used
WAV encoding is lazy — it only happens when you call toWav() or toWav16().
If you already have an instrument loaded, pass the same SampleLoader to avoid re-fetching samples:
import { SplendidGrandPiano, SampleLoader, renderOffline } from "smplr";
const loader = SampleLoader(audioContext);
const piano = SplendidGrandPiano(audioContext, { loader });
await piano.load;
// Offline render reuses cached buffers — no re-fetch
const result = await renderOffline(async (context) => {
const offlinePiano = await SplendidGrandPiano(context, { loader }).load;
offlinePiano.start({ note: "C4", time: 0, duration: 1 });
});Use offline rendering to generate reproducible audio files for issue reports. No install needed — just open your browser's DevTools console on any page and paste:
const { renderOffline, SplendidGrandPiano } =
await import("https://esm.sh/smplr");
const result = await renderOffline(async (context) => {
const piano = await SplendidGrandPiano(context).load;
piano.start({ note: "C4", time: 0, duration: 2 });
});
result.downloadWav16("bug-report.wav");This will download a WAV file you can attach to your issue or pull request.
Detailed configuration for each bundled instrument. For the shared API (load, play, output, effects, events), see Using an instrument.
An audio buffer sampler. Pass a buffers map of name → URL:
import { Sampler } from "smplr";
const buffers = {
kick: "https://smpldsnds.github.io/drum-machines/808-mini/kick.m4a",
snare: "https://smpldsnds.github.io/drum-machines/808-mini/snare-1.m4a",
};
const sampler = Sampler(new AudioContext(), { buffers });And then use the name of the buffer as note name:
sampler.start({ note: "kick" });For advanced use cases (per-region pitch/velocity/round-robin, SFZ-like multi-sample instruments, runtime swaps), pass a SmplrPreset directly:
import { Sampler, type SmplrPreset } from "smplr";
const kitA: SmplrPreset = {
samples: { baseUrl: "https://cdn.example.com/", formats: ["ogg"] },
groups: [
{
regions: [
{ sample: "kick", keyRange: [60, 60], pitch: 60 },
{ sample: "snare", keyRange: [62, 62], pitch: 62 },
],
},
],
};
const sampler = Sampler(new AudioContext(), { preset: kitA });
await sampler.ready;
sampler.start({ note: 60 });
// Swap content at runtime
await sampler.reload(kitB);The full SmplrPreset schema is documented in PRESET_SCHEMA.md. Note: buffers and preset are mutually exclusive on construction — pass exactly one.
sampler.reload(input) accepts either shape (flat buffers record or full SmplrPreset), regardless of which mode was used at construction.
A Soundfont player. By default it loads audio from Benjamin Gleitzman's package of pre-rendered sound fonts.
import { Soundfont, getSoundfontNames, getSoundfontKits } from "smplr";
const marimba = Soundfont(new AudioContext(), { instrument: "marimba" });
marimba.start({ note: "C4" });It's intended to be a modern replacement of soundfont-player
Use getSoundfontNames to get all available instrument names and getSoundfontKits to get kit names.
There are two kits available: MusyngKite or FluidR3_GM. The first one is used by default: it sounds better but the samples are heavier.
const marimba = Soundfont(context, {
instrument: "clavinet",
kit: "FluidR3_GM", // "MusyngKite" is used by default if not specified
});Alternatively, you can pass your custom url as the instrument. In that case, the kit is ignored:
const marimba = Soundfont(context, {
instrumentUrl:
"https://gleitz.github.io/midi-js-soundfonts/MusyngKite/marimba-mp3.js",
});You can enable note looping to make note names indefinitely long by loading loop data:
const marimba = Soundfont(context, {
instrument: "cello",
loadLoopData: true,
});A sampled acoustic piano. It uses Steinway samples with 4 velocity groups from SplendidGrandPiano
import { SplendidGrandPiano } from "smplr";
const piano = SplendidGrandPiano(new AudioContext());
piano.start({ note: "C4" });The second argument of the constructor accepts the following options:
baseUrl: where the piano samples are fetched from. Defaults to the public hosted set onsmpldsnds.github.io; override only if you mirror the samples yourself.detune: global detune in cents (0 if not specified)velocity: default velocity (100 if not specified)volume: default volume (100 if not specified)decayTime: default decay time (0.5 seconds)notesToLoad: an object with the following shape:{ notes: number[], velocityRange: [number, number]}to specify a subset of notes to load
Example:
const piano = SplendidGrandPiano(context, {
detune: -20,
volume: 80,
notesToLoad: {
notes: [60],
velocityRange: [1, 127],
},
});A sampled electric pianos. Samples from https://github.com/sfzinstruments/GregSullivan.E-Pianos
import { ElectricPiano, getElectricPianoNames } from "smplr";
const instruments = getElectricPianoNames(); // => ["CP80", "PianetT", "WurlitzerEP200", "TX81Z"]
const epiano = ElectricPiano(new AudioContext(), {
instrument: "PianetT",
});
epiano.start({ note: "C4" });
// Includes a (basic) tremolo effect:
epiano.tremolo.level(30);Available instruments:
CP80: Yamaha CP80 Electric Grand Piano v1.3 (29-Sep-2004)PianetT: Hohner Pianet T (type 2) v1.3 (24-Sep-2004)WurlitzerEP200: Wurlitzer EP200 Electric Piano v1.1 (16-May-1999)TX81Z: Yamaha TX81Z "FM Piano" patch (from the VCSL Electrophones set)
Samples from The Versilian Community Sample Library
import { Mallet, getMalletNames } from "smplr";
const instruments = getMalletNames();
const mallet = Mallet(new AudioContext(), {
instrument: instruments[0],
});Samples from archive.org
import { Mellotron, getMellotronNames } from "smplr";
const instruments = getMellotronNames();
const mellotron = Mellotron(new AudioContext(), {
instrument: instruments[0],
});Sampled drum machines. Samples from different sources:
import { DrumMachine, getDrumMachineNames } from "smplr";
const instruments = getDrumMachineNames();
const context = new AudioContext();
const drums = DrumMachine(context, { instrument: "TR-808" });
drums.start({ note: "kick" });
// Drum samples are grouped and can have sample variations:
drums.getSampleNames(); // => ['kick-1', 'kick-2', 'snare-1', 'snare-2', ...]
drums.getGroupNames(); // => ['kick', 'snare']
drums.getSampleNamesForGroup("kick"); // => ['kick-1', 'kick-2']
// You can trigger samples by group name or specific sample
drums.start("kick"); // Play the first sample of the group
drums.start("kick-1"); // Play this specific sampleSampled instrument for the Synthabuse drum-machine collection — 5 packs covering ~210 classic drum machines and synths. Samples hosted at smpldsnds.github.io/drum-abuse-{pack}/.
Two source modes: load a single machine's full kit, or load a cross-machine instrument list from a pack.
import { DrumAbuse, getDrumAbuseMachineNames } from "smplr";
const machines = getDrumAbuseMachineNames(); // ~210 machine ids
const context = new AudioContext();
const drums = DrumAbuse(context, {
source: { kind: "machine", machine: "roland-tr-808" },
});
await drums.load;
drums.start({ note: "kick" });
// Samples are grouped by instrument name, like DrumMachine:
drums.getGroupNames(); // => ["kick", "snare", "hi-hat", ...]
drums.getSampleNamesForGroup("kick"); // => ["kick/1", "kick/2", ...]
drums.start({ note: "kick" }); // first sample in the group
drums.start({ note: "kick/1" }); // a specific sampleIf a machine has more than one sample set, pass set to pick a specific one. Omit to load the first set.
const drums = DrumAbuse(context, {
source: { kind: "machine", machine: "roland-tr-808", set: "kit-a" },
});A pack is a cross-machine catalog of named instruments (e.g. all the kicks across vol1). Pass source: { kind: "pack", pack, instrument }:
import {
DrumAbuse,
getDrumAbusePackNames,
getDrumAbuseMachinesForPack,
} from "smplr";
getDrumAbusePackNames(); // => ["vol1", "vol2", "vol3", "vol4", "vol5"]
getDrumAbuseMachinesForPack("vol1"); // => machine ids in vol1
const drums = DrumAbuse(new AudioContext(), {
source: { kind: "pack", pack: "vol1", instrument: "bass-drum" },
});import { Smolken, getSmolkenNames } from "smplr";
const instruments = getSmolkenNames(); // => Arco, Pizzicato & Switched
// Create an instrument
const context = new AudioContext();
const doubleBass = await Smolken(context, { instrument: "Arco" }).load;Plays instruments from the Versilian Community Sample Library.
import { Versilian, getVersilianInstruments } from "smplr";
// getVersilianInstruments returns a Promise
const instrumentNames = await getVersilianInstruments();
const context = new AudioContext();
const versilian = Versilian(context, { instrument: instrumentNames[0] });Sampler capable of reading .sf2 files directly.
import { Soundfont2 } from "smplr";
import { SoundFont2 } from "soundfont2";
const context = new AudioContext();
const sampler = Soundfont2(context, {
url: "https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
createSoundfont: (data) => new SoundFont2(data),
});
sampler.load.then(() => {
// list all available instruments for the soundfont
console.log(sampler.instrumentNames);
// load the first available instrument
sampler.loadInstrument(sampler.instrumentNames[0]);
});If none of the bundled instruments fits your use case, you can author your own with the Instrument builder and the Smplr interface.
See Defining an instrument for the full authoring guide — sync and async examples, third-party package layout, and how to use Smplr as a TypeScript type for generic helpers.
smplr is approaching 1.0; pre-1.0 APIs keep working as deprecated aliases. See MIGRATE.md for the full compatibility table and CHANGELOG for per-release detail.
MIT License