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.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;
@@ -208,68 +215,90 @@ type VoiceParams = {
208
215
  reverse?: boolean;
209
216
  };
210
217
 
218
+ /** Options accepted by `SampleLoader(context, options)`. */
219
+ type SampleLoaderOptions = {
220
+ /** Custom storage backend (e.g. `CacheStorage` for offline). Defaults to `HttpStorage`. */
221
+ storage?: Storage;
222
+ };
223
+ /** Options accepted by `loader.load(json, options)`. */
224
+ type SampleLoaderLoadOptions = {
225
+ /** Pre-decoded buffers keyed by sample name — skip fetch for these. */
226
+ buffers?: Map<string, AudioBuffer>;
227
+ /** Called once per sample (including cache hits) with cumulative progress. */
228
+ onProgress?: (loaded: number, total: number) => void;
229
+ };
230
+ type SampleLoaderFactory = {
231
+ (context: BaseAudioContext, options?: SampleLoaderOptions): SampleLoader;
232
+ /** @deprecated Call as a function: `SampleLoader(...)` instead of `new SampleLoader(...)`. */
233
+ new (context: BaseAudioContext, options?: SampleLoaderOptions): SampleLoader;
234
+ };
211
235
  /**
212
- * Loads and caches AudioBuffers for all samples referenced in a SmplrPreset.
213
- *
214
- * The cache is keyed by resolved URL, so the same audio file is never fetched
215
- * or decoded twice. Multiple Smplr instances can share one SampleLoader by
216
- * passing it via SmplrOptions.loader.
236
+ * Loads and decodes AudioBuffers for the samples referenced by a {@link SmplrPreset}.
237
+ * Used internally by every smplr instrument; pass an instance via
238
+ * {@link SmplrOptions.loader} to share buffer caching across multiple instruments.
217
239
  */
