@ugfoundation/swaralipi-js 0.13.1 → 0.14.1

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.
Files changed (39) hide show
  1. package/README.md +25 -0
  2. package/dist/{batch-XVQRHLTN.js → batch-J72GGBG2.js} +4 -4
  3. package/dist/{batch-XVQRHLTN.js.map → batch-J72GGBG2.js.map} +1 -1
  4. package/dist/{chunk-MORX7G6E.js → chunk-4WDAVRET.js} +3 -3
  5. package/dist/{chunk-MORX7G6E.js.map → chunk-4WDAVRET.js.map} +1 -1
  6. package/dist/{chunk-L6YZ3GC6.js → chunk-GIOGYJMM.js} +180 -19
  7. package/dist/chunk-GIOGYJMM.js.map +1 -0
  8. package/dist/{chunk-GKS4WZZ6.js → chunk-KUZSW4FN.js} +3 -3
  9. package/dist/{chunk-GKS4WZZ6.js.map → chunk-KUZSW4FN.js.map} +1 -1
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +2 -2
  13. package/dist/convert/index.cjs +1 -1
  14. package/dist/convert/index.cjs.map +1 -1
  15. package/dist/convert/index.js +2 -2
  16. package/dist/index.cjs +1 -1
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/{schedule-V-fgepvP.d.cts → instruments-DTIuTlda.d.cts} +174 -140
  20. package/dist/{schedule-ClyON98C.d.ts → instruments-NcUb0ozc.d.ts} +174 -140
  21. package/dist/player/index.cjs +330 -84
  22. package/dist/player/index.cjs.map +1 -1
  23. package/dist/player/index.d.cts +40 -16
  24. package/dist/player/index.d.ts +40 -16
  25. package/dist/player/index.js +150 -69
  26. package/dist/player/index.js.map +1 -1
  27. package/dist/spec/index.cjs +1 -1
  28. package/dist/spec/index.cjs.map +1 -1
  29. package/dist/spec/index.d.cts +1 -1
  30. package/dist/spec/index.d.ts +1 -1
  31. package/dist/spec/index.js +1 -1
  32. package/dist/viewer/index.cjs +189 -31
  33. package/dist/viewer/index.cjs.map +1 -1
  34. package/dist/viewer/index.d.cts +62 -2
  35. package/dist/viewer/index.d.ts +62 -2
  36. package/dist/viewer/index.js +123 -4
  37. package/dist/viewer/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-L6YZ3GC6.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.cjs';
2
2
  import { a as LaySpeeds, T as TaalId } from '../lay-CXhC-HRO.cjs';
3
3
  export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-CXhC-HRO.cjs';
