@ugfoundation/swaralipi-js 0.1.6 → 0.2.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.
@@ -217,6 +217,77 @@ declare class SwaralipiPlayer {
217
217
  dispose(): void;
218
218
  }
219
219
 
220
+ type AudioFormat = 'wav' | 'mp3';
221
+ interface AudioExportOptions {
222
+ /** Samples host (santoor/tabla/tanpura/metronome). Default the project CDN. */
223
+ soundsBaseUrl?: string;
224
+ /** Self-host the grand piano (see the player's `pianoBaseUrl`). */
225
+ pianoBaseUrl?: string;
226
+ /** Extra melody instruments, keyed by id (see the player's option). */
227
+ customInstruments?: Record<string, InstrumentDef>;
228
+ /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
229
+ tonic?: string;
230
+ /** Tempo in matra/min. Default: `meta.bpm`, else 90. Snapped to the tabla grid
231
+ * when a sampled tabla actually plays. */
232
+ bpm?: number;
233
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
234
+ baseOctave?: number;
235
+ /** Melody instrument id. Default `piano`. */
236
+ instrument?: InstrumentId;
237
+ /** Include the notation (melody) voice. Default true. */
238
+ melody?: boolean;
239
+ /** Rhythm accompaniment: `tabla` (→ metronome for unsampled taals),
240
+ * `metronome`, or `none`. Default `none` — the base export is notation only. */
241
+ rhythm?: RhythmMode;
242
+ /** Taal for the rhythm: an explicit id, or `auto` (from `meta.taal`). Default
243
+ * `auto`. Only used when `rhythm` isn't `none`. */
244
+ taal?: TaalId | 'auto';
245
+ /** Include the tanpura drone. Default false. */
246
+ tanpura?: boolean;
247
+ /** Output format. Default `wav` (lossless, zero-dependency). `mp3` needs the
248
+ * optional peer dep `@breezystack/lamejs`. */
249
+ format?: AudioFormat;
250
+ /** MP3 bitrate in kbps. Default 192. */
251
+ mp3Kbps?: number;
252
+ /** Output channels. Default 1 (the content is monophonic and centred, so mono
253
+ * halves the file with no audible loss); set 2 for stereo. */
254
+ channels?: 1 | 2;
255
+ /** Sample rate. Default 44100. */
256
+ sampleRate?: number;
257
+ /** Fade the last N seconds so a looped tabla/tanpura doesn't hard-cut at the
258
+ * render boundary. Default 0.4. */
259
+ fadeOutSec?: number;
260
+ }
261
+ interface AudioExportResult {
262
+ /** The encoded audio, ready to download or upload. */
263
+ blob: Blob;
264
+ /** Suggested `<title>.<ext>` download filename. */
265
+ filename: string;
266
+ mimeType: string;
267
+ /** Rendered length in seconds. */
268
+ durationSec: number;
269
+ }
270
+ /**
271
+ * Render a swaralipi doc to an audio Blob in the browser. Runs in an
272
+ * OfflineAudioContext — no user gesture and no server needed. Throws in an
273
+ * environment without Web Audio (Node/SSR) or, for `format: 'mp3'`, when the
274
+ * optional `@breezystack/lamejs` peer dependency is not installed.
275
+ */
276
+ declare function exportAudio(doc: Swaralipi, options?: AudioExportOptions): Promise<AudioExportResult>;
277
+ /**
278
+ * Encode planar float channels to a 16-bit PCM WAV (RIFF). Pure — no Web Audio
279
+ * — so it runs (and tests) in Node. 16-bit PCM is lossless CD quality and plays
280
+ * everywhere; float/24-bit skipped (add if mastering-grade output is needed).
281
+ */
282
+ declare function encodeWav(channels: Float32Array[], sampleRate: number): Uint8Array;
283
+ /**
284
+ * Encode planar float channels to MP3 via lamejs. lamejs is **dynamically
285
+ * imported** and an *optional* peer dependency (LGPL-3.0) — WAV needs no
286
+ * dependency, so only reach for this when a compressed file is wanted. Samples
287
+ * are fed in 1152-frame blocks (a multiple of 576, lamejs' preferred size).
288
+ */
289
+ declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?: number): Promise<Uint8Array>;
290
+
220
291
  /**
221
292
  * Pure spec-doc → playback timeline. No Tone, no audio — the player turns
222
293
  * this `Schedule` into Transport events, but it is fully unit-testable on its
@@ -270,6 +341,51 @@ interface ScheduleOptions {
270
341
  */
