smplr 0.26.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,7 +46,8 @@ seq.start();
46
46
  import { SplendidGrandPiano, Reverb, renderOffline } from "smplr";
47
47
 
48
48
  const wav = await renderOffline(async (context) => {
49
- const piano = await SplendidGrandPiano(context).load;
49
+ const piano = SplendidGrandPiano(context);
50
+ await piano.ready;
50
51
  piano.output.addEffect("reverb", Reverb(context), 0.3);
51
52
  ["C4", "E4", "G4", "C5"].forEach((note, i) => {
52
53
  piano.start({ note, time: i * 0.4, duration: 0.4 });
@@ -144,10 +145,10 @@ const marimba = Soundfont(context, { instrument: "marimba" });
144
145
  You can start playing notes as soon as one sample is loaded. To wait for all of them, await either:
145
146
 
146
147
  - `piano.ready` — resolves to `void` (preferred for new code).
147
- - `piano.load` — resolves to the instrument itself, so you can create and await in one line:
148
148
 
149
149
  ```js
150
- const piano = await SplendidGrandPiano(context).load;
150
+ const piano = SplendidGrandPiano(context);
151
+ await piano.ready;
151
152
  ```
152
153
 
153
154
  > Upgrading from older versions? See [MIGRATE.md](./MIGRATE.md).
@@ -235,7 +236,7 @@ const now = context.currentTime;
235
236
 
236
237
  #### Looping
237
238
 
238
- You can loop a note by using `loop`, `loopStart` and `loopEnd`:
239
+ You can loop a note by using `loop`, `loopStart` and `loopEnd` (positions in seconds):
239
240
 
240
241
  ```js
241
242
  const context = new AudioContext();
@@ -314,6 +315,8 @@ To change the mix level, use `output.setEffectMix(name, mix)`:
314
315
  piano.output.setEffectMix("reverb", 0.5);
315
316
  ```
316
317
 
318
+ Send buses are **post-fader**: they tap the signal after `output.volume` (and after any inserts), so turning `output.volume` down proportionally reduces what reaches the effect. Set `output.volume` to 0 and the send goes silent too.
319
+
317
320
  ### Events
318
321
 
319
322
  Two events are available: `onStart` and `onEnded`. Both callbacks receive the started note as a parameter, and can be configured globally:
@@ -721,7 +724,8 @@ Render audio offline (faster than real-time) and export it as a WAV file. Uses `
721
724
  import { renderOffline } from "smplr";
722
725
 
723
726
  const result = await renderOffline(async (context) => {
724
- const piano = await SplendidGrandPiano(context).load;
727
+ const piano = SplendidGrandPiano(context);
728
+ await piano.ready;
725
729
  piano.start({ note: "C4", time: 0, duration: 1 });
726
730
  piano.start({ note: "E4", time: 0.5, duration: 1 });
727
731
  });
@@ -764,11 +768,12 @@ import { SplendidGrandPiano, SampleLoader, renderOffline } from "smplr";
764
768
 
765
769
  const loader = SampleLoader(audioContext);
766
770
  const piano = SplendidGrandPiano(audioContext, { loader });
767
- await piano.load;
771
+ await piano.ready;
768
772
 
769
773
  // Offline render reuses cached buffers — no re-fetch
770
774
  const result = await renderOffline(async (context) => {
771
- const offlinePiano = await SplendidGrandPiano(context, { loader }).load;
775
+ const offlinePiano = SplendidGrandPiano(context, { loader });
776
+ await offlinePiano.ready;
772
777
  offlinePiano.start({ note: "C4", time: 0, duration: 1 });
773
778
  });
774
779
  ```
@@ -782,7 +787,8 @@ const { renderOffline, SplendidGrandPiano } =
782
787
  await import("https://esm.sh/smplr");
783
788
 
784
789
  const result = await renderOffline(async (context) => {
785
- const piano = await SplendidGrandPiano(context).load;
790
+ const piano = SplendidGrandPiano(context);
791
+ await piano.ready;
786
792
  piano.start({ note: "C4", time: 0, duration: 2 });
787
793
  });
788
794
  result.downloadWav16("bug-report.wav");
@@ -1029,7 +1035,7 @@ const context = new AudioContext();
1029
1035
  const drums = DrumAbuse(context, {
1030
1036
  source: { kind: "machine", machine: "roland-tr-808" },
1031
1037
  });
1032
- await drums.load;
1038
+ await drums.ready;
1033
1039
 
1034
1040
  drums.start({ note: "kick" });
1035
1041
 
@@ -1076,7 +1082,8 @@ const instruments = getSmolkenNames(); // => Arco, Pizzicato & Switched
1076
1082
 
1077
1083
  // Create an instrument
1078
1084
  const context = new AudioContext();
1079
- const doubleBass = await Smolken(context, { instrument: "Arco" }).load;
1085
+ const doubleBass = Smolken(context, { instrument: "Arco" });
1086
+ await doubleBass.ready;
1080
1087
  ```
1081
1088
 
1082
1089
  ### Versilian
@@ -1109,7 +1116,7 @@ const sampler = Soundfont2(context, {
1109
1116
  createSoundfont: (data) => new SoundFont2(data),
1110
1117
  });
1111
1118
 
1112
- sampler.load.then(() => {
1119
+ sampler.ready.then(() => {
1113
1120
  // list all available instruments for the soundfont
1114
1121
  console.log(sampler.instrumentNames);
1115
1122
 
package/dist/index.d.mts CHANGED
@@ -26,6 +26,15 @@ declare class Channel {
26
26
  get pan(): number;
27
27
  set pan(value: number);
28
28
  addInsert(effect: AudioNode | AudioInsert): void;
29
+ /**
30
+ * Add a send effect on a parallel bus.
31
+ *
32
+ * The send is **post-fader**: it taps the channel signal after the volume
33
+ * gain (and after any inserts added via {@link addInsert}), before the
34
+ * panner. Lowering `volume` proportionally lowers the send level; `volume = 0`
35
+ * silences the send too. Inserts are upstream of the tap, so they are heard
36
+ * on the send.
37
+ */
29
38
  addEffect(name: string, effect: AudioNode | {
30
39
  input: AudioNode;
31
40
  }, mixValue: number): void;
@@ -101,7 +110,6 @@ type SmplrRegion = PlaybackParams & {
101
110
  group?: number;
102
111
  offBy?: number;
103
112
  trigger?: "first" | "legato";
104
- ampVelCurve?: [number, number];
105
113
  /** Auto-compute loop points from buffer duration ratios (0–1). */
106
114
  loopAuto?: {
107
115
  startRatio: number;
@@ -186,7 +194,7 @@ type LoadProgress = {
186
194
  };
187
195
  /**
188
196
  * Fully resolved playback parameters for a single Voice.
189
- * Output of resolveParams() — all fields are required, no optionals except ampVelCurve/loopAuto.
197
+ * Output of resolveParams() — all fields are required, no optionals except loopAuto.
190
198
  */
191
199
  type VoiceParams = {
192
200
  detune: number;
@@ -199,7 +207,6 @@ type VoiceParams = {
199
207
  loop: boolean;
200
208
  loopStart: number;
201
209
  loopEnd: number;
202
- ampVelCurve?: [number, number];
203
210
  /** If set, loop points are computed from buffer.duration at play time. */
204
211
  loopAuto?: {
205
212
  startRatio: number;
@@ -1230,7 +1237,7 @@ declare function sf2InstrumentToPreset(sf2Instrument: Sf2Instrument, context: Ba
1230
1237
  };
1231
1238
  type Soundfont2SamplerExtras = {
1232
1239
  readonly instrumentNames: string[];
1233
- loadInstrument(instrumentName: string): Promise<void> | undefined;
1240
+ loadInstrument(instrumentName: string): Promise<void>;
1234
1241
  };
1235
1242
  declare const Soundfont2: InstrumentFactory<Soundfont2Options, Soundfont2SamplerExtras>;
1236
1243
  /** Instance type returned by the {@link Soundfont2} factory. */
package/dist/index.d.ts CHANGED
@@ -26,6 +26,15 @@ declare class Channel {
26
26
  get pan(): number;
27
27
  set pan(value: number);
28
28
  addInsert(effect: AudioNode | AudioInsert): void;
29
+ /**
30
+ * Add a send effect on a parallel bus.
31
+ *
32
+ * The send is **post-fader**: it taps the channel signal after the volume
33
+ * gain (and after any inserts added via {@link addInsert}), before the
34
+ * panner. Lowering `volume` proportionally lowers the send level; `volume = 0`
35
+ * silences the send too. Inserts are upstream of the tap, so they are heard
36
+ * on the send.
37
+ */
29
38
  addEffect(name: string, effect: AudioNode | {
30
39
  input: AudioNode;
31
40
  }, mixValue: number): void;
@@ -101,7 +110,6 @@ type SmplrRegion = PlaybackParams & {
101
110
  group?: number;
102
111
  offBy?: number;
103
112
  trigger?: "first" | "legato";
104
- ampVelCurve?: [number, number];
105
113
  /** Auto-compute loop points from buffer duration ratios (0–1). */
106
114
  loopAuto?: {
107
115
  startRatio: number;
@@ -186,7 +194,7 @@ type LoadProgress = {
186
194
  };
187
195
  /**
188
196
  * Fully resolved playback parameters for a single Voice.
189
- * Output of resolveParams() — all fields are required, no optionals except ampVelCurve/loopAuto.
197
+ * Output of resolveParams() — all fields are required, no optionals except loopAuto.
190
198
  */
191
199
  type VoiceParams = {
192
200
  detune: number;
@@ -199,7 +207,6 @@ type VoiceParams = {
199
207
  loop: boolean;
200
208
  loopStart: number;
201
209
  loopEnd: number;
202
- ampVelCurve?: [number, number];
203
210
  /** If set, loop points are computed from buffer.duration at play time. */
204
211
  loopAuto?: {
205
212
  startRatio: number;
@@ -1230,7 +1237,7 @@ declare function sf2InstrumentToPreset(sf2Instrument: Sf2Instrument, context: Ba
1230
1237
  };
1231
1238
  type Soundfont2SamplerExtras = {
1232
1239
  readonly instrumentNames: string[];
1233
- loadInstrument(instrumentName: string): Promise<void> | undefined;
1240
+ loadInstrument(instrumentName: string): Promise<void>;
1234
1241
  };
1235
1242
  declare const Soundfont2: InstrumentFactory<Soundfont2Options, Soundfont2SamplerExtras>;
1236
1243
  /** Instance type returned by the {@link Soundfont2} factory. */
package/dist/index.js CHANGED
@@ -136,11 +136,12 @@ var HttpStorage = {
136
136
  return fetch(url);
137
137
  }
138
138
  };
139
- var _cache, _CacheStorageImpl_instances, tryFromCache_fn, saveResponse_fn;
139
+ var _cache, _warned, _CacheStorageImpl_instances, tryFromCache_fn, saveResponse_fn;
140
140
  var CacheStorageImpl = class {
141
141
  constructor(name = "smplr") {
142
142
  __privateAdd(this, _CacheStorageImpl_instances);
143
143
  __privateAdd(this, _cache);
144
+ __privateAdd(this, _warned, false);
144
145
  if (typeof window === "undefined" || !("caches" in window)) {
145
146
  __privateSet(this, _cache, Promise.reject("CacheStorage not supported"));
146
147
  __privateGet(this, _cache).catch(() => {
@@ -163,6 +164,7 @@ var CacheStorageImpl = class {
163
164
  }
164
165
  };
165
166
  _cache = new WeakMap();
167
+ _warned = new WeakMap();
166
168
  _CacheStorageImpl_instances = new WeakSet();
167
169
  tryFromCache_fn = function(request) {
168
170
  return __async(this, null, function* () {
@@ -178,6 +180,10 @@ saveResponse_fn = function(request, response) {
178
180
  const cache = yield __privateGet(this, _cache);
179
181
  yield cache.put(request, response.clone());
180
182
  } catch (err) {
183
+ if (!__privateGet(this, _warned)) {
184
+ __privateSet(this, _warned, true);
185
+ console.warn("smplr: failed to cache response", err);
186
+ }
181
187
  }
182
188
  });
183
189
  };
@@ -297,6 +303,15 @@ var Channel = class {
297
303
  __privateGet(this, _config).destination
298
304
  ]));
299
305
  }
306
+ /**
307
+ * Add a send effect on a parallel bus.
308
+ *
309
+ * The send is **post-fader**: it taps the channel signal after the volume
310
+ * gain (and after any inserts added via {@link addInsert}), before the
311
+ * panner. Lowering `volume` proportionally lowers the send level; `volume = 0`
312
+ * silences the send too. Inserts are upstream of the tap, so they are heard
313
+ * on the send.
314
+ */
300
315
  addEffect(name, effect, mixValue) {
301
316
  var _a;
302
317
  if (__privateGet(this, _disconnected)) {
@@ -403,7 +418,6 @@ function resolveParams(defaults, group, region, midi, velocity, overrides) {
403
418
  loop: (_e = overrides == null ? void 0 : overrides.loop) != null ? _e : merged.loop,
404
419
  loopStart: merged.loopStart,
405
420
  loopEnd: merged.loopEnd,
406
- ampVelCurve: region.ampVelCurve,
407
421
  loopAuto: region.loopAuto,
408
422
  reverse: (_f = overrides == null ? void 0 : overrides.reverse) != null ? _f : merged.reverse
409
423
  };
@@ -806,7 +820,7 @@ var Voice = class {
806
820
  __privateSet(this, _startAt, startAt);
807
821
  let offsetSec = 0;
808
822
  if (params.offset > 0) {
809
- offsetSec = params.reverse ? (buffer.length - params.offset) / buffer.sampleRate : params.offset / buffer.sampleRate;
823
+ offsetSec = params.reverse ? buffer.duration - params.offset : params.offset;
810
824
  }
811
825
  source.start(startAt, offsetSec);
812
826
  __privateSet(this, _source, source);
@@ -1733,6 +1747,9 @@ function fetchJSON(url, storage) {
1733
1747
  return r.json();
1734
1748
  });
1735
1749
  jsonCache.set(url, p);
1750
+ p.catch(() => {
1751
+ if (jsonCache.get(url) === p) jsonCache.delete(url);
1752
+ });
1736
1753
  }
1737
1754
  return p;
1738
1755
  }
@@ -3057,8 +3074,6 @@ function buildRegion(props, pathFromSampleName) {
3057
3074
  if (tune !== void 0) region.tune = tune / 100;
3058
3075
  const ampRelease = num(props, "ampeg_release");
3059
3076
  if (ampRelease !== void 0) region.ampRelease = ampRelease;
3060
- const ampVelcurve = numArr(props, "amp_velcurve");
3061
- if (ampVelcurve) region.ampVelCurve = ampVelcurve;
3062
3077
  return region;
3063
3078
  }
3064
3079
  function resolveDefines(sfz) {
@@ -3136,16 +3151,18 @@ function str(props, key) {
3136
3151
  if (typeof v === "string") return v;
3137
3152
  return void 0;
3138
3153
  }
3139
- function numArr(props, _prefix) {
3140
- for (const [k, v] of Object.entries(props)) {
3141
- if (k.startsWith("amp_velcurve_")) {
3142
- const vel = Number(k.slice("amp_velcurve_".length));
3143
- if (!isNaN(vel) && typeof v === "number") {
3144
- return [vel, v];
3145
- }
3154
+
3155
+ // src/fetch-ok.ts
3156
+ function fetchOk(url) {
3157
+ return __async(this, null, function* () {
3158
+ const res = yield fetch(url);
3159
+ if (!res.ok) {
3160
+ throw new Error(
3161
+ `smplr: failed to fetch ${url} (${res.status} ${res.statusText})`
3162
+ );
3146
3163
  }
3147
- }
3148
- return void 0;
3164
+ return res;
3165
+ });
3149
3166
  }
3150
3167
 
3151
3168
  // src/tremolo.ts
@@ -3247,7 +3264,7 @@ var ElectricPiano = Instrument(
3247
3264
  };
3248
3265
  const tremoloNode = createTremolo(ctx, depth.subscribe);
3249
3266
  smplr.output.addInsert(tremoloNode);
3250
- const ready = fetch(config.sfzUrl).then((r) => r.text()).then(
3267
+ const ready = fetchOk(config.sfzUrl).then((r) => r.text()).then(
3251
3268
  (sfzText) => {
3252
3269
  var _a;
3253
3270
  return smplr.loadInstrument(
@@ -3267,7 +3284,7 @@ var ElectricPiano = Instrument(
3267
3284
  var VCSL_BASE_URL = "https://smpldsnds.github.io/sgossner-vcsl";
3268
3285
  var instrumentsPromise;
3269
3286
  function getVersilianInstruments() {
3270
- return instrumentsPromise != null ? instrumentsPromise : instrumentsPromise = fetch(
3287
+ return instrumentsPromise != null ? instrumentsPromise : instrumentsPromise = fetchOk(
3271
3288
  VCSL_BASE_URL + "/sfz_files.json"
3272
3289
  ).then((res) => res.json());
3273
3290
  }
@@ -3280,7 +3297,7 @@ function loadVersilianInstrument(smplr, options) {
3280
3297
  const sfzUrl = `${VCSL_BASE_URL}/${instrument}.sfz`;
3281
3298
  const base = instrument.slice(0, instrument.lastIndexOf("/") + 1);
3282
3299
  const sampleBaseUrl2 = `${VCSL_BASE_URL}/${base}`;
3283
- return fetch(sfzUrl).then((r) => r.text()).then(
3300
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3284
3301
  (sfzText) => smplr.loadInstrument(
3285
3302
  sfzToPreset(sfzText, {
3286
3303
  baseUrl: sampleBaseUrl2,
@@ -3385,7 +3402,7 @@ var Mellotron = Instrument(
3385
3402
  const variation = INSTRUMENT_VARIATIONS[instrument];
3386
3403
  const instrumentName = variation ? variation[0] : instrument;
3387
3404
  const baseUrl = `https://smpldsnds.github.io/archiveorg-mellotron/${instrumentName}/`;
3388
- return fetch(baseUrl + "files.json").then((r) => r.json()).then(
3405
+ return fetchOk(baseUrl + "files.json").then((r) => r.json()).then(
3389
3406
  (names) => smplr.loadInstrument(
3390
3407
  mellotronToPreset(names, {
3391
3408
  instrument: instrumentName,
@@ -3454,7 +3471,7 @@ function createDattorroReverbEffect(context) {
3454
3471
  if (!ready) {
3455
3472
  const blob = new Blob([PROCESSOR], { type: "application/javascript" });
3456
3473
  const url = URL.createObjectURL(blob);
3457
- ready = context.audioWorklet.addModule(url);
3474
+ ready = context.audioWorklet.addModule(url).finally(() => URL.revokeObjectURL(url));
3458
3475
  init.set(context, ready);
3459
3476
  }
3460
3477
  yield ready;
@@ -3495,11 +3512,13 @@ var ReverbImpl = class {
3495
3512
  return __privateGet(this, _ready);
3496
3513
  }
3497
3514
  connect(output) {
3498
- if (__privateGet(this, _effect)) {
3499
- __privateGet(this, _effect).disconnect(__privateGet(this, _output));
3500
- __privateGet(this, _effect).connect(output);
3501
- }
3502
3515
  __privateSet(this, _output, output);
3516
+ __privateGet(this, _ready).then(() => {
3517
+ if (__privateGet(this, _effect)) {
3518
+ __privateGet(this, _effect).disconnect();
3519
+ __privateGet(this, _effect).connect(__privateGet(this, _output));
3520
+ }
3521
+ });
3503
3522
  }
3504
3523
  };
3505
3524
  _effect = new WeakMap();
@@ -3646,7 +3665,7 @@ var Smolken = Instrument(
3646
3665
  (ctx, options = {}, smplr) => {
3647
3666
  var _a;
3648
3667
  const sfzUrl = getSmolkenUrl((_a = options.instrument) != null ? _a : "Arco");
3649
- return fetch(sfzUrl).then((r) => r.text()).then(
3668
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3650
3669
  (sfzText) => smplr.loadInstrument(
3651
3670
  sfzToPreset(sfzText, {
3652
3671
  baseUrl: SMOLKEN_BASE_URL,
@@ -3807,8 +3826,7 @@ function fetchSoundfontLoopData(url, sampleRate = 44100) {
3807
3826
  return __async(this, null, function* () {
3808
3827
  if (!url) return void 0;
3809
3828
  try {
3810
- const req = yield fetch(url);
3811
- if (req.status !== 200) return;
3829
+ const req = yield fetchOk(url);
3812
3830
  const raw = yield req.json();
3813
3831
  const loopData = {};
3814
3832
  Object.keys(raw).forEach((key) => {
@@ -3961,7 +3979,7 @@ function removeBase64Prefix(audioBase64) {
3961
3979
  return audioBase64.slice(audioBase64.indexOf(",") + 1);
3962
3980
  }
3963
3981
  function base64ToArrayBuffer(base64) {
3964
- const decoded = window.atob(base64);
3982
+ const decoded = atob(base64);
3965
3983
  const len = decoded.length;
3966
3984
  const bytes = new Uint8Array(len);
3967
3985
  for (let i = 0; i < len; i++) {
@@ -4018,7 +4036,11 @@ var Soundfont2 = Instrument(
4018
4036
  const sf2inst = soundfont == null ? void 0 : soundfont.instruments.find(
4019
4037
  (inst) => inst.header.name === instrumentName
4020
4038
  );
4021
- if (!sf2inst) return void 0;
4039
+ if (!sf2inst) {
4040
+ throw new Error(
4041
+ `Soundfont2: instrument "${instrumentName}" not found`
4042
+ );
4043
+ }
4022
4044
  const { json, buffers } = sf2InstrumentToPreset(sf2inst, ctx);
4023
4045
  return baseLoadInstrument(json, buffers);
4024
4046
  }