smplr 0.25.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/dist/index.mjs CHANGED
@@ -68,11 +68,12 @@ var HttpStorage = {
68
68
  return fetch(url);
69
69
  }
70
70
  };
71
- var _cache, _CacheStorageImpl_instances, tryFromCache_fn, saveResponse_fn;
71
+ var _cache, _warned, _CacheStorageImpl_instances, tryFromCache_fn, saveResponse_fn;
72
72
  var CacheStorageImpl = class {
73
73
  constructor(name = "smplr") {
74
74
  __privateAdd(this, _CacheStorageImpl_instances);
75
75
  __privateAdd(this, _cache);
76
+ __privateAdd(this, _warned, false);
76
77
  if (typeof window === "undefined" || !("caches" in window)) {
77
78
  __privateSet(this, _cache, Promise.reject("CacheStorage not supported"));
78
79
  __privateGet(this, _cache).catch(() => {
@@ -95,6 +96,7 @@ var CacheStorageImpl = class {
95
96
  }
96
97
  };
97
98
  _cache = new WeakMap();
99
+ _warned = new WeakMap();
98
100
  _CacheStorageImpl_instances = new WeakSet();
99
101
  tryFromCache_fn = function(request) {
100
102
  return __async(this, null, function* () {
@@ -110,6 +112,10 @@ saveResponse_fn = function(request, response) {
110
112
  const cache = yield __privateGet(this, _cache);
111
113
  yield cache.put(request, response.clone());
112
114
  } catch (err) {
115
+ if (!__privateGet(this, _warned)) {
116
+ __privateSet(this, _warned, true);
117
+ console.warn("smplr: failed to cache response", err);
118
+ }
113
119
  }
114
120
  });
115
121
  };
@@ -229,6 +235,15 @@ var Channel = class {
229
235
  __privateGet(this, _config).destination
230
236
  ]));
231
237
  }
238
+ /**
239
+ * Add a send effect on a parallel bus.
240
+ *
241
+ * The send is **post-fader**: it taps the channel signal after the volume
242
+ * gain (and after any inserts added via {@link addInsert}), before the
243
+ * panner. Lowering `volume` proportionally lowers the send level; `volume = 0`
244
+ * silences the send too. Inserts are upstream of the tap, so they are heard
245
+ * on the send.
246
+ */
232
247
  addEffect(name, effect, mixValue) {
233
248
  var _a;
234
249
  if (__privateGet(this, _disconnected)) {
@@ -318,7 +333,7 @@ function pickPlaybackParams(obj) {
318
333
  return result;
319
334
  }
320
335
  function resolveParams(defaults, group, region, midi, velocity, overrides) {
321
- var _a, _b, _c, _d, _e;
336
+ var _a, _b, _c, _d, _e, _f;
322
337
  const merged = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, PARAM_DEFAULTS), defaults), pickPlaybackParams(group)), pickPlaybackParams(region));
323
338
  const pitch = (_b = (_a = region.pitch) != null ? _a : region.key) != null ? _b : midi;
324
339
  const semitones = midi - pitch;
@@ -335,9 +350,8 @@ function resolveParams(defaults, group, region, midi, velocity, overrides) {
335
350
  loop: (_e = overrides == null ? void 0 : overrides.loop) != null ? _e : merged.loop,
336
351
  loopStart: merged.loopStart,
337
352
  loopEnd: merged.loopEnd,
338
- ampVelCurve: region.ampVelCurve,
339
353
  loopAuto: region.loopAuto,
340
- reverse: overrides == null ? void 0 : overrides.reverse
354
+ reverse: (_f = overrides == null ? void 0 : overrides.reverse) != null ? _f : merged.reverse
341
355
  };
342
356
  }
343
357
 
@@ -515,14 +529,6 @@ var SampleLoaderImpl = class {
515
529
  __privateSet(this, _context, context);
516
530
  __privateSet(this, _storage, (_a = options == null ? void 0 : options.storage) != null ? _a : HttpStorage);
517
531
  }