271
342
  declare function buildSchedule(doc: Swaralipi, opts?: ScheduleOptions): Schedule;
272
343
 
344
+ /**
345
+ * MIDI export — write a swaralipi doc to a downloadable Standard MIDI File.
346
+ *
347
+ * Unlike the audio export this needs **no Tone and no AudioContext**: MIDI is
348
+ * just note events, and `buildSchedule` already gives us (midi, beat,
349
+ * durationBeats) per note. So the whole thing is pure bytes — it runs (and
350
+ * tests) in plain Node, and carries no dependency (the SMF format is trivial),
351
+ * which keeps it clear of any redistribution/license question.
352
+ *
353
+ * Scope = the **melody voice only**. Tabla is prerecorded audio loops with no
354
+ * per-stroke note data, so there's nothing to turn into MIDI notes; tanpura is
355
+ * a fixed background drone. A `.mid` here is the notation line — the user opens
356
+ * it in any DAW / notation editor and assigns whatever instrument they like.
357
+ */
358
+
359
+ interface MidiExportOptions {
360
+ /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
361
+ tonic?: string;
362
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
363
+ baseOctave?: number;
364
+ /** Tempo in matra/min written as the file's tempo. Default `meta.bpm`, else 90. */
365
+ bpm?: number;
366
+ /** Track name meta-event. Default the song title. */
367
+ name?: string;
368
+ }
369
+ interface MidiExportResult {
370
+ blob: Blob;
371
+ /** Suggested `<title>.mid` filename. */
372
+ filename: string;
373
+ mimeType: string;
374
+ }
375
+ /**
376
+ * Encode a schedule to a format-0 Standard MIDI File (one track, one channel).
377
+ * Pure — no Tone, no browser — so it's directly unit-testable.
378
+ */
379
+ declare function encodeMidi(schedule: Schedule, opts: {
380
+ bpm: number;
381
+ name?: string;
382
+ }): Uint8Array;
383
+ /**
384
+ * Export a swaralipi doc to a `.mid` Blob (melody voice only). No browser APIs
385
+ * beyond `Blob` (present in Node 20+ too), so it works client-side or server-side.
386
+ */
387
+ declare function exportMidi(doc: Swaralipi, options?: MidiExportOptions): MidiExportResult;
388
+
273
389
  /**
274
390
  * Pure svara → pitch math for the player. No Tone, no audio — unit-testable
275
391
  * in plain Node and shared by the scheduler.
@@ -313,4 +429,4 @@ declare function midiToSampleName(midi: number): string;
313
429
  /** MIDI → santoor sample filename stem (`cs4`): lowercase, `#` → `s`. */
314
430
  declare function sampleFileName(midi: number): string;
315
431
 