218
- declare class SampleLoaderImpl {
219
- #private;
220
- constructor(context: BaseAudioContext, options?: {
221
- storage?: Storage;
222
- });
240
+ interface SampleLoader {
223
241
  /**
224
- * Load all samples referenced in `json`. Returns a Map of sample name →
225
- * AudioBuffer. Progress is reported via `onProgress` callback or via
226
- * options object.
242
+ * Load all samples referenced by `json`. Returns a Map keyed by sample
243
+ * name (`region.sample`), values are decoded `AudioBuffer`s. Failed
244
+ * samples are silently omitted (callers handle absence at lookup time).
245
+ *
246
+ * Internally cached by resolved URL, so repeated calls with the same
247
+ * baseUrl/format/path do not re-fetch.
227
248
  *
228
- * - `buffers` in options: pre-loaded buffers skips fetch for these names.
229
- * - All samples load in parallel. Failed samples are silently omitted.
249
+ * @param json The preset describing samples to load.
250
+ * @param options
251
+ * - `buffers`: pre-decoded buffers keyed by sample name — skip fetch for these.
252
+ * - `onProgress`: called with `(loaded, total)` per sample (including cache hits).
230
253
  */
231
- load(json: SmplrPreset, onProgressOrOptions?: ((loaded: number, total: number) => void) | {
232
- buffers?: Map<string, AudioBuffer>;
233
- onProgress?: (loaded: number, total: number) => void;
234
- }): Promise<Map<string, AudioBuffer>>;
254
+ load(json: SmplrPreset, options?: SampleLoaderLoadOptions): Promise<Map<string, AudioBuffer>>;
255
+ /**
256
+ * @deprecated Pass `{ onProgress }` instead. The bare-callback form is kept
257
+ * for compatibility; the options form is the canonical 1.x signature.
258
+ */
259
+ load(json: SmplrPreset, onProgress: (loaded: number, total: number) => void): Promise<Map<string, AudioBuffer>>;
235
260
  }
236
- declare const SampleLoader: Constructable<[context: BaseAudioContext, options?: {
237
- storage?: Storage;
238
- } | undefined], SampleLoaderImpl>;
239
- type SampleLoader = ReturnType<typeof SampleLoader>;
261
+ declare const SampleLoader: SampleLoaderFactory;
240
262
 
263
+ /** Options accepted by `Scheduler(context, options)`. */
264
+ type SchedulerOptions = {
265
+ /**
266
+ * How far ahead of `currentTime` events are dispatched synchronously.
267
+ * Defaults to 200ms.
268
+ */
269
+ lookaheadMs?: number;
270
+ /**
271
+ * How often the queue is polled for events ready to dispatch.
272
+ * Defaults to 50ms.
273
+ */
274
+ intervalMs?: number;
275
+ };
276
+ type SchedulerFactory = {
277
+ (context: BaseAudioContext, options?: SchedulerOptions): Scheduler;
278
+ /** @deprecated Call as a function: `Scheduler(...)` instead of `new Scheduler(...)`. */
279
+ new (context: BaseAudioContext, options?: SchedulerOptions): Scheduler;
280
+ };
241
281
  /**
242
- * Standalone scheduler. Dispatches NoteEvents immediately when they fall within the
243
- * lookahead window, or queues them for future dispatch via a self-managing interval.
244
- *
245
- * Multiple Smplr instances can share a single Scheduler for coordinated timing.
282
+ * Schedules note events for future dispatch. Used internally by every smplr
283
+ * instrument; pass an instance via {@link SmplrOptions.scheduler} to share one
284
+ * scheduler across multiple instruments.
246
285
  */
247
- declare class SchedulerImpl {
248
- #private;
249
- constructor(context: BaseAudioContext, options?: {
250
- lookaheadMs?: number;
251
- intervalMs?: number;
252
- });
286
+ interface Scheduler {
253
287
  /**
254
- * Schedule a callback for a NoteEvent.
288
+ * Dispatch `callback` at `event.time`. If `event.time` is within the
289
+ * scheduler's lookahead window (or omitted), the callback fires synchronously
290
+ * and the returned {@link StopFn} is a no-op. Otherwise the event is queued.
255
291
  *
256
- * - If the event's time falls within the lookahead window (or has no time), the
257
- * callback is called synchronously and a no-op StopFn is returned.
258
- * - Otherwise the event is queued, the interval is started if needed, and a StopFn
259
- * is returned that removes the event from the queue before it is dispatched.
292
+ * The returned function removes the event from the queue before dispatch.
260
293
  */
261
294
  schedule(event: NoteEvent, callback: (event: NoteEvent) => void): StopFn;
262
295
  /**
263
- * Clear all queued (not-yet-dispatched) events and stop the interval.
264
- * Does not affect voices that are already playing.
296
+ * Clear all queued (not-yet-dispatched) events and stop the polling
297
+ * interval. Does not affect voices already playing.
265
298
  */
266
299
  stop(): void;
267
300
  }
268
- declare const Scheduler: Constructable<[context: BaseAudioContext, options?: {
269
- lookaheadMs?: number;
270
- intervalMs?: number;
271
- } | undefined], SchedulerImpl>;
272
- type Scheduler = ReturnType<typeof Scheduler>;
301
+ declare const Scheduler: SchedulerFactory;
273
302
 
274
303
  type SmplrOptions = {
275
304
  /** Custom storage backend for sample fetching (e.g. CacheStorage). */
@@ -1208,7 +1237,7 @@ declare function sf2InstrumentToPreset(sf2Instrument: Sf2Instrument, context: Ba
1208
1237
  };
1209
1238
  type Soundfont2SamplerExtras = {
1210
1239
  readonly instrumentNames: string[];
1211
- loadInstrument(instrumentName: string): Promise<void> | undefined;
1240
+ loadInstrument(instrumentName: string): Promise<void>;
1212
1241
  };
1213
1242
  declare const Soundfont2: InstrumentFactory<Soundfont2Options, Soundfont2SamplerExtras>;
1214
1243
  /** Instance type returned by the {@link Soundfont2} factory. */
@@ -1273,4 +1302,4 @@ declare const LAYERS: ({
1273
1302
  cutoff?: undefined;
1274
1303
  })[];
1275
1304
 
1276
- export { type AddTrackOptions, CacheStorage, DRUM_ABUSE_PACKS, DrumAbuse, type DrumAbuseConfig, type DrumAbuseExtras, type DrumAbuseOptions, type DrumAbusePackId, type DrumAbuseSource, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, Instrument, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PatternInput, type PlaybackParams, type RenderOfflineOptions, RenderResult, Reverb, SampleLoader, Sampler, type SamplerConfig, type SamplerReloadInput, Scheduler, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, type Smplr, type SmplrGroup, type SmplrOptions, type SmplrPlugin, type SmplrPreset, type SmplrRegion, type SmplrSamples, Soundfont, Soundfont2, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type StopFn, type StopTarget, type Storage, type StorageResponse, type TimeSignature, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, audioBufferToWav, audioBufferToWav16, drumAbuseSampleUrl, drumMachineToPreset, getDrumAbuseMachineNames, getDrumAbuseMachinePack, getDrumAbuseMachinesForPack, getDrumAbusePackNames, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, loadVersilianInstrument, mellotronToPreset, pianoToPreset, renderOffline, samplerToPreset, sf2InstrumentToPreset, soundfontToPreset, trimSilence };
1305
+ export { type AddTrackOptions, CacheStorage, DRUM_ABUSE_PACKS, DrumAbuse, type DrumAbuseConfig, type DrumAbuseExtras, type DrumAbuseOptions, type DrumAbusePackId, type DrumAbuseSource, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, Instrument, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PatternInput, type PlaybackParams, type RenderOfflineOptions, RenderResult, Reverb, SampleLoader, type SampleLoaderLoadOptions, type SampleLoaderOptions, Sampler, type SamplerConfig, type SamplerReloadInput, Scheduler, type SchedulerOptions, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, type Smplr, type SmplrGroup, type SmplrOptions, type SmplrPlugin, type SmplrPreset, type SmplrRegion, type SmplrSamples, Soundfont, Soundfont2, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type StopFn, type StopTarget, type Storage, type StorageResponse, type TimeSignature, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, audioBufferToWav, audioBufferToWav16, drumAbuseSampleUrl, drumMachineToPreset, getDrumAbuseMachineNames, getDrumAbuseMachinePack, getDrumAbuseMachinesForPack, getDrumAbusePackNames, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, loadVersilianInstrument, mellotronToPreset, pianoToPreset, renderOffline, samplerToPreset, sf2InstrumentToPreset, soundfontToPreset, trimSilence };
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)) {
@@ -386,7 +401,7 @@ function pickPlaybackParams(obj) {
386
401
  return result;
387
402
  }
388
403
  function resolveParams(defaults, group, region, midi, velocity, overrides) {
389
- var _a, _b, _c, _d, _e;
404
+ var _a, _b, _c, _d, _e, _f;
390
405
  const merged = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, PARAM_DEFAULTS), defaults), pickPlaybackParams(group)), pickPlaybackParams(region));