518
- /**
519
- * Load all samples referenced in `json`. Returns a Map of sample name →
520
- * AudioBuffer. Progress is reported via `onProgress` callback or via
521
- * options object.
522
- *
523
- * - `buffers` in options: pre-loaded buffers — skips fetch for these names.
524
- * - All samples load in parallel. Failed samples are silently omitted.
525
- */
526
532
  load(json, onProgressOrOptions) {
527
533
  return __async(this, null, function* () {
528
534
  var _a, _b;
@@ -642,14 +648,6 @@ var SchedulerImpl = class {
642
648
  __privateSet(this, _intervalMs, (_b = options == null ? void 0 : options.intervalMs) != null ? _b : INTERVAL_MS_DEFAULT);
643
649
  __privateSet(this, _queue, new SortedQueue((a, b) => a.time - b.time));
644
650
  }
645
- /**
646
- * Schedule a callback for a NoteEvent.
647
- *
648
- * - If the event's time falls within the lookahead window (or has no time), the
649
- * callback is called synchronously and a no-op StopFn is returned.
650
- * - Otherwise the event is queued, the interval is started if needed, and a StopFn
651
- * is returned that removes the event from the queue before it is dispatched.
652
- */
653
651
  schedule(event, callback) {
654
652
  var _a;
655
653
  const now = __privateGet(this, _context2).currentTime;
@@ -665,10 +663,6 @@ var SchedulerImpl = class {
665
663
  __privateGet(this, _queue).removeAll((q) => q === item);
666
664
  };
667
665
  }
668
- /**
669
- * Clear all queued (not-yet-dispatched) events and stop the interval.
670
- * Does not affect voices that are already playing.
671
- */
672
666
  stop() {
673
667
  __privateGet(this, _queue).clear();
674
668
  if (__privateGet(this, _intervalId) !== void 0) {
@@ -758,7 +752,7 @@ var Voice = class {
758
752
  __privateSet(this, _startAt, startAt);
759
753
  let offsetSec = 0;
760
754
  if (params.offset > 0) {
761
- offsetSec = params.reverse ? (buffer.length - params.offset) / buffer.sampleRate : params.offset / buffer.sampleRate;
755
+ offsetSec = params.reverse ? buffer.duration - params.offset : params.offset;
762
756
  }
763
757
  source.start(startAt, offsetSec);
764
758
  __privateSet(this, _source, source);
@@ -941,10 +935,12 @@ var SmplrImpl = class {
941
935
  __privateSet(this, _voices2, new VoiceManager());
942
936
  this.loader = (_c = options == null ? void 0 : options.loader) != null ? _c : SampleLoader(context, { storage: options == null ? void 0 : options.storage });
943
937
  if (json) {
944
- this.ready = this.loader.load(json, (loaded, total) => {
945
- var _a2;
946
- __privateSet(this, _loadProgress, { loaded, total });
947
- (_a2 = __privateGet(this, _onLoadProgress)) == null ? void 0 : _a2.call(this, { loaded, total });
938
+ this.ready = this.loader.load(json, {
939
+ onProgress: (loaded, total) => {
940
+ var _a2;
941
+ __privateSet(this, _loadProgress, { loaded, total });
942
+ (_a2 = __privateGet(this, _onLoadProgress)) == null ? void 0 : _a2.call(this, { loaded, total });
943
+ }
948
944
  }).then((buffers) => {
949
945
  __privateSet(this, _buffers, buffers);
950
946
  });
@@ -1683,6 +1679,9 @@ function fetchJSON(url, storage) {
1683
1679
  return r.json();
1684
1680
  });
1685
1681
  jsonCache.set(url, p);
1682
+ p.catch(() => {
1683
+ if (jsonCache.get(url) === p) jsonCache.delete(url);
1684
+ });
1686
1685
  }
1687
1686
  return p;
1688
1687
  }
@@ -2503,6 +2502,7 @@ var SequencerImpl = class {
2503
2502
  this._clock.stop();
2504
2503
  this._stopLoop();
2505
2504
  this._endScheduled = false;
2505
+ for (const stopFn of this._activeVoices.values()) stopFn();
2506
2506
  this._activeVoices.clear();
2507
2507
  this._emitStateChange("stopped");
2508
2508
  return this;
@@ -3006,8 +3006,6 @@ function buildRegion(props, pathFromSampleName) {
3006
3006
  if (tune !== void 0) region.tune = tune / 100;
3007
3007
  const ampRelease = num(props, "ampeg_release");
3008
3008
  if (ampRelease !== void 0) region.ampRelease = ampRelease;
3009
- const ampVelcurve = numArr(props, "amp_velcurve");
3010
- if (ampVelcurve) region.ampVelCurve = ampVelcurve;
3011
3009
  return region;
3012
3010
  }
3013
3011
  function resolveDefines(sfz) {
@@ -3085,16 +3083,18 @@ function str(props, key) {
3085
3083
  if (typeof v === "string") return v;
3086
3084
  return void 0;
3087
3085
  }
3088
- function numArr(props, _prefix) {
3089
- for (const [k, v] of Object.entries(props)) {
3090
- if (k.startsWith("amp_velcurve_")) {
3091
- const vel = Number(k.slice("amp_velcurve_".length));
3092
- if (!isNaN(vel) && typeof v === "number") {
3093
- return [vel, v];
3094
- }
3086
+
3087
+ // src/fetch-ok.ts
3088
+ function fetchOk(url) {
3089
+ return __async(this, null, function* () {
3090
+ const res = yield fetch(url);
3091
+ if (!res.ok) {
3092
+ throw new Error(
3093
+ `smplr: failed to fetch ${url} (${res.status} ${res.statusText})`
3094
+ );
3095
3095
  }
3096
- }
3097
- return void 0;
3096
+ return res;
3097
+ });
3098
3098
  }
3099
3099
 
3100
3100
  // src/tremolo.ts
@@ -3140,8 +3140,10 @@ function createTremolo(context, depth) {
3140
3140
  splitter.disconnect(ampR, 1);
3141
3141
  ampL.disconnect(merger, 0, 0);
3142
3142
  ampR.disconnect(merger, 0, 1);
3143
- lfoL.disconnect(ampL);
3144
- lfoR.disconnect(ampR);
3143
+ lfoL.disconnect(lfoLAmp);
3144
+ lfoLAmp.disconnect(ampL.gain);
3145
+ lfoR.disconnect(lfoRAmp);
3146
+ lfoRAmp.disconnect(ampR.gain);
3145
3147
  merger.disconnect(output);
3146
3148
  };
3147
3149
  return { input, output };
@@ -3194,7 +3196,7 @@ var ElectricPiano = Instrument(
3194
3196
  };
3195
3197
  const tremoloNode = createTremolo(ctx, depth.subscribe);
3196
3198
  smplr.output.addInsert(tremoloNode);
3197
- const ready = fetch(config.sfzUrl).then((r) => r.text()).then(
3199
+ const ready = fetchOk(config.sfzUrl).then((r) => r.text()).then(
3198
3200
  (sfzText) => {
3199
3201
  var _a;
3200
3202
  return smplr.loadInstrument(
@@ -3212,15 +3214,11 @@ var ElectricPiano = Instrument(
3212
3214
 
3213
3215
  // src/versilian.ts
3214
3216
  var VCSL_BASE_URL = "https://smpldsnds.github.io/sgossner-vcsl";
3215
- var instruments = [];
3217
+ var instrumentsPromise;
3216
3218
  function getVersilianInstruments() {
3217
- return __async(this, null, function* () {
3218
- if (instruments.length) return instruments;
3219
- instruments = yield fetch(VCSL_BASE_URL + "/sfz_files.json").then(
3220
- (res) => res.json()
3221
- );
3222
- return instruments;
3223
- });
3219
+ return instrumentsPromise != null ? instrumentsPromise : instrumentsPromise = fetchOk(
3220
+ VCSL_BASE_URL + "/sfz_files.json"
3221
+ ).then((res) => res.json());
3224
3222
  }
3225
3223
  var Versilian = Instrument(
3226
3224
  (ctx, options = {}, smplr) => loadVersilianInstrument(smplr, options)
@@ -3231,7 +3229,7 @@ function loadVersilianInstrument(smplr, options) {
3231
3229
  const sfzUrl = `${VCSL_BASE_URL}/${instrument}.sfz`;
3232
3230
  const base = instrument.slice(0, instrument.lastIndexOf("/") + 1);
3233
3231
  const sampleBaseUrl2 = `${VCSL_BASE_URL}/${base}`;
3234
- return fetch(sfzUrl).then((r) => r.text()).then(
3232
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3235
3233
  (sfzText) => smplr.loadInstrument(
3236
3234
  sfzToPreset(sfzText, {
3237
3235
  baseUrl: sampleBaseUrl2,
@@ -3336,7 +3334,7 @@ var Mellotron = Instrument(
3336
3334
  const variation = INSTRUMENT_VARIATIONS[instrument];
3337
3335
  const instrumentName = variation ? variation[0] : instrument;
3338
3336
  const baseUrl = `https://smpldsnds.github.io/archiveorg-mellotron/${instrumentName}/`;
3339
- return fetch(baseUrl + "files.json").then((r) => r.json()).then(
3337
+ return fetchOk(baseUrl + "files.json").then((r) => r.json()).then(
3340
3338
  (names) => smplr.loadInstrument(
3341
3339
  mellotronToPreset(names, {
3342
3340
  instrument: instrumentName,
@@ -3352,7 +3350,7 @@ function mellotronToPreset(sampleNames, config) {
3352
3350
  for (const sampleName of sampleNames) {
3353
3351
  if (config.variation && !sampleName.includes(config.variation)) continue;
3354
3352
  const midi = toMidi((_a = sampleName.split(" ")[0]) != null ? _a : "");
3355
- if (!midi) continue;
3353
+ if (midi === void 0) continue;
3356
3354
  entries.push([midi, sampleName]);
3357
3355
  }
3358
3356
  const spread = spreadKeyRanges(entries);
@@ -3405,7 +3403,7 @@ function createDattorroReverbEffect(context) {
3405
3403
  if (!ready) {
3406
3404
  const blob = new Blob([PROCESSOR], { type: "application/javascript" });
3407
3405
  const url = URL.createObjectURL(blob);
3408
- ready = context.audioWorklet.addModule(url);
3406
+ ready = context.audioWorklet.addModule(url).finally(() => URL.revokeObjectURL(url));
3409
3407
  init.set(context, ready);
3410
3408
  }
3411
3409
  yield ready;
@@ -3437,7 +3435,7 @@ var ReverbImpl = class {
3437
3435
  }
3438
3436
  getParam(name) {
3439
3437
  var _a;
3440
- return (_a = __privateGet(this, _effect)) == null ? void 0 : _a.parameters.get("preDelay");
3438
+ return (_a = __privateGet(this, _effect)) == null ? void 0 : _a.parameters.get(name);
3441
3439
  }
3442
3440
  get isReady() {
3443
3441
  return __privateGet(this, _effect) !== void 0;
@@ -3446,11 +3444,13 @@ var ReverbImpl = class {
3446
3444
  return __privateGet(this, _ready);
3447
3445
  }
3448
3446
  connect(output) {
3449
- if (__privateGet(this, _effect)) {
3450
- __privateGet(this, _effect).disconnect(__privateGet(this, _output));
3451
- __privateGet(this, _effect).connect(output);
3452
- }
3453
3447
  __privateSet(this, _output, output);
3448
+ __privateGet(this, _ready).then(() => {
3449
+ if (__privateGet(this, _effect)) {
3450
+ __privateGet(this, _effect).disconnect();
3451
+ __privateGet(this, _effect).connect(__privateGet(this, _output));
3452
+ }
3453
+ });
3454
3454
  }
3455
3455
  };
3456
3456
  _effect = new WeakMap();
@@ -3597,7 +3597,7 @@ var Smolken = Instrument(
3597
3597
  (ctx, options = {}, smplr) => {
3598
3598
  var _a;
3599
3599
  const sfzUrl = getSmolkenUrl((_a = options.instrument) != null ? _a : "Arco");
3600
- return fetch(sfzUrl).then((r) => r.text()).then(
3600
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3601
3601
  (sfzText) => smplr.loadInstrument(
3602
3602
  sfzToPreset(sfzText, {
3603
3603
  baseUrl: SMOLKEN_BASE_URL,
@@ -3758,16 +3758,14 @@ function fetchSoundfontLoopData(url, sampleRate = 44100) {
3758
3758
  return __async(this, null, function* () {
3759
3759
  if (!url) return void 0;
3760
3760
  try {
3761
- const req = yield fetch(url);
3762
- if (req.status !== 200) return;
3761
+ const req = yield fetchOk(url);
3763
3762
  const raw = yield req.json();
3764
3763
  const loopData = {};
3765
3764
  Object.keys(raw).forEach((key) => {
3766
3765
  const midi = toMidi(key);
3767
- if (midi) {
3768
- const offsets = raw[key];
3769
- loopData[midi] = [offsets[0] / sampleRate, offsets[1] / sampleRate];
3770
- }
3766
+ if (midi === void 0) return;
3767
+ const offsets = raw[key];
3768
+ loopData[midi] = [offsets[0] / sampleRate, offsets[1] / sampleRate];
3771
3769
  });
3772
3770
  return loopData;
3773
3771
  } catch (err) {
@@ -3812,7 +3810,7 @@ function decodeSoundfontFile(context, config) {
3812
3810
  yield Promise.all(
3813
3811
  noteNames.map((noteName) => __async(null, null, function* () {
3814
3812
  const midi = toMidi(noteName);
3815
- if (!midi) return;
3813
+ if (midi === void 0) return;
3816
3814
  try {
3817
3815
  const audioData = base64ToArrayBuffer(
3818
3816
  removeBase64Prefix(json[noteName])
@@ -3913,7 +3911,7 @@ function removeBase64Prefix(audioBase64) {
3913
3911
  return audioBase64.slice(audioBase64.indexOf(",") + 1);
3914
3912
  }
3915
3913
  function base64ToArrayBuffer(base64) {
3916
- const decoded = window.atob(base64);
3914
+ const decoded = atob(base64);
3917
3915
  const len = decoded.length;
3918
3916
  const bytes = new Uint8Array(len);
3919
3917
  for (let i = 0; i < len; i++) {
@@ -3970,7 +3968,11 @@ var Soundfont2 = Instrument(
3970
3968
  const sf2inst = soundfont == null ? void 0 : soundfont.instruments.find(
3971
3969
  (inst) => inst.header.name === instrumentName
3972
3970
  );
3973
- if (!sf2inst) return void 0;
3971
+ if (!sf2inst) {
3972
+ throw new Error(
3973
+ `Soundfont2: instrument "${instrumentName}" not found`
3974
+ );
3975
+ }
3974
3976
  const { json, buffers } = sf2InstrumentToPreset(sf2inst, ctx);
3975
3977
  return baseLoadInstrument(json, buffers);
3976
3978
  }