316
- export { type BindSheetOptions, DEFAULT_SOUNDS_BASE, INSTRUMENTS, type InstrumentDef, type InstrumentId, OCTAVE_OFFSET, type PlayEvent, type PlayerOptions, type PlayerState, type RhythmMode, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, type Schedule, type ScheduleOptions, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, type ToneModule, buildSchedule, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
432
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DEFAULT_SOUNDS_BASE, INSTRUMENTS, type InstrumentDef, type InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayEvent, type PlayerOptions, type PlayerState, type RhythmMode, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, type Schedule, type ScheduleOptions, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, type ToneModule, buildSchedule, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
@@ -217,6 +217,77 @@ declare class SwaralipiPlayer {
217
217
  dispose(): void;
218
218
  }
219
219
 
220
+ type AudioFormat = 'wav' | 'mp3';
221
+ interface AudioExportOptions {
222
+ /** Samples host (santoor/tabla/tanpura/metronome). Default the project CDN. */
223
+ soundsBaseUrl?: string;
224
+ /** Self-host the grand piano (see the player's `pianoBaseUrl`). */
225
+ pianoBaseUrl?: string;
226
+ /** Extra melody instruments, keyed by id (see the player's option). */
227
+ customInstruments?: Record<string, InstrumentDef>;
228
+ /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
229
+ tonic?: string;
230
+ /** Tempo in matra/min. Default: `meta.bpm`, else 90. Snapped to the tabla grid
231
+ * when a sampled tabla actually plays. */
232
+ bpm?: number;
233
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
234
+ baseOctave?: number;
235
+ /** Melody instrument id. Default `piano`. */
236
+ instrument?: InstrumentId;
237
+ /** Include the notation (melody) voice. Default true. */
238
+ melody?: boolean;
239
+ /** Rhythm accompaniment: `tabla` (→ metronome for unsampled taals),
240
+ * `metronome`, or `none`. Default `none` — the base export is notation only. */
241
+ rhythm?: RhythmMode;
242
+ /** Taal for the rhythm: an explicit id, or `auto` (from `meta.taal`). Default
243
+ * `auto`. Only used when `rhythm` isn't `none`. */
244
+ taal?: TaalId | 'auto';
245
+ /** Include the tanpura drone. Default false. */
246
+ tanpura?: boolean;
247
+ /** Output format. Default `wav` (lossless, zero-dependency). `mp3` needs the
248
+ * optional peer dep `@breezystack/lamejs`. */
249
+ format?: AudioFormat;
250
+ /** MP3 bitrate in kbps. Default 192. */
251
+ mp3Kbps?: number;
252
+ /** Output channels. Default 1 (the content is monophonic and centred, so mono
253
+ * halves the file with no audible loss); set 2 for stereo. */
254
+ channels?: 1 | 2;
255
+ /** Sample rate. Default 44100. */
256
+ sampleRate?: number;
257
+ /** Fade the last N seconds so a looped tabla/tanpura doesn't hard-cut at the
258
+ * render boundary. Default 0.4. */
259
+ fadeOutSec?: number;
260
+ }
261
+ interface AudioExportResult {
262
+ /** The encoded audio, ready to download or upload. */
263
+ blob: Blob;
264
+ /** Suggested `<title>.<ext>` download filename. */
265
+ filename: string;
266
+ mimeType: string;
267
+ /** Rendered length in seconds. */
268
+ durationSec: number;
269
+ }
270
+ /**
271
+ * Render a swaralipi doc to an audio Blob in the browser. Runs in an
272
+ * OfflineAudioContext — no user gesture and no server needed. Throws in an
273
+ * environment without Web Audio (Node/SSR) or, for `format: 'mp3'`, when the
274
+ * optional `@breezystack/lamejs` peer dependency is not installed.
275
+ */
276
+ declare function exportAudio(doc: Swaralipi, options?: AudioExportOptions): Promise<AudioExportResult>;
277
+ /**
278
+ * Encode planar float channels to a 16-bit PCM WAV (RIFF). Pure — no Web Audio
279
+ * — so it runs (and tests) in Node. 16-bit PCM is lossless CD quality and plays
280
+ * everywhere; float/24-bit skipped (add if mastering-grade output is needed).
281
+ */
282
+ declare function encodeWav(channels: Float32Array[], sampleRate: number): Uint8Array;
283
+ /**
284
+ * Encode planar float channels to MP3 via lamejs. lamejs is **dynamically
285
+ * imported** and an *optional* peer dependency (LGPL-3.0) — WAV needs no
286
+ * dependency, so only reach for this when a compressed file is wanted. Samples
287
+ * are fed in 1152-frame blocks (a multiple of 576, lamejs' preferred size).
288
+ */
289
+ declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?: number): Promise<Uint8Array>;
290
+
220
291
  /**
221
292
  * Pure spec-doc → playback timeline. No Tone, no audio — the player turns
222
293
  * this `Schedule` into Transport events, but it is fully unit-testable on its
@@ -270,6 +341,51 @@ interface ScheduleOptions {
270
341
  */