4
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-V-fgepvP.cjs';
5
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-V-fgepvP.cjs';
4
+ import { I as InstrumentId, a as InstrumentDef, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-DTIuTlda.cjs';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-DTIuTlda.cjs';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -140,8 +140,12 @@ declare class SwaralipiPlayer {
140
140
  get loop(): LineRange | null;
141
141
  /** Whether each repeat is followed by a silent pass for the learner. */
142
142
  get echo(): boolean;
143
- /** Set the document to play. Stops any current playback. */
144
- load(doc: Swaralipi): void;
143
+ /** Set the document to play. Stops any current playback. With `excerpt`,
144
+ * only the notes singing those stretches of the song's text play, in order
145
+ * (see `alignLyrics`); the length, seeks and repeats all keep to them. */
146
+ load(doc: Swaralipi, { excerpt }?: {
147
+ excerpt?: Excerpt;
148
+ }): void;
145
149
  /**
146
150
  * Taal mode: loop one avartan of a taal's theka (rhythm only, no melody) —
147
151
  * for a standalone theka player. `play()` loops indefinitely (tabla loop /
@@ -235,6 +239,10 @@ declare class SwaralipiPlayer {
235
239
 
236
240
  type AudioFormat = 'wav' | 'mp3';
237
241
  interface AudioExportOptions {
242
+ /** Play only the notes singing these stretches of the text (see alignLyrics). */
243
+ excerpt?: Excerpt;
244
+ /** The song's words: when given, the result carries their `cues` (an LRC's timing). */
245
+ lyrics?: string;
238
246
  /** Samples host (santoor/tabla/tanpura/metronome). Default the project CDN. */
239
247
  soundsBaseUrl?: string;
240
248
  /** Self-host the grand piano (see the player's `pianoBaseUrl`). */
@@ -289,6 +297,15 @@ interface AudioExportResult {
289
297
  mimeType: string;
290
298
  /** Rendered length in seconds. */
291
299
  durationSec: number;
300
+ /** Each sung line and its words, in seconds on this file's timeline (given `lyrics`). */
301
+ cues?: LyricCue[];
302
+ }
303
+ interface LyricCue {
304
+ sec: number;
305
+ words: {
306
+ sec: number;
307
+ text: string;
308
+ }[];
292
309
  }
293
310
  /**
294
311
  * Render a swaralipi doc to an audio Blob in the browser. Runs in an
@@ -304,11 +321,17 @@ declare function exportAudio(doc: Swaralipi, options?: AudioExportOptions): Prom
304
321
  */
305
322
  declare function encodeWav(channels: Float32Array[], sampleRate: number): Uint8Array;
306
323
  /**
307
- * Encode planar float channels to MP3 via lamejs. lamejs is **dynamically
308
- * imported** and an *optional* peer dependency (LGPL-3.0) — WAV needs no
309
- * dependency, so only reach for this when a compressed file is wanted. Samples
310
- * are fed in 1152-frame blocks (a multiple of 576, lamejs' preferred size).
324
+ * A streaming MP3 encoder: feed 16-bit PCM in any number of pieces, get one MP3 — one encoder
325
+ * delay, so a track joined from several renders keeps its timing. lamejs is **dynamically
326
+ * imported** and an *optional* peer dependency (LGPL-3.0). Samples go in 1152-frame blocks (a
327
+ * multiple of 576, lamejs' preferred size).
311
328
  */
329
+ declare function mp3Stream(sampleRate?: number, kbps?: number, channels?: 1 | 2): Promise<{
330
+ push(left: Int16Array, right?: Int16Array): void;
331
+ end(): Uint8Array[];
332
+ }>;
333
+ /** Encode planar float channels to MP3 via lamejs (see mp3Stream). WAV needs no dependency, so
334
+ * only reach for this when a compressed file is wanted. */
312
335
  declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?: number): Promise<Uint8Array>;
313
336
 
314
337
  /**
@@ -329,6 +352,8 @@ declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?:
329
352
  */
330
353
 
331
354
  interface MidiExportOptions {
355
+ /** Play only the notes singing these stretches of the text (see alignLyrics). */
356
+ excerpt?: Excerpt;
332
357
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
333
358
  tonic?: string;
334
359
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
@@ -353,13 +378,6 @@ interface MidiExportResult {
353
378
  filename: string;
354
379
  mimeType: string;
355
380
  }
356
- /**
357
- * Encode a schedule to a Standard MIDI File. Melody rides channel 0; when
358
- * `drums` is given, its per-matra voices ride a second GM channel-10 track
359
- * (the file becomes format 1). `drums` is one pattern for the whole song, or a
360
- * function picking each লয় / তাল segment's own (তালফেরতা; `undefined` = rest).
361
- * Pure — no Tone, no browser — directly testable.
362
- */
363
381
  declare function encodeMidi(schedule: Schedule, opts: {
364
382
  bpm: number;
365
383
  name?: string;
@@ -370,6 +388,12 @@ declare function encodeMidi(schedule: Schedule, opts: {
370
388
  * beyond `Blob` (present in Node 20+ too), so it works client-side or server-side.
371
389
  */
372
390
  declare function exportMidi(doc: Swaralipi, options?: MidiExportOptions): MidiExportResult;
391
+ /** Several songs on one timeline (a whole dance-drama), back to back with two beats' rest:
392
+ * each song opens with its own tempo and a marker naming it; drums share one channel-10 track. */
393
+ declare function exportMidiSet(items: {
394
+ doc: Swaralipi;
395
+ options?: MidiExportOptions;
396
+ }[], name: string): MidiExportResult;
373
397
 
374
398
  /** Octave register offset in octaves, relative to madhyam (middle). */
375
399
  declare const OCTAVE_OFFSET: Record<Octave, number>;
@@ -417,4 +441,4 @@ declare function sampleFileName(midi: number): string;
417
441
  */
418
442
  declare function sCurve(x: number, steepness?: number): number;
419
443
 
420
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, LaySpeeds, type LineRange, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type PracticeState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, TempoSegment, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
444
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, Excerpt, InstrumentDef, InstrumentId, LaySpeeds, type LineRange, type LyricCue, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type PracticeState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, TempoSegment, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, exportMidiSet, midiToSampleName, mp3Stream, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,8 +1,8 @@
1
1
  import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.js';
2
2
  import { a as LaySpeeds, T as TaalId } from '../lay-CWlEhImP.js';
3
3
  export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-CWlEhImP.js';
4
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-ClyON98C.js';
5
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-ClyON98C.js';
4
+ import { I as InstrumentId, a as InstrumentDef, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-NcUb0ozc.js';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-NcUb0ozc.js';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -140,8 +140,12 @@ declare class SwaralipiPlayer {
140
140
  get loop(): LineRange | null;
141
141
  /** Whether each repeat is followed by a silent pass for the learner. */
142
142
  get echo(): boolean;
143
- /** Set the document to play. Stops any current playback. */
144
- load(doc: Swaralipi): void;
143
+ /** Set the document to play. Stops any current playback. With `excerpt`,
144
+ * only the notes singing those stretches of the song's text play, in order
145
+ * (see `alignLyrics`); the length, seeks and repeats all keep to them. */
146
+ load(doc: Swaralipi, { excerpt }?: {
147
+ excerpt?: Excerpt;
148
+ }): void;
145
149
  /**
146
150
  * Taal mode: loop one avartan of a taal's theka (rhythm only, no melody) —
147
151
  * for a standalone theka player. `play()` loops indefinitely (tabla loop /
@@ -235,6 +239,10 @@ declare class SwaralipiPlayer {
235
239
 
236
240
  type AudioFormat = 'wav' | 'mp3';
237
241
  interface AudioExportOptions {
242
+ /** Play only the notes singing these stretches of the text (see alignLyrics). */
243
+ excerpt?: Excerpt;
244
+ /** The song's words: when given, the result carries their `cues` (an LRC's timing). */
245
+ lyrics?: string;
238
246
  /** Samples host (santoor/tabla/tanpura/metronome). Default the project CDN. */
239
247
  soundsBaseUrl?: string;
240
248
  /** Self-host the grand piano (see the player's `pianoBaseUrl`). */
@@ -289,6 +297,15 @@ interface AudioExportResult {
289
297
  mimeType: string;
290
298
  /** Rendered length in seconds. */
291
299
  durationSec: number;
300
+ /** Each sung line and its words, in seconds on this file's timeline (given `lyrics`). */
301
+ cues?: LyricCue[];
302
+ }
303
+ interface LyricCue {
304
+ sec: number;
305
+ words: {
306
+ sec: number;
307
+ text: string;
308
+ }[];
292
309
  }
293
310
  /**
294
311
  * Render a swaralipi doc to an audio Blob in the browser. Runs in an
@@ -304,11 +321,17 @@ declare function exportAudio(doc: Swaralipi, options?: AudioExportOptions): Prom
304
321
  */
305
322
  declare function encodeWav(channels: Float32Array[], sampleRate: number): Uint8Array;
306
323
  /**
307
- * Encode planar float channels to MP3 via lamejs. lamejs is **dynamically
308
- * imported** and an *optional* peer dependency (LGPL-3.0) — WAV needs no
309
- * dependency, so only reach for this when a compressed file is wanted. Samples
310
- * are fed in 1152-frame blocks (a multiple of 576, lamejs' preferred size).
324
+ * A streaming MP3 encoder: feed 16-bit PCM in any number of pieces, get one MP3 — one encoder
325
+ * delay, so a track joined from several renders keeps its timing. lamejs is **dynamically
326
+ * imported** and an *optional* peer dependency (LGPL-3.0). Samples go in 1152-frame blocks (a
327
+ * multiple of 576, lamejs' preferred size).
311
328
  */
329
+ declare function mp3Stream(sampleRate?: number, kbps?: number, channels?: 1 | 2): Promise<{
330
+ push(left: Int16Array, right?: Int16Array): void;
331
+ end(): Uint8Array[];
332
+ }>;
333
+ /** Encode planar float channels to MP3 via lamejs (see mp3Stream). WAV needs no dependency, so
334
+ * only reach for this when a compressed file is wanted. */
312
335
  declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?: number): Promise<Uint8Array>;
313
336
 
314
337
  /**
@@ -329,6 +352,8 @@ declare function encodeMp3(channels: Float32Array[], sampleRate: number, kbps?:
329
352
  */
330
353
 
331
354
  interface MidiExportOptions {
355
+ /** Play only the notes singing these stretches of the text (see alignLyrics). */
356
+ excerpt?: Excerpt;
332
357
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
333
358
  tonic?: string;
334
359
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
@@ -353,13 +378,6 @@ interface MidiExportResult {
353
378
  filename: string;
354
379
  mimeType: string;
355
380
  }
356
- /**
357
- * Encode a schedule to a Standard MIDI File. Melody rides channel 0; when
358
- * `drums` is given, its per-matra voices ride a second GM channel-10 track
359
- * (the file becomes format 1). `drums` is one pattern for the whole song, or a
360
- * function picking each লয় / তাল segment's own (তালফেরতা; `undefined` = rest).
361
- * Pure — no Tone, no browser — directly testable.
362
- */
363
381
  declare function encodeMidi(schedule: Schedule, opts: {
364
382
  bpm: number;
365
383
  name?: string;
@@ -370,6 +388,12 @@ declare function encodeMidi(schedule: Schedule, opts: {
370
388
  * beyond `Blob` (present in Node 20+ too), so it works client-side or server-side.
371
389
  */
372
390
  declare function exportMidi(doc: Swaralipi, options?: MidiExportOptions): MidiExportResult;
391
+ /** Several songs on one timeline (a whole dance-drama), back to back with two beats' rest:
392
+ * each song opens with its own tempo and a marker naming it; drums share one channel-10 track. */
393
+ declare function exportMidiSet(items: {
394
+ doc: Swaralipi;
395
+ options?: MidiExportOptions;
396
+ }[], name: string): MidiExportResult;
373
397
 
374
398
  /** Octave register offset in octaves, relative to madhyam (middle). */
375
399
  declare const OCTAVE_OFFSET: Record<Octave, number>;
@@ -417,4 +441,4 @@ declare function sampleFileName(midi: number): string;
417
441
  */
418
442
  declare function sCurve(x: number, steepness?: number): number;
419
443
 
420
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, LaySpeeds, type LineRange, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type PracticeState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, TempoSegment, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
444
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, Excerpt, InstrumentDef, InstrumentId, LaySpeeds, type LineRange, type LyricCue, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type PracticeState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, TempoSegment, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, exportMidiSet, midiToSampleName, mp3Stream, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,5 +1,5 @@
1
- import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, eventAt, lineAt, byRow, lineFor, quantizeLoopEnd, instrumentSampleUrl, probeUrl, scheduleLines, GRACE_STEP, normalizeBase, nearestTablaBpm, tablaBpms, graceTouches, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaShiftSemitones, tablaUrl, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-L6YZ3GC6.js';
2
- export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, GRACE_STEP, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-L6YZ3GC6.js';
1
+ import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, eventAt, lineAt, byRow, lineFor, quantizeLoopEnd, instrumentSampleUrl, probeUrl, scheduleLines, GRACE_STEP, normalizeBase, nearestTablaBpm, tablaBpms, graceTouches, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaShiftSemitones, tablaUrl, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, lyricCues, alignLyrics, MEEND_HOLD, sCurve } from '../chunk-GIOGYJMM.js';
2
+ export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, GRACE_STEP, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, alignLyrics, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, lyricCues, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-GIOGYJMM.js';
3
3
  import '../chunk-BSZ3WQUP.js';
4
4
  import { TAALS } from '../chunk-KOHSKHXI.js';
5
5
  export { LAYS, LAY_SPEEDS, printedLay, resolveLaySpeeds, rowLay } from '../chunk-KOHSKHXI.js';
@@ -45,6 +45,8 @@ var SwaralipiPlayer = class {
45
45
  /** Per-song setBpm tweak: beats meta.bpm but is cleared by the next load(). */
46
46
  #bpmTweak;
47
47
  #doc;
48
+ /** Play only these stretches of the song's text (see `load`). */
49
+ #excerpt;
48
50
  /** Taal mode (`loadTaal`): loops one avartan of this taal's theka with no
49
51
  * melody. Mutually exclusive with `#doc`. */
50
52
  #taal;
@@ -87,6 +89,8 @@ var SwaralipiPlayer = class {
87
89
  /** Listeners on the bound sheet, removed on rebind. */
88
90
  #unbind;
89
91
  #sheet;
92
+ /** The bound sheet's highlight — beside, never instead of, the consumer's `onHighlight`. */
93
+ #paint;
90
94
  /** Preview cap, counted in listening time so a seek or a loop can't dodge it. */
91
95
  #listened = 0;
92
96
  #listenedSince = 0;
@@ -177,10 +181,13 @@ var SwaralipiPlayer = class {
177
181
  /* ----------------------------------------------------------------- */
178
182
  /* Loading + transport control */
179
183
  /* ----------------------------------------------------------------- */
180
- /** Set the document to play. Stops any current playback. */
181
- load(doc) {
184
+ /** Set the document to play. Stops any current playback. With `excerpt`,
185
+ * only the notes singing those stretches of the song's text play, in order
186
+ * (see `alignLyrics`); the length, seeks and repeats all keep to them. */
187
+ load(doc, { excerpt } = {}) {
182
188
  this.#halt();
183
189
  this.#doc = doc;
190
+ this.#excerpt = excerpt;
184
191
  this.#taal = void 0;
185
192
  this.#taalOverride = void 0;
186
193
  this.#bpmTweak = void 0;
@@ -503,7 +510,7 @@ var SwaralipiPlayer = class {
503
510
  */
504
511
  bindSheet(target, opts = {}) {
505
512
  const block = opts.autoScroll === true ? "center" : opts.autoScroll || void 0;
506
- this.#opts.onHighlight = (path, yourTurn) => {
513
+ this.#paint = (path, yourTurn) => {
507
514
  target.clearHighlights();
508
515
  if (path === null) return;
509
516
  const el2 = target.highlight(path, yourTurn);
@@ -615,6 +622,7 @@ var SwaralipiPlayer = class {
615
622
  this.#opts.onState?.(state);
616
623
  }
617
624
  #emitHighlight(path, yourTurn = false) {
625
+ this.#paint?.(path, yourTurn);
618
626
  this.#opts.onHighlight?.(path, yourTurn);
619
627
  }
620
628
  #emitLoop() {
@@ -772,7 +780,8 @@ var SwaralipiPlayer = class {
772
780
  tonic: this.#opts.tonic,
773
781
  baseOctave: this.#opts.baseOctave,
774
782
  laySpeeds: this.#opts.laySpeeds,
775
- repeats: this.#opts.repeats
783
+ repeats: this.#opts.repeats,
784
+ excerpt: this.#excerpt
776
785
  };
777
786
  }
778
787
  /** Effective tempo: snapped to the taal's available tabla tempos while a
@@ -1095,6 +1104,38 @@ function midiRange2(schedule) {
1095
1104
  }
1096
1105
  return lo <= hi ? { lo, hi } : void 0;
1097
1106
  }
1107
+ var AHEAD_SEC = 2;
1108
+ async function renderOffline(Tone, build, durationSec, channels, sampleRate) {
1109
+ const chunked = "suspend" in OfflineAudioContext.prototype && "positionX" in AudioListener.prototype;
1110
+ const context = chunked ? new Tone.OfflineContext(new OfflineAudioContext(channels, Math.ceil(durationSec * sampleRate), sampleRate)) : new Tone.OfflineContext(channels, durationSec, sampleRate);
1111
+ const raw = context.rawContext;
1112
+ const original = Tone.getContext();
1113
+ Tone.setContext(context);
1114
+ try {
1115
+ await build(context);
1116
+ } finally {
1117
+ Tone.setContext(original);
1118
+ }
1119
+ const clock = context;
1120
+ const run = (until) => {
1121
+ while (clock._currentTime <= Math.min(until, durationSec)) {
1122
+ clock.emit("tick");
1123
+ clock._currentTime += 128 / sampleRate;
1124
+ }
1125
+ };
1126
+ if (chunked) {
1127
+ run(2 * AHEAD_SEC);
1128
+ for (let t = AHEAD_SEC; t < durationSec; t += AHEAD_SEC) {
1129
+ void raw.suspend(t).then(() => {
1130
+ run(t + 2 * AHEAD_SEC);
1131
+ void raw.resume();
1132
+ });
1133
+ }
1134
+ } else {
1135
+ run(Infinity);
1136
+ }
1137
+ return raw.startRendering();
1138
+ }
1098
1139
  function pickDef(id, o) {
1099
1140
  const def = o.customInstruments?.[id] ?? INSTRUMENTS[id];
1100
1141
  if (def === void 0) return INSTRUMENTS.harmonium;
@@ -1123,7 +1164,8 @@ async function exportAudio(doc, options = {}) {
1123
1164
  tonic: options.tonic,
1124
1165
  baseOctave: options.baseOctave,
1125
1166
  laySpeeds: options.laySpeeds,
1126
- repeats: options.repeats ?? true
1167
+ repeats: options.repeats ?? true,
1168
+ excerpt: options.excerpt
1127
1169
  });
1128
1170
  const range = midiRange2(schedule);
1129
1171
  const taalId = resolveTaal(doc, options.taal);
@@ -1136,7 +1178,8 @@ async function exportAudio(doc, options = {}) {
1136
1178
  const def = pickDef(id, options);
1137
1179
  const shift = rhythm !== "none" ? schedule.pickupBeats * spb : 0;
1138
1180
  const durationSec = Math.max(schedule.totalBeats * spb + shift + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
1139
- const rendered = await Tone.Offline(
1181
+ const rendered = await renderOffline(
1182
+ Tone,
1140
1183
  async ({ transport }) => {
1141
1184
  transport.bpm.value = bpm;
1142
1185
  const master = new Tone.Gain(1).toDestination();
@@ -1283,11 +1326,16 @@ async function exportAudio(doc, options = {}) {
1283
1326
  for (let c = 0; c < rendered.numberOfChannels; c++) chans.push(rendered.getChannelData(c));
1284
1327
  const bytes = format === "mp3" ? await encodeMp3(chans, rendered.sampleRate, options.mp3Kbps) : encodeWav(chans, rendered.sampleRate);
1285
1328
  const mimeType = format === "mp3" ? "audio/mpeg" : "audio/wav";
1329
+ const cues = options.lyrics === void 0 ? void 0 : lyricCues(schedule.events, alignLyrics(doc, options.lyrics), options.lyrics).map((cue) => ({
1330
+ sec: cue.beat * spb + shift,
1331
+ words: cue.words.map((word) => ({ sec: word.beat * spb + shift, text: word.text }))
1332
+ }));
1286
1333
  return {
1287
1334
  blob: new Blob([bytes], { type: mimeType }),
1288
1335
  filename: `${safeName(doc.meta.title)}.${format}`,
1289
1336
  mimeType,
1290
- durationSec: rendered.duration
1337
+ durationSec: rendered.duration,
1338
+ ...cues === void 0 ? {} : { cues }
1291
1339
  };
1292
1340
  }
1293
1341
  function toInt16(f) {
@@ -1354,22 +1402,32 @@ async function loadLame() {
1354
1402
  );
1355
1403
  }
1356
1404
  }
1357
- async function encodeMp3(channels, sampleRate, kbps = 192) {
1405
+ async function mp3Stream(sampleRate = 44100, kbps = 192, channels = 1) {
1358
1406
  const Mp3Encoder = await loadLame();
1359
- const numCh = Math.min(channels.length, 2);
1360
- const enc = new Mp3Encoder(numCh, sampleRate, kbps);
1361
- const left = floatToInt16(channels[0] ?? new Float32Array());
1362
- const right = numCh > 1 ? floatToInt16(channels[1]) : void 0;
1363
- const block = 1152;
1407
+ const enc = new Mp3Encoder(channels, sampleRate, kbps);
1364
1408
  const out = [];
1365
- for (let i = 0; i < left.length; i += block) {
1366
- const l = left.subarray(i, i + block);
1367
- const chunk = right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l);
1409
+ const keep = (chunk) => {
1368
1410
  if (chunk.length > 0) out.push(new Uint8Array(chunk));
1369
- }
1370
- const tail = enc.flush();
1371
- if (tail.length > 0) out.push(new Uint8Array(tail));
1372
- return concatBytes(out);
1411
+ };
1412
+ const block = 1152;
1413
+ return {
1414
+ push(left, right) {
1415
+ for (let i = 0; i < left.length; i += block) {
1416
+ const l = left.subarray(i, i + block);
1417
+ keep(right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l));
1418
+ }
1419
+ },
1420
+ end() {
1421
+ keep(enc.flush());
1422
+ return out;
1423
+ }
1424
+ };
1425
+ }
1426
+ async function encodeMp3(channels, sampleRate, kbps = 192) {
1427
+ const numCh = Math.min(channels.length, 2);
1428
+ const mp3 = await mp3Stream(sampleRate, kbps, numCh);
1429
+ mp3.push(floatToInt16(channels[0] ?? new Float32Array()), numCh > 1 ? floatToInt16(channels[1]) : void 0);
1430
+ return concatBytes(mp3.end());
1373
1431
  }
1374
1432
 
1375
1433
  // src/player/midi.ts
@@ -1415,6 +1473,11 @@ function trackChunk(raw, head) {
1415
1473
  const track = [...head];
1416
1474
  let last = 0;
1417
1475
  for (const e of raw) {
1476
+ if (e.meta) {
1477
+ track.push(...vlq(e.tick - last), ...e.meta);
1478
+ last = e.tick;
1479
+ continue;
1480
+ }
1418
1481
  const status = e.status !== void 0 ? e.status | e.ch & 15 : (e.on ? 144 : 128) | e.ch & 15;
1419
1482
  track.push(...vlq(e.tick - last), status, e.note & 127, e.vel & 127);
1420
1483
  last = e.tick;
@@ -1422,22 +1485,22 @@ function trackChunk(raw, head) {
1422
1485
  track.push(...vlq(0), 255, 47, 0);
1423
1486
  return [77, 84, 114, 107, ...u32be(track.length), ...track];
1424
1487
  }
1425
- function encodeMidi(schedule, opts) {
1488
+ function melodyEvents(schedule, offset = 0) {
1426
1489
  const melody = [];
1427
1490
  const hasGlide = schedule.events.some((ev) => ev.glide === true);
1428
1491
  if (hasGlide) melody.push(...bendRangeSetup());
1429
1492
  schedule.events.forEach((ev, i) => {
1430
1493
  if (ev.glide === true) return;
1431
1494
  for (const g of graceTouches(ev)) {
1432
- const on = Math.max(Math.round(g.beat * PPQ), 0);
1495
+ const on = Math.max(Math.round(g.beat * PPQ) + offset, 0);
1433
1496
  melody.push({ tick: on, on: true, note: g.midi, vel: 70, ch: 0 });
1434
- melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ)), on: false, note: g.midi, vel: 0, ch: 0 });
1497
+ melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ) + offset), on: false, note: g.midi, vel: 0, ch: 0 });
1435
1498
  }
1436
1499
  if (ev.midi === null) return;
1437
- const start = Math.round(ev.beat * PPQ);
1500
+ const start = Math.round(ev.beat * PPQ) + offset;
1438
1501
  const targets = glideTargets(schedule.events, i);
1439
1502
  const last = targets[targets.length - 1] ?? ev;
1440
- const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ), start + 1);
1503
+ const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ) + offset, start + 1);
1441
1504
  melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
1442
1505
  melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
1443
1506
  let from = ev;
@@ -1447,72 +1510,90 @@ function encodeMidi(schedule, opts) {
1447
1510
  const u = n / BEND_STEPS;
1448
1511
  const beat = glideStart + (to.beat - glideStart) * u;
1449
1512
  const semis = from.midi - ev.midi + (to.midi - from.midi) * sCurve(u);
1450
- melody.push(bendEvent(Math.round(beat * PPQ), semis));
1513
+ melody.push(bendEvent(Math.round(beat * PPQ) + offset, semis));
1451
1514
  }
1452
1515
  from = to;
1453
1516
  }
1454
1517
  if (targets.length > 0) melody.push(bendEvent(end, 0));
1455
1518
  });
1519
+ return melody;
1520
+ }
1521
+ function drumEvents(schedule, drums, offset = 0) {
1522
+ const drum = [];
1523
+ for (const seg of schedule.tempo) {
1524
+ const pattern = typeof drums === "function" ? drums(seg) : drums;
1525
+ if (!pattern?.length) continue;
1526
+ const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
1527
+ for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
1528
+ const note = DRUM_NOTE[pattern[k % pattern.length]];
1529
+ const t = Math.round((seg.beat + k / seg.scale) * PPQ) + offset;
1530
+ drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
1531
+ drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
1532
+ }
1533
+ }
1534
+ return drum;
1535
+ }
1536
+ function midiFile(chunks) {
1537
+ const format = chunks.length > 1 ? 1 : 0;
1538
+ const header = [77, 84, 104, 100, ...u32be(6), 0, format, chunks.length >> 8 & 255, chunks.length & 255, PPQ >> 8 & 255, PPQ & 255];
1539
+ return new Uint8Array([...header, ...chunks.flat()]);
1540
+ }
1541
+ function meta(type, data) {
1542
+ return [255, type, ...vlq(data.length), ...data];
1543
+ }
1544
+ function encodeMidi(schedule, opts) {
1456
1545
  const usPerBeat = Math.round(6e7 / opts.bpm);
1457
1546
  const head = [];
1458
1547
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
1459
1548
  head.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
1460
- const chunks = [trackChunk(melody, head)];
1549
+ const chunks = [trackChunk(melodyEvents(schedule), head)];
1461
1550
  if (typeof opts.drums === "function" || opts.drums && opts.drums.length > 0) {
1462
- const drum = [];
1463
- for (const seg of schedule.tempo) {
1464
- const pattern = typeof opts.drums === "function" ? opts.drums(seg) : opts.drums;
1465
- if (!pattern?.length) continue;
1466
- const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
1467
- for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
1468
- const note = DRUM_NOTE[pattern[k % pattern.length]];
1469
- const t = Math.round((seg.beat + k / seg.scale) * PPQ);
1470
- drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
1471
- drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
1472
- }
1473
- }
1551
+ const drum = drumEvents(schedule, opts.drums);
1474
1552
  chunks.push(trackChunk(drum, [...vlq(0), 255, 3, ...vlq(5), 68, 114, 117, 109, 115]));
1475
1553
  }
1476
- const format = chunks.length > 1 ? 1 : 0;
1477
- const header = [
1478
- 77,
1479
- 84,
1480
- 104,
1481
- 100,
1482
- ...u32be(6),
1483
- 0,
1484
- format,
1485
- chunks.length >> 8 & 255,
1486
- chunks.length & 255,
1487
- PPQ >> 8 & 255,
1488
- PPQ & 255
1489
- ];
1490
- return new Uint8Array([...header, ...chunks.flat()]);
1554
+ return midiFile(chunks);
1555
+ }
1556
+ function exportSchedule(doc, o) {
1557
+ return buildSchedule(doc, { tonic: o.tonic, baseOctave: o.baseOctave, laySpeeds: o.laySpeeds, repeats: o.repeats ?? true, excerpt: o.excerpt });
1558
+ }
1559
+ function taalDrums(seg) {
1560
+ if (seg.taal === null) return void 0;
1561
+ const id = seg.taal?.id;
1562
+ return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
1491
1563
  }
1492
1564
  function safeName2(title) {
1493
1565
  return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
1494
1566
  }
1495
1567
  function exportMidi(doc, options = {}) {
1496
- const schedule = buildSchedule(doc, {
1497
- tonic: options.tonic,
1498
- baseOctave: options.baseOctave,
1499
- laySpeeds: options.laySpeeds,
1500
- repeats: options.repeats ?? true
1501
- });
1568
+ const schedule = exportSchedule(doc, options);
1502
1569
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1503
- const drums = options.drums ? (seg) => {
1504
- const id = seg.taal?.id;
1505
- if (seg.taal === null) return void 0;
1506
- return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
1507
- } : void 0;
1508
- const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums });
1570
+ const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums: options.drums ? taalDrums : void 0 });
1509
1571
  return {
1510
1572
  blob: new Blob([bytes], { type: "audio/midi" }),
1511
1573
  filename: `${safeName2(doc.meta.title)}.mid`,
1512
1574
  mimeType: "audio/midi"
1513
1575
  };
1514
1576
  }
1577
+ function exportMidiSet(items, name) {
1578
+ const melody = [];
1579
+ const drum = [];
1580
+ let beat = 0;
1581
+ for (const { doc, options = {} } of items) {
1582
+ const schedule = exportSchedule(doc, options);
1583
+ const tick = beat * PPQ;
1584
+ const usPerBeat = Math.round(6e7 / (options.bpm ?? doc.meta.bpm ?? 90));
1585
+ const at = (bytes2) => ({ tick, on: false, note: 0, vel: 0, ch: 0, meta: bytes2 });
1586
+ melody.push(at(meta(81, [usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255])));
1587
+ melody.push(at(meta(6, utf8(options.name ?? doc.meta.title ?? ""))), ...melodyEvents(schedule, tick));
1588
+ if (options.drums) drum.push(...drumEvents(schedule, taalDrums, tick));
1589
+ beat += Math.ceil(schedule.totalBeats) + 2;
1590
+ }
1591
+ const chunks = [trackChunk(melody, [...vlq(0), ...meta(3, utf8(name))])];
1592
+ if (drum.length > 0) chunks.push(trackChunk(drum, [...vlq(0), ...meta(3, utf8("Drums"))]));
1593
+ const bytes = midiFile(chunks);
1594
+ return { blob: new Blob([bytes], { type: "audio/midi" }), filename: `${safeName2(name)}.mid`, mimeType: "audio/midi" };
1595
+ }
1515
1596
 
1516
- export { SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi };
1597
+ export { SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, exportMidiSet, mp3Stream };
1517
1598
  //# sourceMappingURL=index.js.map
1518
1599
  //# sourceMappingURL=index.js.map