Skip to content

Commit d780dc1

Browse files
authored
0.14.0 - soundfont2 sampler (#81)
* feat: add SamplerInstrument * feat: remove options from RegionGroup * feat: initial sf2 implementation * feat: add soundfon2 example * chore: changeset * chore: version bump * chore: documentation
1 parent 90c75df commit d780dc1

17 files changed

+473
-64
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ src/**/*.js
77
.next
88
.DS_Store
99
tmp/
10-
bun.lockb
10+
bun.lockb
11+
*.sf2

CHANGELOG.md

+35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
# smplr
22

3+
## 0.14.x
4+
5+
#### Load soundfont files directly via Soundfont2 (385d492)
6+
7+
Now is possible to load soundfont files directly thanks to [soundfont2](https://www.npmjs.com/package/soundfont2) package.
8+
9+
First you need to add the dependency to the project:
10+
11+
```bash
12+
npm i soundfont2
13+
```
14+
15+
Then, use the new Soundfont2Sampler class:
16+
17+
```ts
18+
import { Soundfont2Sampler } from "smplr";
19+
import { SoundFont2 } from "soundfont2";
20+
21+
const context = new AudioContext();
22+
const sampler = Soundfont2Sampler(context, {
23+
url: "https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
24+
createSoundfont: (data) => new SoundFont2(data),
25+
});
26+
27+
sampler.load.then(() => {
28+
// list all available instruments for the soundfont
29+
console.log(sampler.instrumentNames);
30+
31+
// load the first available instrument
32+
sampler.loadInstrument(sampler.instrumentNames[0]);
33+
});
34+
```
35+
36+
Support is still very limited. Lot of soundfont features are still not implemented, however looping seems to work quite well (read #78)
37+
338
## 0.13.x
439

540
#### DrumMachines accept a DrumMachineInstrument as source

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,31 @@ const context = new AudioContext();
501501
const sampler = new Versilian(context, { instrument: instrumentNAmes[0] });
502502
```
503503

504+
### Soundfont2Sampler
505+
506+
Sampler capable of reading .sf2 files directly:
507+
508+
```ts
509+
import { Soundfont2Sampler } from "smplr";
510+
import { SoundFont2 } from "soundfont2";
511+
512+
const context = new AudioContext();
513+
const sampler = Soundfont2Sampler(context, {
514+
url: "https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
515+
createSoundfont: (data) => new SoundFont2(data),
516+
});
517+
518+
sampler.load.then(() => {
519+
// list all available instruments for the soundfont
520+
console.log(sampler.instrumentNames);
521+
522+
// load the first available instrument
523+
sampler.loadInstrument(sampler.instrumentNames[0]);
524+
});
525+
```
526+
527+
Still limited support. API may vary.
528+
504529
## License
505530

506531
MIT License

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "smplr",
3-
"version": "0.13.4",
3+
"version": "0.14.0",
44
"homepage": "https://github.com/danigb/smplr#readme",
55
"description": "A Sampled collection of instruments",
66
"main": "dist/index.js",

site/package-lock.json

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/pages/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ElectricPianoExample } from "src/ElectricPianoExample";
55
import { MalletExample } from "src/MalletExample";
66
import { MellotronExample } from "src/MellotronExample";
77
import { SmolkenExample } from "src/SmolkenExample";
8+
import { Soundfont2Example } from "src/Soundfont2Example";
89
import { SoundfontExample } from "src/SoundfontExample";
910
import { VersilianExample } from "src/VersilianExample";
1011
import { PianoExample } from "../src/PianoExample";
@@ -33,6 +34,7 @@ export default function Home() {
3334
<MellotronExample />
3435
<SmolkenExample />
3536
<VersilianExample />
37+
<Soundfont2Example />
3638
</div>
3739
</main>
3840
</>

site/src/Soundfont2Example.tsx

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Reverb, Soundfont2Sampler, Storage } from "smplr";
5+
import { SoundFont2 } from "soundfont2";
6+
import { ConnectMidi } from "./ConnectMidi";
7+
import { PianoKeyboard } from "./PianoKeyboard";
8+
import { getAudioContext } from "./audio-context";
9+
import { LoadWithStatus, useStatus } from "./useStatus";
10+
11+
const SF2_INSTRUMENTS: Record<string, string> = {
12+
"Galaxy Electric Pianos":
13+
"https://smpldsnds.github.io/soundfonts/soundfonts/galaxy-electric-pianos.sf2",
14+
"Giga MIDI":
15+
"https://smpldsnds.github.io/soundfonts/soundfonts/giga-hq-fm-gm.sf2",
16+
Supersaw:
17+
"https://smpldsnds.github.io/soundfonts/soundfonts/supersaw-collection.sf2",
18+
};
19+
const SF_NAMES = Object.keys(SF2_INSTRUMENTS);
20+
21+
let reverb: Reverb | undefined;
22+
let storage: Storage | undefined;
23+
let samplerNames: string[] = [
24+
"Supersaw",
25+
"Giga MIDI",
26+
"Galaxy Electric Pianos",
27+
];
28+
29+
export function Soundfont2Example({ className }: { className?: string }) {
30+
const [sampler, setSampler] = useState<Soundfont2Sampler | undefined>(
31+
undefined
32+
);
33+
const [samplerName, setSamplerName] = useState<string>(samplerNames[0]);
34+
const [instrumentName, setInstrumentName] = useState<string>("");
35+
const [status, setStatus] = useStatus();
36+
const [reverbMix, setReverbMix] = useState(0);
37+
const [volume, setVolume] = useState(100);
38+
39+
function loadSampler(sf2Name: string) {
40+
if (sampler) sampler.disconnect();
41+
setStatus("loading");
42+
const context = getAudioContext();
43+
setSamplerName(sf2Name);
44+
45+
reverb ??= new Reverb(context);
46+
const newSampler = new Soundfont2Sampler(context, {
47+
url: SF2_INSTRUMENTS[sf2Name],
48+
createSoundfont: (data) => new SoundFont2(data),
49+
});
50+
newSampler.output.addEffect("reverb", reverb, reverbMix);
51+
setSampler(newSampler);
52+
53+
newSampler.load.then((sampler) => {
54+
const instrumentName = sampler.instrumentNames[0];
55+
setInstrumentName(instrumentName);
56+
sampler.loadInstrument(instrumentName);
57+
setStatus("ready");
58+
});
59+
}
60+
61+
return (
62+
<div className={className}>
63+
<div className="flex gap-2 items-end mb-2">
64+
<div className="flex flex-col">
65+
<h1 className="text-3xl">Soundfont2</h1>
66+
<h2>Soundfont2 sampler</h2>
67+
</div>
68+
69+
<div>(experimental)</div>
70+
71+
<LoadWithStatus
72+
status={status}
73+
onClick={() => loadSampler("Supersaw")}
74+
/>
75+
<ConnectMidi instrument={sampler} />
76+
</div>
77+
<div className="my-2">
78+
<select
79+
className="appearance-none bg-zinc-700 text-zinc-200 rounded border border-gray-400 py-2 px-3 leading-tight focus:outline-none focus:border-blue-500"
80+
value={samplerName}
81+
onChange={(e) => {
82+
const sf2name = e.target.value;
83+
loadSampler(sf2name);
84+
}}
85+
>
86+
{samplerNames.map((name) => (
87+
<option key={name} value={name}>
88+
{name}
89+
</option>
90+
))}
91+
</select>
92+
</div>
93+
<div className={status !== "ready" ? "opacity-30" : ""}>
94+
<div className="flex gap-4 mb-2 no-select">
95+
<select
96+
className="appearance-none bg-zinc-700 text-zinc-200 rounded border border-gray-400 py-2 px-3 leading-tight focus:outline-none focus:border-blue-500"
97+
value={instrumentName}
98+
onChange={(e) => {
99+
const instrumentName = e.target.value;
100+
setInstrumentName(instrumentName);
101+
sampler?.loadInstrument(instrumentName);
102+
}}
103+
>
104+
{sampler?.instrumentNames.map((name) => (
105+
<option key={name} value={name}>
106+
{name}
107+
</option>
108+
))}
109+
</select>
110+
<button
111+
className="bg-zinc-700 rounded px-3 py-0.5 shadow"
112+
onClick={() => {
113+
sampler?.stop();
114+
}}
115+
>
116+
Stop all
117+
</button>
118+
</div>
119+
<div className="flex gap-4 mb-2 no-select">
120+
<div>Volume:</div>
121+
<input
122+
type="range"
123+
min={0}
124+
max={127}
125+
step={1}
126+
value={volume}
127+
onChange={(e) => {
128+
const volume = e.target.valueAsNumber;
129+
sampler?.output.setVolume(volume);
130+
setVolume(volume);
131+
}}
132+
/>
133+
<div>Reverb:</div>
134+
<input
135+
type="range"
136+
min={0}
137+
max={1}
138+
step={0.001}
139+
value={reverbMix}
140+
onChange={(e) => {
141+
const mix = e.target.valueAsNumber;
142+
sampler?.output.sendEffect("reverb", mix);
143+
setReverbMix(mix);
144+
}}
145+
/>
146+
</div>
147+
<PianoKeyboard
148+
borderColor="border-blue-700"
149+
onPress={(note) => {
150+
if (!sampler) return;
151+
note.time = (note.time ?? 0) + sampler.context.currentTime;
152+
sampler.start(note);
153+
}}
154+
onRelease={(midi) => {
155+
sampler?.stop({ stopId: midi });
156+
}}
157+
/>
158+
</div>
159+
</div>
160+
);
161+
}

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "./reverb/reverb";
66
export * from "./sampler";
77
export * from "./smolken";
88
export * from "./soundfont/soundfont";
9+
export * from "./soundfont2";
910
export * from "./splendid-grand-piano";
1011
export * from "./storage";
1112
export * from "./versilian";

src/mellotron.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ export class Mellotron implements InternalPlayer {
7979

8080
public constructor(
8181
public readonly context: BaseAudioContext,
82-
options: MellotronOptions
82+
private readonly options: MellotronOptions
8383
) {
8484
this.config = getMellotronConfig(options);
8585
this.player = new DefaultPlayer(context, options);
86-
this.group = createEmptyRegionGroup(options);
86+
this.group = createEmptyRegionGroup();
8787

8888
const loader = loadMellotronInstrument(
8989
this.config.instrument,
@@ -104,7 +104,9 @@ export class Mellotron implements InternalPlayer {
104104
start(sample: SampleStart | string | number) {
105105
const found = findFirstSampleInRegions(
106106
this.group,
107-
typeof sample === "object" ? sample : { note: sample }
107+
typeof sample === "object" ? sample : { note: sample },
108+
undefined,
109+
this.options
108110
);
109111

110112
if (!found) return () => undefined;

0 commit comments

Comments
 (0)