271
342
  declare function buildSchedule(doc: Swaralipi, opts?: ScheduleOptions): Schedule;
272
343
 
344
+ /**
345
+ * MIDI export — write a swaralipi doc to a downloadable Standard MIDI File.
346
+ *
347
+ * Unlike the audio export this needs **no Tone and no AudioContext**: MIDI is
348
+ * just note events, and `buildSchedule` already gives us (midi, beat,
349
+ * durationBeats) per note. So the whole thing is pure bytes — it runs (and
350
+ * tests) in plain Node, and carries no dependency (the SMF format is trivial),
351
+ * which keeps it clear of any redistribution/license question.
352
+ *
353
+ * Scope = the **melody voice only**. Tabla is prerecorded audio loops with no
354
+ * per-stroke note data, so there's nothing to turn into MIDI notes; tanpura is
355
+ * a fixed background drone. A `.mid` here is the notation line — the user opens
356
+ * it in any DAW / notation editor and assigns whatever instrument they like.
357
+ */
358
+
359
+ interface MidiExportOptions {
360
+ /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
361
+ tonic?: string;
362
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
363
+ baseOctave?: number;
364
+ /** Tempo in matra/min written as the file's tempo. Default `meta.bpm`, else 90. */
365
+ bpm?: number;
366
+ /** Track name meta-event. Default the song title. */
367
+ name?: string;
368
+ }
369
+ interface MidiExportResult {
370
+ blob: Blob;
371
+ /** Suggested `<title>.mid` filename. */
372
+ filename: string;
373
+ mimeType: string;
374
+ }
375
+ /**
376
+ * Encode a schedule to a format-0 Standard MIDI File (one track, one channel).
377
+ * Pure — no Tone, no browser — so it's directly unit-testable.
378
+ */
379
+ declare function encodeMidi(schedule: Schedule, opts: {
380
+ bpm: number;
381
+ name?: string;
382
+ }): Uint8Array;
383
+ /**
384
+ * Export a swaralipi doc to a `.mid` Blob (melody voice only). No browser APIs
385
+ * beyond `Blob` (present in Node 20+ too), so it works client-side or server-side.
386
+ */
387
+ declare function exportMidi(doc: Swaralipi, options?: MidiExportOptions): MidiExportResult;
388
+
273
389
  /**
274
390
  * Pure svara → pitch math for the player. No Tone, no audio — unit-testable
275
391
  * in plain Node and shared by the scheduler.
@@ -313,4 +429,4 @@ declare function midiToSampleName(midi: number): string;
313
429
  /** MIDI → santoor sample filename stem (`cs4`): lowercase, `#` → `s`. */
314
430
  declare function sampleFileName(midi: number): string;
315
431
 
316
- export { type BindSheetOptions, DEFAULT_SOUNDS_BASE, INSTRUMENTS, type InstrumentDef, type InstrumentId, OCTAVE_OFFSET, type PlayEvent, type PlayerOptions, type PlayerState, type RhythmMode, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, type Schedule, type ScheduleOptions, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, type ToneModule, buildSchedule, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
432
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DEFAULT_SOUNDS_BASE, INSTRUMENTS, type InstrumentDef, type InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayEvent, type PlayerOptions, type PlayerState, type RhythmMode, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, type Schedule, type ScheduleOptions, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, type ToneModule, buildSchedule, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
@@ -773,6 +773,269 @@ var SwaralipiPlayer = class {
773
773
  }
774
774
  };
775
775
 