391
406
  const pitch = (_b = (_a = region.pitch) != null ? _a : region.key) != null ? _b : midi;
392
407
  const semitones = midi - pitch;
@@ -403,9 +418,8 @@ 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
- reverse: overrides == null ? void 0 : overrides.reverse
422
+ reverse: (_f = overrides == null ? void 0 : overrides.reverse) != null ? _f : merged.reverse
409
423
  };
410
424
  }
411
425
 
@@ -583,14 +597,6 @@ var SampleLoaderImpl = class {
583
597
  __privateSet(this, _context, context);
584
598
  __privateSet(this, _storage, (_a = options == null ? void 0 : options.storage) != null ? _a : HttpStorage);
585
599
  }
586
- /**
587
- * Load all samples referenced in `json`. Returns a Map of sample name →
588
- * AudioBuffer. Progress is reported via `onProgress` callback or via
589
- * options object.
590
- *
591
- * - `buffers` in options: pre-loaded buffers — skips fetch for these names.
592
- * - All samples load in parallel. Failed samples are silently omitted.
593
- */
594
600
  load(json, onProgressOrOptions) {
595
601
  return __async(this, null, function* () {
596
602
  var _a, _b;
@@ -710,14 +716,6 @@ var SchedulerImpl = class {
710
716
  __privateSet(this, _intervalMs, (_b = options == null ? void 0 : options.intervalMs) != null ? _b : INTERVAL_MS_DEFAULT);
711
717
  __privateSet(this, _queue, new SortedQueue((a, b) => a.time - b.time));
712
718
  }
713
- /**
714
- * Schedule a callback for a NoteEvent.
715
- *
716
- * - If the event's time falls within the lookahead window (or has no time), the
717
- * callback is called synchronously and a no-op StopFn is returned.
718
- * - Otherwise the event is queued, the interval is started if needed, and a StopFn
719
- * is returned that removes the event from the queue before it is dispatched.
720
- */
721
719
  schedule(event, callback) {
722
720
  var _a;
723
721
  const now = __privateGet(this, _context2).currentTime;
@@ -733,10 +731,6 @@ var SchedulerImpl = class {
733
731
  __privateGet(this, _queue).removeAll((q) => q === item);
734
732
  };
735
733
  }
736
- /**
737
- * Clear all queued (not-yet-dispatched) events and stop the interval.
738
- * Does not affect voices that are already playing.
739
- */
740
734
  stop() {
741
735
  __privateGet(this, _queue).clear();
742
736
  if (__privateGet(this, _intervalId) !== void 0) {
@@ -826,7 +820,7 @@ var Voice = class {
826
820
  __privateSet(this, _startAt, startAt);
827
821
  let offsetSec = 0;
828
822
  if (params.offset > 0) {
829
- offsetSec = params.reverse ? (buffer.length - params.offset) / buffer.sampleRate : params.offset / buffer.sampleRate;
823
+ offsetSec = params.reverse ? buffer.duration - params.offset : params.offset;
830
824
  }
831
825
  source.start(startAt, offsetSec);
832
826
  __privateSet(this, _source, source);
@@ -1009,10 +1003,12 @@ var SmplrImpl = class {
1009
1003
  __privateSet(this, _voices2, new VoiceManager());
1010
1004
  this.loader = (_c = options == null ? void 0 : options.loader) != null ? _c : SampleLoader(context, { storage: options == null ? void 0 : options.storage });
1011
1005
  if (json) {
1012
- this.ready = this.loader.load(json, (loaded, total) => {
1013
- var _a2;
1014
- __privateSet(this, _loadProgress, { loaded, total });
1015
- (_a2 = __privateGet(this, _onLoadProgress)) == null ? void 0 : _a2.call(this, { loaded, total });
1006
+ this.ready = this.loader.load(json, {
1007
+ onProgress: (loaded, total) => {
1008
+ var _a2;
1009
+ __privateSet(this, _loadProgress, { loaded, total });
1010
+ (_a2 = __privateGet(this, _onLoadProgress)) == null ? void 0 : _a2.call(this, { loaded, total });
1011
+ }
1016
1012
  }).then((buffers) => {
1017
1013
  __privateSet(this, _buffers, buffers);
1018
1014
  });
@@ -1751,6 +1747,9 @@ function fetchJSON(url, storage) {
1751
1747
  return r.json();
1752
1748
  });
1753
1749
  jsonCache.set(url, p);
1750
+ p.catch(() => {
1751
+ if (jsonCache.get(url) === p) jsonCache.delete(url);
1752
+ });
1754
1753
  }
1755
1754
  return p;
1756
1755
  }
@@ -2571,6 +2570,7 @@ var SequencerImpl = class {
2571
2570
  this._clock.stop();
2572
2571
  this._stopLoop();
2573
2572
  this._endScheduled = false;
2573
+ for (const stopFn of this._activeVoices.values()) stopFn();
2574
2574
  this._activeVoices.clear();
2575
2575
  this._emitStateChange("stopped");
2576
2576
  return this;
@@ -3074,8 +3074,6 @@ function buildRegion(props, pathFromSampleName) {
3074
3074
  if (tune !== void 0) region.tune = tune / 100;
3075
3075
  const ampRelease = num(props, "ampeg_release");
3076
3076
  if (ampRelease !== void 0) region.ampRelease = ampRelease;
3077
- const ampVelcurve = numArr(props, "amp_velcurve");
3078
- if (ampVelcurve) region.ampVelCurve = ampVelcurve;
3079
3077
  return region;
3080
3078
  }
3081
3079
  function resolveDefines(sfz) {
@@ -3153,16 +3151,18 @@ function str(props, key) {
3153
3151
  if (typeof v === "string") return v;
3154
3152
  return void 0;
3155
3153
  }
3156
- function numArr(props, _prefix) {
3157
- for (const [k, v] of Object.entries(props)) {
3158
- if (k.startsWith("amp_velcurve_")) {
3159
- const vel = Number(k.slice("amp_velcurve_".length));
3160
- if (!isNaN(vel) && typeof v === "number") {
3161
- return [vel, v];
3162
- }
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
+ );
3163
3163
  }
3164
- }
3165
- return void 0;
3164
+ return res;
3165
+ });
3166
3166
  }
3167
3167
 
3168
3168
  // src/tremolo.ts
@@ -3208,8 +3208,10 @@ function createTremolo(context, depth) {
3208
3208
  splitter.disconnect(ampR, 1);
3209
3209
  ampL.disconnect(merger, 0, 0);
3210
3210
  ampR.disconnect(merger, 0, 1);
3211
- lfoL.disconnect(ampL);
3212
- lfoR.disconnect(ampR);
3211
+ lfoL.disconnect(lfoLAmp);
3212
+ lfoLAmp.disconnect(ampL.gain);
3213
+ lfoR.disconnect(lfoRAmp);
3214
+ lfoRAmp.disconnect(ampR.gain);
3213
3215
  merger.disconnect(output);
3214
3216
  };
3215
3217
  return { input, output };
@@ -3262,7 +3264,7 @@ var ElectricPiano = Instrument(
3262
3264
  };
3263
3265
  const tremoloNode = createTremolo(ctx, depth.subscribe);
3264
3266
  smplr.output.addInsert(tremoloNode);
3265
- const ready = fetch(config.sfzUrl).then((r) => r.text()).then(
3267
+ const ready = fetchOk(config.sfzUrl).then((r) => r.text()).then(
3266
3268
  (sfzText) => {
3267
3269
  var _a;
3268
3270
  return smplr.loadInstrument(
@@ -3280,15 +3282,11 @@ var ElectricPiano = Instrument(
3280
3282
 
3281
3283
  // src/versilian.ts
3282
3284
  var VCSL_BASE_URL = "https://smpldsnds.github.io/sgossner-vcsl";
3283
- var instruments = [];
3285
+ var instrumentsPromise;
3284
3286
  function getVersilianInstruments() {
3285
- return __async(this, null, function* () {
3286
- if (instruments.length) return instruments;
3287
- instruments = yield fetch(VCSL_BASE_URL + "/sfz_files.json").then(
3288
- (res) => res.json()
3289
- );
3290
- return instruments;
3291
- });
3287
+ return instrumentsPromise != null ? instrumentsPromise : instrumentsPromise = fetchOk(
3288
+ VCSL_BASE_URL + "/sfz_files.json"
3289
+ ).then((res) => res.json());
3292
3290
  }
3293
3291
  var Versilian = Instrument(
3294
3292
  (ctx, options = {}, smplr) => loadVersilianInstrument(smplr, options)
@@ -3299,7 +3297,7 @@ function loadVersilianInstrument(smplr, options) {
3299
3297
  const sfzUrl = `${VCSL_BASE_URL}/${instrument}.sfz`;
3300
3298
  const base = instrument.slice(0, instrument.lastIndexOf("/") + 1);
3301
3299
  const sampleBaseUrl2 = `${VCSL_BASE_URL}/${base}`;
3302
- return fetch(sfzUrl).then((r) => r.text()).then(
3300
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3303
3301
  (sfzText) => smplr.loadInstrument(
3304
3302
  sfzToPreset(sfzText, {
3305
3303
  baseUrl: sampleBaseUrl2,
@@ -3404,7 +3402,7 @@ var Mellotron = Instrument(
3404
3402
  const variation = INSTRUMENT_VARIATIONS[instrument];
3405
3403
  const instrumentName = variation ? variation[0] : instrument;
3406
3404
  const baseUrl = `https://smpldsnds.github.io/archiveorg-mellotron/${instrumentName}/`;
3407
- return fetch(baseUrl + "files.json").then((r) => r.json()).then(
3405
+ return fetchOk(baseUrl + "files.json").then((r) => r.json()).then(
3408
3406
  (names) => smplr.loadInstrument(
3409
3407
  mellotronToPreset(names, {
3410
3408
  instrument: instrumentName,
@@ -3420,7 +3418,7 @@ function mellotronToPreset(sampleNames, config) {
3420
3418
  for (const sampleName of sampleNames) {
3421
3419
  if (config.variation && !sampleName.includes(config.variation)) continue;
3422
3420
  const midi = toMidi((_a = sampleName.split(" ")[0]) != null ? _a : "");
3423
- if (!midi) continue;
3421
+ if (midi === void 0) continue;
3424
3422
  entries.push([midi, sampleName]);
3425
3423
  }
3426
3424
  const spread = spreadKeyRanges(entries);
@@ -3473,7 +3471,7 @@ function createDattorroReverbEffect(context) {
3473
3471
  if (!ready) {
3474
3472
  const blob = new Blob([PROCESSOR], { type: "application/javascript" });
3475
3473
  const url = URL.createObjectURL(blob);
3476
- ready = context.audioWorklet.addModule(url);
3474
+ ready = context.audioWorklet.addModule(url).finally(() => URL.revokeObjectURL(url));
3477
3475
  init.set(context, ready);
3478
3476
  }
3479
3477
  yield ready;
@@ -3505,7 +3503,7 @@ var ReverbImpl = class {
3505
3503
  }
3506
3504
  getParam(name) {
3507
3505
  var _a;
3508
- return (_a = __privateGet(this, _effect)) == null ? void 0 : _a.parameters.get("preDelay");
3506
+ return (_a = __privateGet(this, _effect)) == null ? void 0 : _a.parameters.get(name);
3509
3507
  }
3510
3508
  get isReady() {
3511
3509
  return __privateGet(this, _effect) !== void 0;
@@ -3514,11 +3512,13 @@ var ReverbImpl = class {
3514
3512
  return __privateGet(this, _ready);
3515
3513
  }
3516
3514
  connect(output) {
3517
- if (__privateGet(this, _effect)) {
3518
- __privateGet(this, _effect).disconnect(__privateGet(this, _output));
3519
- __privateGet(this, _effect).connect(output);
3520
- }
3521
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
+ });
3522
3522
  }
3523
3523
  };
3524
3524
  _effect = new WeakMap();
@@ -3665,7 +3665,7 @@ var Smolken = Instrument(
3665
3665
  (ctx, options = {}, smplr) => {
3666
3666
  var _a;
3667
3667
  const sfzUrl = getSmolkenUrl((_a = options.instrument) != null ? _a : "Arco");
3668
- return fetch(sfzUrl).then((r) => r.text()).then(
3668
+ return fetchOk(sfzUrl).then((r) => r.text()).then(
3669
3669
  (sfzText) => smplr.loadInstrument(
3670
3670
  sfzToPreset(sfzText, {
3671
3671
  baseUrl: SMOLKEN_BASE_URL,
@@ -3826,16 +3826,14 @@ function fetchSoundfontLoopData(url, sampleRate = 44100) {
3826
3826
  return __async(this, null, function* () {
3827
3827
  if (!url) return void 0;
3828
3828
  try {
3829
- const req = yield fetch(url);
3830
- if (req.status !== 200) return;
3829
+ const req = yield fetchOk(url);
3831
3830
  const raw = yield req.json();
3832
3831
  const loopData = {};
3833
3832
  Object.keys(raw).forEach((key) => {
3834
3833
  const midi = toMidi(key);
3835
- if (midi) {
3836
- const offsets = raw[key];
3837
- loopData[midi] = [offsets[0] / sampleRate, offsets[1] / sampleRate];
3838
- }
3834
+ if (midi === void 0) return;
3835
+ const offsets = raw[key];
3836
+ loopData[midi] = [offsets[0] / sampleRate, offsets[1] / sampleRate];
3839
3837
  });
3840
3838
  return loopData;
3841
3839
  } catch (err) {
@@ -3880,7 +3878,7 @@ function decodeSoundfontFile(context, config) {
3880
3878
  yield Promise.all(
3881
3879
  noteNames.map((noteName) => __async(null, null, function* () {
3882
3880
  const midi = toMidi(noteName);
3883
- if (!midi) return;
3881
+ if (midi === void 0) return;
3884
3882
  try {
3885
3883
  const audioData = base64ToArrayBuffer(
3886
3884
  removeBase64Prefix(json[noteName])
@@ -3981,7 +3979,7 @@ function removeBase64Prefix(audioBase64) {
3981
3979
  return audioBase64.slice(audioBase64.indexOf(",") + 1);
3982
3980
  }
3983
3981
  function base64ToArrayBuffer(base64) {
3984
- const decoded = window.atob(base64);
3982
+ const decoded = atob(base64);
3985
3983
  const len = decoded.length;
3986
3984
  const bytes = new Uint8Array(len);
3987
3985
  for (let i = 0; i < len; i++) {
@@ -4038,7 +4036,11 @@ var Soundfont2 = Instrument(
4038
4036
  const sf2inst = soundfont == null ? void 0 : soundfont.instruments.find(
4039
4037
  (inst) => inst.header.name === instrumentName
4040
4038
  );
4041
- if (!sf2inst) return void 0;
4039
+ if (!sf2inst) {
4040
+ throw new Error(
4041
+ `Soundfont2: instrument "${instrumentName}" not found`
4042
+ );
4043
+ }
4042
4044
  const { json, buffers } = sf2InstrumentToPreset(sf2inst, ctx);
4043
4045
  return baseLoadInstrument(json, buffers);
4044
4046
  }