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 +78 -0
- package/dist/index.d.mts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +162 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +157 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
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:
|
|
282
|
-
constructor(context:
|
|
283
|
-
constructor(context:
|
|
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:
|
|
282
|
-
constructor(context:
|
|
283
|
-
constructor(context:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|