776
- export { DEFAULT_SOUNDS_BASE, INSTRUMENTS, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
776
+ // src/player/export.ts
777
+ function midiRange2(schedule) {
778
+ let lo = Infinity;
779
+ let hi = -Infinity;
780
+ for (const ev of schedule.events) {
781
+ for (const m of [ev.midi, ev.kan]) {
782
+ if (m !== null) {
783
+ if (m < lo) lo = m;
784
+ if (m > hi) hi = m;
785
+ }
786
+ }
787
+ }
788
+ return lo <= hi ? { lo, hi } : void 0;
789
+ }
790
+ function pickDef(id, o) {
791
+ const def = o.customInstruments?.[id] ?? INSTRUMENTS[id];
792
+ if (def === void 0) return INSTRUMENTS.harmonium;
793
+ if (id === "piano" && o.pianoBaseUrl) return { ...def, baseUrl: normalizeBase(o.pianoBaseUrl) };
794
+ if (def.local === true && !o.soundsBaseUrl) return INSTRUMENTS.harmonium;
795
+ return def;
796
+ }
797
+ function resolveTaal(doc, taal) {
798
+ const id = taal !== void 0 && taal !== "auto" ? taal : doc.meta.taal?.id;
799
+ return id !== void 0 && id in TAALS ? id : void 0;
800
+ }
801
+ function safeName(title) {
802
+ return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
803
+ }
804
+ async function exportAudio(doc, options = {}) {
805
+ const Tone = await import('tone');
806
+ const base = options.soundsBaseUrl ?? DEFAULT_SOUNDS_BASE;
807
+ const format = options.format ?? "wav";
808
+ const channels = options.channels ?? 1;
809
+ const sampleRate = options.sampleRate ?? 44100;
810
+ const fadeOutSec = options.fadeOutSec ?? 0.4;
811
+ const rhythm = options.rhythm ?? "none";
812
+ const withMelody = options.melody ?? true;
813
+ const withTanpura = options.tanpura ?? false;
814
+ const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
815
+ const range = midiRange2(schedule);
816
+ const taalId = resolveTaal(doc, options.taal);
817
+ const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
818
+ const usesTabla = rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && !!base;
819
+ const reqBpm = options.bpm ?? doc.meta.bpm ?? 90;
820
+ const bpm = usesTabla && taalId ? nearestTablaBpm(reqBpm, tablaBpms(taalId)) : reqBpm;
821
+ const spb = 60 / bpm;
822
+ const id = options.instrument ?? "piano";
823
+ const def = pickDef(id, options);
824
+ const durationSec = Math.max(schedule.totalBeats * spb + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
825
+ const rendered = await Tone.Offline(
826
+ async ({ transport }) => {
827
+ transport.bpm.value = bpm;
828
+ const master = new Tone.Gain(1).toDestination();
829
+ if (withMelody) {
830
+ const sampler = createSampler(Tone, def, base, range).connect(master);
831
+ for (const ev of schedule.events) {
832
+ if (ev.midi === null) continue;
833
+ const at = ev.beat * spb;
834
+ const dur = Math.max(ev.durationBeats * spb, 0.05);
835
+ if (ev.kan !== null) {
836
+ const name2 = midiToSampleName(ev.kan);
837
+ transport.scheduleOnce((t) => sampler.triggerAttackRelease(name2, spb * 0.4, t), Math.max(at - spb * 0.12, 0));
838
+ }
839
+ const name = midiToSampleName(ev.midi);
840
+ transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, dur, t), at);
841
+ }
842
+ }
843
+ let tabla;
844
+ if (rhythm !== "none" && base) {
845
+ if (usesTabla && taalId) {
846
+ const url = tablaUrl(base, taalId, bpm, variation);
847
+ tabla = new Tone.Player({ url, loop: true }).connect(master);
848
+ tabla.sync().start(0);
849
+ } else {
850
+ const metro = createMetronome(Tone, base).connect(master);
851
+ const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : 4;
852
+ let beat = 0;
853
+ transport.scheduleRepeat((time) => {
854
+ metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
855
+ metro.start(time);
856
+ beat += 1;
857
+ }, spb, 0);
858
+ }
859
+ }
860
+ if (withTanpura && base) {
861
+ const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
862
+ const sa = TANPURA_PCS[tonicPc];
863
+ const pa = TANPURA_PCS[(tonicPc + 7) % 12];
864
+ const players = createTanpura(Tone, base, [sa, pa]).connect(master);
865
+ players.volume.value = -14;
866
+ const pattern = [pa, sa, sa, sa];
867
+ let i = 0;
868
+ transport.scheduleRepeat((time) => {
869
+ const stem = pattern[i % pattern.length];
870
+ if (players.has(stem)) players.player(stem).start(time);
871
+ i += 1;
872
+ }, spb * 2, 0);
873
+ }
874
+ await Tone.loaded();
875
+ if (tabla?.buffer?.loaded) tabla.loopEnd = quantizeLoopEnd(tabla.buffer.duration, spb);
876
+ if (fadeOutSec > 0) {
877
+ master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
878
+ master.gain.linearRampToValueAtTime(0, durationSec);
879
+ }
880
+ transport.start(0);
881
+ },
882
+ durationSec,
883
+ channels,
884
+ sampleRate
885
+ );
886
+ const chans = [];
887
+ for (let c = 0; c < rendered.numberOfChannels; c++) chans.push(rendered.getChannelData(c));
888
+ const bytes = format === "mp3" ? await encodeMp3(chans, rendered.sampleRate, options.mp3Kbps) : encodeWav(chans, rendered.sampleRate);
889
+ const mimeType = format === "mp3" ? "audio/mpeg" : "audio/wav";
890
+ return {
891
+ blob: new Blob([bytes], { type: mimeType }),
892
+ filename: `${safeName(doc.meta.title)}.${format}`,
893
+ mimeType,
894
+ durationSec: rendered.duration
895
+ };
896
+ }
897
+ function toInt16(f) {
898
+ const s = f < -1 ? -1 : f > 1 ? 1 : f;
899
+ return s < 0 ? s * 32768 : s * 32767;
900
+ }
901
+ function encodeWav(channels, sampleRate) {
902
+ const numCh = channels.length;
903
+ const frames = channels[0]?.length ?? 0;
904
+ const bytesPerSample = 2;
905
+ const blockAlign = numCh * bytesPerSample;
906
+ const dataSize = frames * blockAlign;
907
+ const buf = new ArrayBuffer(44 + dataSize);
908
+ const view = new DataView(buf);
909
+ const str = (off2, s) => {
910
+ for (let i = 0; i < s.length; i++) view.setUint8(off2 + i, s.charCodeAt(i));
911
+ };
912
+ str(0, "RIFF");
913
+ view.setUint32(4, 36 + dataSize, true);
914
+ str(8, "WAVE");
915
+ str(12, "fmt ");
916
+ view.setUint32(16, 16, true);
917
+ view.setUint16(20, 1, true);
918
+ view.setUint16(22, numCh, true);
919
+ view.setUint32(24, sampleRate, true);
920
+ view.setUint32(28, sampleRate * blockAlign, true);
921
+ view.setUint16(32, blockAlign, true);
922
+ view.setUint16(34, bytesPerSample * 8, true);
923
+ str(36, "data");
924
+ view.setUint32(40, dataSize, true);
925
+ let off = 44;
926
+ for (let f = 0; f < frames; f++) {
927
+ for (let c = 0; c < numCh; c++) {
928
+ view.setInt16(off, toInt16(channels[c][f]), true);
929
+ off += 2;
930
+ }
931
+ }
932
+ return new Uint8Array(buf);
933
+ }
934
+ function floatToInt16(f) {
935
+ const out = new Int16Array(f.length);
936
+ for (let i = 0; i < f.length; i++) out[i] = toInt16(f[i]);
937
+ return out;
938
+ }
939
+ function concatBytes(chunks) {
940
+ const total = chunks.reduce((n, c) => n + c.length, 0);
941
+ const out = new Uint8Array(total);
942
+ let off = 0;
943
+ for (const c of chunks) {
944
+ out.set(c, off);
945
+ off += c.length;
946
+ }
947
+ return out;
948
+ }
949
+ async function loadLame() {
950
+ try {
951
+ const mod = await import('@breezystack/lamejs');
952
+ const Mp3Encoder = mod.Mp3Encoder ?? mod.default?.Mp3Encoder;
953
+ if (!Mp3Encoder) throw new Error("no Mp3Encoder export");
954
+ return Mp3Encoder;
955
+ } catch {
956
+ throw new Error(
957
+ 'MP3 export needs the optional peer dependency "@breezystack/lamejs" \u2014 install it, or export as WAV (format: "wav").'
958
+ );
959
+ }
960
+ }
961
+ async function encodeMp3(channels, sampleRate, kbps = 192) {
962
+ const Mp3Encoder = await loadLame();
963
+ const numCh = Math.min(channels.length, 2);
964
+ const enc = new Mp3Encoder(numCh, sampleRate, kbps);
965
+ const left = floatToInt16(channels[0] ?? new Float32Array());
966
+ const right = numCh > 1 ? floatToInt16(channels[1]) : void 0;
967
+ const block = 1152;
968
+ const out = [];
969
+ for (let i = 0; i < left.length; i += block) {
970
+ const l = left.subarray(i, i + block);
971
+ const chunk = right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l);
972
+ if (chunk.length > 0) out.push(new Uint8Array(chunk));
973
+ }
974
+ const tail = enc.flush();
975
+ if (tail.length > 0) out.push(new Uint8Array(tail));
976
+ return concatBytes(out);
977
+ }
978
+
979
+ // src/player/midi.ts
980
+ var PPQ = 480;
981
+ function vlq(n) {
982
+ const out = [n & 127];
983
+ n >>>= 7;
984
+ while (n > 0) {
985
+ out.unshift(n & 127 | 128);
986
+ n >>>= 7;
987
+ }
988
+ return out;
989
+ }
990
+ function utf8(s) {
991
+ return [...new TextEncoder().encode(s)];
992
+ }
993
+ function u32be(n) {
994
+ return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
995
+ }
996
+ function encodeMidi(schedule, opts) {
997
+ const raw = [];
998
+ for (const ev of schedule.events) {
999
+ if (ev.midi === null) continue;
1000
+ const start = Math.round(ev.beat * PPQ);
1001
+ const end = Math.max(Math.round((ev.beat + ev.durationBeats) * PPQ), start + 1);
1002
+ if (ev.kan !== null) {
1003
+ const kOn = Math.max(start - Math.round(0.12 * PPQ), 0);
1004
+ raw.push({ tick: kOn, on: true, note: ev.kan, vel: 70 });
1005
+ raw.push({ tick: start, on: false, note: ev.kan, vel: 0 });
1006
+ }
1007
+ raw.push({ tick: start, on: true, note: ev.midi, vel: 100 });
1008
+ raw.push({ tick: end, on: false, note: ev.midi, vel: 0 });
1009
+ }
1010
+ raw.sort((a, b) => a.tick - b.tick || Number(a.on) - Number(b.on));
1011
+ const track = [];
1012
+ if (opts.name) track.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
1013
+ const usPerBeat = Math.round(6e7 / opts.bpm);
1014
+ track.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
1015
+ let last = 0;
1016
+ for (const e of raw) {
1017
+ track.push(...vlq(e.tick - last), e.on ? 144 : 128, e.note & 127, e.vel & 127);
1018
+ last = e.tick;
1019
+ }
1020
+ track.push(...vlq(0), 255, 47, 0);
1021
+ const header = [77, 84, 104, 100, ...u32be(6), 0, 0, 0, 1, PPQ >> 8 & 255, PPQ & 255];
1022
+ const trackChunk = [77, 84, 114, 107, ...u32be(track.length), ...track];
1023
+ return new Uint8Array([...header, ...trackChunk]);
1024
+ }
1025
+ function safeName2(title) {
1026
+ return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
1027
+ }
1028
+ function exportMidi(doc, options = {}) {
1029
+ const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
1030
+ const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1031
+ const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title });
1032
+ return {
1033
+ blob: new Blob([bytes], { type: "audio/midi" }),
1034
+ filename: `${safeName2(doc.meta.title)}.mid`,
1035
+ mimeType: "audio/midi"
1036
+ };
1037
+ }
1038
+
1039
+ export { DEFAULT_SOUNDS_BASE, INSTRUMENTS, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, SwaralipiPlayer, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl };
777
1040
  //# sourceMappingURL=index.js.map
778
1041
  //# sourceMappingURL=index.js.map