smplr 0.19.0 → 0.20.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
@@ -522,6 +522,84 @@ const seq = new Sequencer(context, {
522
522
 
523
523
  ---
524
524
 
525
+ ## Export Audio
526
+
527
+ Render audio offline (faster than real-time) and export it as a WAV file. Uses `OfflineAudioContext` under the hood.
528
+
529
+ ```js
530
+ import { renderOffline } from "smplr";
531
+
532
+ const result = await renderOffline(async (context) => {
533
+ const piano = await new SplendidGrandPiano(context).load;
534
+ piano.start({ note: "C4", time: 0, duration: 1 });
535
+ piano.start({ note: "E4", time: 0.5, duration: 1 });
536
+ });
537
+
538
+ result.downloadWav("export.wav");
539
+ ```
540
+
541
+ #### Options
542
+
543
+ ```js
544
+ const result = await renderOffline(callback, {
545
+ duration: 10, // Total duration in seconds (auto-detected if omitted)
546
+ sampleRate: 48000, // Sample rate (default: 48000)
547
+ channels: 2, // Number of channels (default: 2)
548
+ });
549
+ ```
550
+
551
+ 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.
552
+
553
+ #### RenderResult
554
+
555
+ `renderOffline` returns a `RenderResult` object:
556
+
557
+ - `result.audioBuffer` — the raw `AudioBuffer`
558
+ - `result.toWav()` — encode as 32-bit float WAV `Blob` (lossless)
559
+ - `result.toWav16()` — encode as 16-bit integer WAV `Blob` (smaller file)
560
+ - `result.downloadWav(filename?)` — download as 32-bit WAV
561
+ - `result.downloadWav16(filename?)` — download as 16-bit WAV
562
+ - `result.duration` — actual duration in seconds
563
+ - `result.sampleRate` — sample rate used
564
+
565
+ WAV encoding is lazy — it only happens when you call `toWav()` or `toWav16()`.
566
+
567
+ #### Buffer reuse
568
+
569
+ If you already have an instrument loaded, pass the same `SampleLoader` to avoid re-fetching samples:
570
+
571
+ ```js
572
+ import { SplendidGrandPiano, SampleLoader, renderOffline } from "smplr";
573
+
574
+ const loader = new SampleLoader(audioContext);
575
+ const piano = new SplendidGrandPiano(audioContext, { loader });
576
+ await piano.load;
577
+
578
+ // Offline render reuses cached buffers — no re-fetch
579
+ const result = await renderOffline(async (context) => {
580
+ const offlinePiano = await new SplendidGrandPiano(context, { loader }).load;
581
+ offlinePiano.start({ note: "C4", time: 0, duration: 1 });
582
+ });
583
+ ```
584
+
585
+ #### Bug reports
586
+
587
+ 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:
588
+
589
+ ```js
590
+ const { renderOffline, SplendidGrandPiano } = await import("https://esm.sh/smplr");
591
+
592
+ const result = await renderOffline(async (context) => {
593
+ const piano = await new SplendidGrandPiano(context).load;
594
+ piano.start({ note: "C4", time: 0, duration: 2 });
595
+ });
596
+ result.downloadWav16("bug-report.wav");
597
+ ```
598
+
599
+ This will download a WAV file you can attach to your issue or pull request.
600
+
601
+ ---
602
+
525
603
  ## Instruments
526
604
 
527
605
  ### Sampler
package/dist/index.d.mts CHANGED
@@ -277,10 +277,10 @@ declare class Smplr {
277
277
  #private;
278
278
  /** Resolves with `this` once all sample buffers are loaded. */
279
279
  readonly load: Promise<Smplr>;
280
- /** The AudioContext passed to the constructor. */
281
- readonly context: AudioContext;
282
- constructor(context: AudioContext, json: SmplrJson, options?: SmplrOptions);
283
- constructor(context: AudioContext, options?: SmplrOptions);
280
+ /** The AudioContext (or OfflineAudioContext) passed to the constructor. */
281
+ readonly context: BaseAudioContext;
282
+ constructor(context: BaseAudioContext, json: SmplrJson, options?: SmplrOptions);
283
+ constructor(context: BaseAudioContext, options?: SmplrOptions);
284
284
  /**
285
285
  * Load (or replace) the instrument descriptor. Creates a new RegionMatcher
286
286
  * and fetches all sample buffers. Pre-loaded buffers (e.g. base64-decoded)
@@ -384,6 +384,73 @@ declare class DrumMachine {
384
384
  */
385
385
  declare function drumMachineToSmplrJson(instrument: DrumMachineInstrument): SmplrJson;
386
386
 
387
+ /**
388
+ * The result of an offline render. Provides the raw AudioBuffer and
389
+ * lazy WAV encoding / download convenience methods.
390
+ */
391
+ declare class RenderResult {
392
+ #private;
393
+ readonly audioBuffer: AudioBuffer;
394
+ readonly duration: number;
395
+ readonly sampleRate: number;
396
+ constructor(audioBuffer: AudioBuffer);
397
+ /** Encode as 32-bit float WAV. Cached after first call. */
398
+ toWav(): Blob;
399
+ /** Encode as 16-bit integer WAV. Cached after first call. */
400
+ toWav16(): Blob;
401
+ /** Download as 32-bit float WAV file. */
402
+ downloadWav(filename?: string): void;
403
+ /** Download as 16-bit integer WAV file. */
404
+ downloadWav16(filename?: string): void;
405
+ }
406
+
407
+ interface RenderOfflineOptions {
408
+ /** Total duration in seconds. When omitted, uses 60s max and trims trailing silence. */
409
+ duration?: number;
410
+ /** Sample rate. Default: 48000. */
411
+ sampleRate?: number;
412
+ /** Number of output channels. Default: 2 (stereo). */
413
+ channels?: number;
414
+ }
415
+ /**
416
+ * Render audio offline using an OfflineAudioContext.
417
+ *
418
+ * The callback receives an OfflineAudioContext. Create instruments,
419
+ * schedule notes using absolute times (starting from 0), then return.
420
+ * The audio is rendered as fast as possible (not real-time).
421
+ *
422
+ * Returns a RenderResult with the rendered AudioBuffer and
423
+ * convenience methods for WAV encoding and download.
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * const result = await renderOffline(async (context) => {
428
+ * const piano = await new SplendidGrandPiano(context).load;
429
+ * piano.start({ note: "C4", time: 0, duration: 1 });
430
+ * });
431
+ * result.downloadWav("export.wav");
432
+ * ```
433
+ */
434
+ declare function renderOffline(callback: (context: OfflineAudioContext) => Promise<void>, options?: RenderOfflineOptions): Promise<RenderResult>;
435
+
436
+ /**
437
+ * Encode an AudioBuffer as a WAV file Blob.
438
+ *
439
+ * Supports 32-bit float (lossless) and 16-bit integer (CD quality) formats.
440
+ */
441
+ /** Encode AudioBuffer as 32-bit float WAV. */
442
+ declare function audioBufferToWav(buffer: AudioBuffer): Blob;
443
+ /** Encode AudioBuffer as 16-bit integer WAV. */
444
+ declare function audioBufferToWav16(buffer: AudioBuffer): Blob;
445
+
446
+ /**
447
+ * Trim trailing silence from an AudioBuffer.
448
+ *
449
+ * Scans all channels from the end to find the last sample above the threshold,
450
+ * then returns a new AudioBuffer trimmed to that length.
451
+ */
452
+ declare function trimSilence(buffer: AudioBuffer): AudioBuffer;
453
+
387
454
  /**
388
455
  * TransportClock
389
456
  *
@@ -917,4 +984,4 @@ declare const LAYERS: ({
917
984
  cutoff?: undefined;
918
985
  })[];
919
986
 
920
- export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PlaybackParams, Reverb, SampleLoader, Sampler, type SamplerConfig, Scheduler, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, Smplr, type SmplrGroup, type SmplrJson, type SmplrOptions, type SmplrRegion, type SmplrSamples, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type SpreadResult, type StopFn, type StopTarget, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges };
987
+ export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PlaybackParams, type RenderOfflineOptions, RenderResult, Reverb, SampleLoader, Sampler, type SamplerConfig, Scheduler, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, Smplr, type SmplrGroup, type SmplrJson, type SmplrOptions, type SmplrRegion, type SmplrSamples, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type SpreadResult, type StopFn, type StopTarget, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, audioBufferToWav, audioBufferToWav16, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, renderOffline, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges, trimSilence };
package/dist/index.d.ts CHANGED
@@ -277,10 +277,10 @@ declare class Smplr {
277
277
  #private;
278
278
  /** Resolves with `this` once all sample buffers are loaded. */
279
279
  readonly load: Promise<Smplr>;
280
- /** The AudioContext passed to the constructor. */
281
- readonly context: AudioContext;
282
- constructor(context: AudioContext, json: SmplrJson, options?: SmplrOptions);
283
- constructor(context: AudioContext, options?: SmplrOptions);
280
+ /** The AudioContext (or OfflineAudioContext) passed to the constructor. */
281
+ readonly context: BaseAudioContext;
282
+ constructor(context: BaseAudioContext, json: SmplrJson, options?: SmplrOptions);
283
+ constructor(context: BaseAudioContext, options?: SmplrOptions);
284
284
  /**
285
285
  * Load (or replace) the instrument descriptor. Creates a new RegionMatcher
286
286
  * and fetches all sample buffers. Pre-loaded buffers (e.g. base64-decoded)
@@ -384,6 +384,73 @@ declare class DrumMachine {
384
384
  */
385
385
  declare function drumMachineToSmplrJson(instrument: DrumMachineInstrument): SmplrJson;
386
386
 
387
+ /**
388
+ * The result of an offline render. Provides the raw AudioBuffer and
389
+ * lazy WAV encoding / download convenience methods.
390
+ */
391
+ declare class RenderResult {
392
+ #private;
393
+ readonly audioBuffer: AudioBuffer;
394
+ readonly duration: number;
395
+ readonly sampleRate: number;
396
+ constructor(audioBuffer: AudioBuffer);
397
+ /** Encode as 32-bit float WAV. Cached after first call. */
398
+ toWav(): Blob;
399
+ /** Encode as 16-bit integer WAV. Cached after first call. */
400
+ toWav16(): Blob;
401
+ /** Download as 32-bit float WAV file. */
402
+ downloadWav(filename?: string): void;
403
+ /** Download as 16-bit integer WAV file. */
404
+ downloadWav16(filename?: string): void;
405
+ }
406
+
407
+ interface RenderOfflineOptions {
408
+ /** Total duration in seconds. When omitted, uses 60s max and trims trailing silence. */
409
+ duration?: number;
410
+ /** Sample rate. Default: 48000. */
411
+ sampleRate?: number;
412
+ /** Number of output channels. Default: 2 (stereo). */
413
+ channels?: number;
414
+ }
415
+ /**
416
+ * Render audio offline using an OfflineAudioContext.
417
+ *
418
+ * The callback receives an OfflineAudioContext. Create instruments,
419
+ * schedule notes using absolute times (starting from 0), then return.
420
+ * The audio is rendered as fast as possible (not real-time).
421
+ *
422
+ * Returns a RenderResult with the rendered AudioBuffer and
423
+ * convenience methods for WAV encoding and download.
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * const result = await renderOffline(async (context) => {
428
+ * const piano = await new SplendidGrandPiano(context).load;
429
+ * piano.start({ note: "C4", time: 0, duration: 1 });
430
+ * });
431
+ * result.downloadWav("export.wav");
432
+ * ```
433
+ */
434
+ declare function renderOffline(callback: (context: OfflineAudioContext) => Promise<void>, options?: RenderOfflineOptions): Promise<RenderResult>;
435
+
436
+ /**
437
+ * Encode an AudioBuffer as a WAV file Blob.
438
+ *
439
+ * Supports 32-bit float (lossless) and 16-bit integer (CD quality) formats.
440
+ */
441
+ /** Encode AudioBuffer as 32-bit float WAV. */
442
+ declare function audioBufferToWav(buffer: AudioBuffer): Blob;
443
+ /** Encode AudioBuffer as 16-bit integer WAV. */
444
+ declare function audioBufferToWav16(buffer: AudioBuffer): Blob;
445
+
446
+ /**
447
+ * Trim trailing silence from an AudioBuffer.
448
+ *
449
+ * Scans all channels from the end to find the last sample above the threshold,
450
+ * then returns a new AudioBuffer trimmed to that length.
451
+ */
452
+ declare function trimSilence(buffer: AudioBuffer): AudioBuffer;
453
+
387
454
  /**
388
455
  * TransportClock
389
456
  *
@@ -917,4 +984,4 @@ declare const LAYERS: ({
917
984
  cutoff?: undefined;
918
985
  })[];
919
986
 
920
- export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PlaybackParams, Reverb, SampleLoader, Sampler, type SamplerConfig, Scheduler, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, Smplr, type SmplrGroup, type SmplrJson, type SmplrOptions, type SmplrRegion, type SmplrSamples, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type SpreadResult, type StopFn, type StopTarget, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges };
987
+ export { CacheStorage, DrumMachine, type DrumMachineOptions, ElectricPiano, type ElectricPianoOptions, HttpStorage, LAYERS, type LoadProgress, Mallet, Mellotron, type MellotronConfig, type MellotronOptions, NAME_TO_PATH, type NoteEvent, type PlaybackParams, type RenderOfflineOptions, RenderResult, Reverb, SampleLoader, Sampler, type SamplerConfig, Scheduler, Sequencer, type SequencerInstrument, type SequencerNote, type SequencerNoteEvent, type SequencerOptions, Smolken, type SmolkenConfig, type SmolkenOptions, Smplr, type SmplrGroup, type SmplrJson, type SmplrOptions, type SmplrRegion, type SmplrSamples, Soundfont, type Soundfont2Options, Soundfont2Sampler, type SoundfontOptions, SplendidGrandPiano, type SplendidGrandPianoConfig, type SpreadResult, type StopFn, type StopTarget, type Storage, type StorageResponse, Versilian, type VersilianConfig, type VersilianOptions, type VoiceParams, audioBufferToWav, audioBufferToWav16, drumMachineToSmplrJson, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSmolkenNames, getSoundfontKits, getSoundfontNames, getVersilianInstruments, mellotronToSmplrJson, pianoToSmplrJson, renderOffline, samplerToSmplrJson, sf2InstrumentToSmplrJson, soundfontToSmplrJson, spreadKeyRanges, trimSilence };
package/dist/index.js CHANGED
@@ -84,6 +84,8 @@ __export(index_exports, {
84
84
  Soundfont2Sampler: () => Soundfont2Sampler,
85
85
  SplendidGrandPiano: () => SplendidGrandPiano,
86
86
  Versilian: () => Versilian,
87
+ audioBufferToWav: () => audioBufferToWav,
88
+ audioBufferToWav16: () => audioBufferToWav16,
87
89
  drumMachineToSmplrJson: () => drumMachineToSmplrJson,
88
90
  getDrumMachineNames: () => getDrumMachineNames,
89
91
  getElectricPianoNames: () => getElectricPianoNames,
@@ -95,10 +97,12 @@ __export(index_exports, {
95
97
  getVersilianInstruments: () => getVersilianInstruments,
96
98
  mellotronToSmplrJson: () => mellotronToSmplrJson,
97
99
  pianoToSmplrJson: () => pianoToSmplrJson,
100
+ renderOffline: () => renderOffline,
98
101
  samplerToSmplrJson: () => samplerToSmplrJson,
99
102
  sf2InstrumentToSmplrJson: () => sf2InstrumentToSmplrJson,
100
103
  soundfontToSmplrJson: () => soundfontToSmplrJson,
101
- spreadKeyRanges: () => spreadKeyRanges
104
+ spreadKeyRanges: () => spreadKeyRanges,
105
+ trimSilence: () => trimSilence
102
106
  });
103
107
  module.exports = __toCommonJS(index_exports);
104
108
 
@@ -1131,8 +1135,7 @@ playNote_fn = function(event) {
1131
1135
  if (duration != null) {
1132
1136
  const startT = time != null ? time : this.context.currentTime;
1133
1137
  const releaseAt = startT + duration;
1134
- const delayMs = Math.max(0, (releaseAt - this.context.currentTime) * 1e3);
1135
- setTimeout(() => voice.stop(releaseAt), delayMs);
1138
+ voice.stop(releaseAt);
1136
1139
  }
1137
1140
  }
1138
1141
  };
@@ -1334,6 +1337,155 @@ function drumMachineToSmplrJson(instrument) {
1334
1337
  };
1335
1338
  }
1336
1339
 
1340
+ // src/offline/wav-encoder.ts
1341
+ function audioBufferToWav(buffer) {
1342
+ return encodeWav(buffer, 32);
1343
+ }
1344
+ function audioBufferToWav16(buffer) {
1345
+ return encodeWav(buffer, 16);
1346
+ }
1347
+ function encodeWav(buffer, bitDepth) {
1348
+ const numChannels = buffer.numberOfChannels;
1349
+ const sampleRate = buffer.sampleRate;
1350
+ const length = buffer.length;
1351
+ const bytesPerSample = bitDepth / 8;
1352
+ const blockAlign = numChannels * bytesPerSample;
1353
+ const dataSize = length * blockAlign;
1354
+ const headerSize = 44;
1355
+ const arrayBuffer = new ArrayBuffer(headerSize + dataSize);
1356
+ const view = new DataView(arrayBuffer);
1357
+ writeString(view, 0, "RIFF");
1358
+ view.setUint32(4, 36 + dataSize, true);
1359
+ writeString(view, 8, "WAVE");
1360
+ writeString(view, 12, "fmt ");
1361
+ view.setUint32(16, 16, true);
1362
+ view.setUint16(20, bitDepth === 32 ? 3 : 1, true);
1363
+ view.setUint16(22, numChannels, true);
1364
+ view.setUint32(24, sampleRate, true);
1365
+ view.setUint32(28, sampleRate * blockAlign, true);
1366
+ view.setUint16(32, blockAlign, true);
1367
+ view.setUint16(34, bitDepth, true);
1368
+ writeString(view, 36, "data");
1369
+ view.setUint32(40, dataSize, true);
1370
+ const channels = [];
1371
+ for (let ch = 0; ch < numChannels; ch++) {
1372
+ channels.push(buffer.getChannelData(ch));
1373
+ }
1374
+ let offset = headerSize;
1375
+ for (let i = 0; i < length; i++) {
1376
+ for (let ch = 0; ch < numChannels; ch++) {
1377
+ const sample = channels[ch][i];
1378
+ if (bitDepth === 32) {
1379
+ view.setFloat32(offset, sample, true);
1380
+ } else {
1381
+ const clamped = Math.max(-1, Math.min(1, sample));
1382
+ view.setInt16(offset, clamped < 0 ? clamped * 32768 : clamped * 32767, true);
1383
+ }
1384
+ offset += bytesPerSample;
1385
+ }
1386
+ }
1387
+ return new Blob([arrayBuffer], { type: "audio/wav" });
1388
+ }
1389
+ function writeString(view, offset, str2) {
1390
+ for (let i = 0; i < str2.length; i++) {
1391
+ view.setUint8(offset + i, str2.charCodeAt(i));
1392
+ }
1393
+ }
1394
+
1395
+ // src/offline/render-result.ts
1396
+ var _wavCache, _wav16Cache;
1397
+ var RenderResult = class {
1398
+ constructor(audioBuffer) {
1399
+ __privateAdd(this, _wavCache);
1400
+ __privateAdd(this, _wav16Cache);
1401
+ this.audioBuffer = audioBuffer;
1402
+ this.duration = audioBuffer.duration;
1403
+ this.sampleRate = audioBuffer.sampleRate;
1404
+ }
1405
+ /** Encode as 32-bit float WAV. Cached after first call. */
1406
+ toWav() {
1407
+ if (!__privateGet(this, _wavCache)) {
1408
+ __privateSet(this, _wavCache, audioBufferToWav(this.audioBuffer));
1409
+ }
1410
+ return __privateGet(this, _wavCache);
1411
+ }
1412
+ /** Encode as 16-bit integer WAV. Cached after first call. */
1413
+ toWav16() {
1414
+ if (!__privateGet(this, _wav16Cache)) {
1415
+ __privateSet(this, _wav16Cache, audioBufferToWav16(this.audioBuffer));
1416
+ }
1417
+ return __privateGet(this, _wav16Cache);
1418
+ }
1419
+ /** Download as 32-bit float WAV file. */
1420
+ downloadWav(filename = "render.wav") {
1421
+ downloadBlob(this.toWav(), filename);
1422
+ }
1423
+ /** Download as 16-bit integer WAV file. */
1424
+ downloadWav16(filename = "render.wav") {
1425
+ downloadBlob(this.toWav16(), filename);
1426
+ }
1427
+ };
1428
+ _wavCache = new WeakMap();
1429
+ _wav16Cache = new WeakMap();
1430
+ function downloadBlob(blob, filename) {
1431
+ const url = URL.createObjectURL(blob);
1432
+ const a = document.createElement("a");
1433
+ a.href = url;
1434
+ a.download = filename;
1435
+ a.click();
1436
+ URL.revokeObjectURL(url);
1437
+ }
1438
+
1439
+ // src/offline/trim-silence.ts
1440
+ var SILENCE_THRESHOLD = 1e-4;
1441
+ function trimSilence(buffer) {
1442
+ const { numberOfChannels, sampleRate, length } = buffer;
1443
+ let lastNonSilent = 0;
1444
+ for (let ch = 0; ch < numberOfChannels; ch++) {
1445
+ const data = buffer.getChannelData(ch);
1446
+ for (let i = length - 1; i >= 0; i--) {
1447
+ if (Math.abs(data[i]) > SILENCE_THRESHOLD) {
1448
+ if (i > lastNonSilent) lastNonSilent = i;
1449
+ break;
1450
+ }
1451
+ }
1452
+ }
1453
+ const trimmedLength = Math.max(1, lastNonSilent + 1);
1454
+ if (trimmedLength === length) return buffer;
1455
+ const trimmed = new AudioBuffer({
1456
+ numberOfChannels,
1457
+ length: trimmedLength,
1458
+ sampleRate
1459
+ });
1460
+ for (let ch = 0; ch < numberOfChannels; ch++) {
1461
+ const source = buffer.getChannelData(ch);
1462
+ trimmed.copyToChannel(source.subarray(0, trimmedLength), ch);
1463
+ }
1464
+ return trimmed;
1465
+ }
1466
+
1467
+ // src/offline/render-offline.ts
1468
+ var DEFAULT_SAMPLE_RATE = 48e3;
1469
+ var DEFAULT_CHANNELS = 2;
1470
+ var DEFAULT_MAX_DURATION = 60;
1471
+ function renderOffline(callback, options) {
1472
+ return __async(this, null, function* () {
1473
+ var _a, _b;
1474
+ const sampleRate = (_a = options == null ? void 0 : options.sampleRate) != null ? _a : DEFAULT_SAMPLE_RATE;
1475
+ const channels = (_b = options == null ? void 0 : options.channels) != null ? _b : DEFAULT_CHANNELS;
1476
+ const explicitDuration = options == null ? void 0 : options.duration;
1477
+ const duration = explicitDuration != null ? explicitDuration : DEFAULT_MAX_DURATION;
1478
+ const length = Math.ceil(duration * sampleRate);
1479
+ const offlineContext = new OfflineAudioContext(channels, length, sampleRate);
1480
+ yield callback(offlineContext);
1481
+ let buffer = yield offlineContext.startRendering();
1482
+ if (explicitDuration === void 0) {
1483
+ buffer = trimSilence(buffer);
1484
+ }
1485
+ return new RenderResult(buffer);
1486
+ });
1487
+ }
1488
+
1337
1489
  // src/sequencer/time-parser.ts
1338
1490
  function parseTicks(time, ppq, timeSignature) {
1339
1491
  if (typeof time === "number") return time;
@@ -2701,7 +2853,7 @@ function buildSamplerBuffers(source, context, storage, options) {
2701
2853
  return __async(this, null, function* () {
2702
2854
  const { json, urlMap, preloaded } = samplerToSmplrJson(source, options);
2703
2855
  yield Promise.all(
2704
- Object.entries(urlMap).map((_0) => __async(this, [_0], function* ([name, url]) {
2856
+ Object.entries(urlMap).map((_0) => __async(null, [_0], function* ([name, url]) {
2705
2857
  const buffer = yield loadAudioBuffer(context, url, storage);
2706
2858
  if (buffer) preloaded.set(name, buffer);
2707
2859
  }))
@@ -3095,7 +3247,7 @@ function decodeSoundfontFile(context, config) {
3095
3247
  const noteNames = Object.keys(json);
3096
3248
  const buffers = /* @__PURE__ */ new Map();
3097
3249
  yield Promise.all(
3098
- noteNames.map((noteName) => __async(this, null, function* () {
3250
+ noteNames.map((noteName) => __async(null, null, function* () {
3099
3251
  const midi = toMidi(noteName);
3100
3252
  if (!midi) return;
3101
3253
  try {
@@ -3727,6 +3879,8 @@ var LAYERS = [
3727
3879
  Soundfont2Sampler,
3728
3880
  SplendidGrandPiano,
3729
3881
  Versilian,
3882
+ audioBufferToWav,
3883
+ audioBufferToWav16,
3730
3884
  drumMachineToSmplrJson,
3731
3885
  getDrumMachineNames,
3732
3886
  getElectricPianoNames,
@@ -3738,9 +3892,11 @@ var LAYERS = [
3738
3892
  getVersilianInstruments,
3739
3893
  mellotronToSmplrJson,
3740
3894
  pianoToSmplrJson,
3895
+ renderOffline,
3741
3896
  samplerToSmplrJson,
3742
3897
  sf2InstrumentToSmplrJson,
3743
3898
  soundfontToSmplrJson,
3744
- spreadKeyRanges
3899
+ spreadKeyRanges,
3900
+ trimSilence
3745
3901
  });
3746
3902
  //# sourceMappingURL=index.js.map