@ugfoundation/swaralipi-js 0.7.6 → 0.8.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.
Files changed (50) hide show
  1. package/dist/{batch-5NMEPJ6E.js → batch-NJMT3M5Z.js} +5 -5
  2. package/dist/{batch-5NMEPJ6E.js.map → batch-NJMT3M5Z.js.map} +1 -1
  3. package/dist/{chunk-ITOP5O5M.js → chunk-4HMHTXDE.js} +15 -4
  4. package/dist/chunk-4HMHTXDE.js.map +1 -0
  5. package/dist/{chunk-UF5OSJRP.js → chunk-4HNT6OAT.js} +32 -3
  6. package/dist/chunk-4HNT6OAT.js.map +1 -0
  7. package/dist/{chunk-2SAZWTTK.js → chunk-4IVUPMRK.js} +29 -11
  8. package/dist/chunk-4IVUPMRK.js.map +1 -0
  9. package/dist/{chunk-NJGHFQJP.js → chunk-YFVAVICF.js} +4 -4
  10. package/dist/{chunk-NJGHFQJP.js.map → chunk-YFVAVICF.js.map} +1 -1
  11. package/dist/cli.cjs +13 -2
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +3 -3
  14. package/dist/convert/index.cjs +12 -1
  15. package/dist/convert/index.cjs.map +1 -1
  16. package/dist/convert/index.d.cts +1 -1
  17. package/dist/convert/index.d.ts +1 -1
  18. package/dist/convert/index.js +3 -3
  19. package/dist/index.cjs +49 -1
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +3 -3
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.js +2 -2
  24. package/dist/{taals-CjzBuZ35.d.ts → lay-Csa4CD46.d.cts} +38 -1
  25. package/dist/{taals-CjzBuZ35.d.cts → lay-Ct9ky_EE.d.ts} +38 -1
  26. package/dist/player/index.cjs +286 -119
  27. package/dist/player/index.cjs.map +1 -1
  28. package/dist/player/index.d.cts +22 -9
  29. package/dist/player/index.d.ts +22 -9
  30. package/dist/player/index.js +230 -114
  31. package/dist/player/index.js.map +1 -1
  32. package/dist/{schedule-BpBYGXPL.d.cts → schedule-B_UaluBi.d.cts} +27 -3
  33. package/dist/{schedule-Tzk01FKj.d.ts → schedule-CdILTNsk.d.ts} +27 -3
  34. package/dist/{schema-DqBXPGkL.d.ts → schema-DGA_bNVZ.d.cts} +97 -1
  35. package/dist/{schema-DqBXPGkL.d.cts → schema-DGA_bNVZ.d.ts} +97 -1
  36. package/dist/spec/index.cjs +49 -1
  37. package/dist/spec/index.cjs.map +1 -1
  38. package/dist/spec/index.d.cts +31 -5
  39. package/dist/spec/index.d.ts +31 -5
  40. package/dist/spec/index.js +2 -2
  41. package/dist/viewer/index.cjs +134 -16
  42. package/dist/viewer/index.cjs.map +1 -1
  43. package/dist/viewer/index.d.cts +10 -32
  44. package/dist/viewer/index.d.ts +10 -32
  45. package/dist/viewer/index.js +44 -9
  46. package/dist/viewer/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-2SAZWTTK.js.map +0 -1
  49. package/dist/chunk-ITOP5O5M.js.map +0 -1
  50. package/dist/chunk-UF5OSJRP.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DqBXPGkL.cjs';
2
- import { T as TaalId } from '../taals-CjzBuZ35.cjs';
3
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-BpBYGXPL.cjs';
4
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as createBuffers, m as drumPattern, n as glideTargets, o as instrumentSampleUrl, p as isArcRaw, q as meendContinuations, r as nearestSampleKey, s as nearestTablaBpm, t as normalizeBase, u as probeUrl, v as quantizeLoopEnd, w as santoorUrls, x as subsetUrls, y as tablaBpms, z as tablaUrl, B as triggerGlide } from '../schedule-BpBYGXPL.cjs';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DGA_bNVZ.cjs';
2
+ import { a as LaySpeeds, T as TaalId } from '../lay-Csa4CD46.cjs';
3
+ export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-Csa4CD46.cjs';
4
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-B_UaluBi.cjs';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as TempoSegment, k as ToneModule, l as buildSchedule, m as createBuffers, n as drumPattern, o as glideTargets, p as instrumentSampleUrl, q as isArcRaw, r as meendContinuations, s as nearestSampleKey, t as nearestTablaBpm, u as normalizeBase, v as probeUrl, w as quantizeLoopEnd, x as santoorUrls, y as subsetUrls, z as tablaBpms, B as tablaUrl, C as triggerGlide } from '../schedule-B_UaluBi.cjs';
5
6
  import 'zod';
6
7
  import 'tone';
7
8
 
@@ -17,8 +18,13 @@ interface PlayerOptions {
17
18
  pianoBaseUrl?: string;
18
19
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
19
20
  tonic?: string;
20
- /** Tempo in matra/min. Default 90. Snapped to the 15-grid while tabla plays. */
21
+ /** Tempo in matra/min — the **দ্রুত** (fast) tempo; লয় marks scale down from
22
+ * it. Default 90. Snapped to the 15-grid while tabla plays. */
21
23
  bpm?: number;
24
+ /** Override the লয় tempo factors, e.g. `{ vilambit: 0.6 }`. Defaults come
25
+ * from the document's `meta.laySpeeds`, else `LAY_SPEEDS` (drut 1, madhya
26
+ * 0.75, vilambit 0.5). */
27
+ laySpeeds?: LaySpeeds;
22
28
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
23
29
  baseOctave?: number;
24
30
  /** Active melody instrument id (a built-in or a `customInstruments` key).
@@ -168,9 +174,12 @@ interface AudioExportOptions {
168
174
  customInstruments?: Record<string, InstrumentDef>;
169
175
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
170
176
  tonic?: string;
171
- /** Tempo in matra/min. Default: `meta.bpm`, else 90. Snapped to the tabla grid
172
- * when a sampled tabla actually plays. */
177
+ /** Tempo in matra/min — the **দ্রুত** (fast) tempo; লয় marks scale down from
178
+ * it. Default: `meta.bpm`, else 90. Snapped to the tabla grid when a sampled
179
+ * tabla actually plays. */
173
180
  bpm?: number;
181
+ /** Override the লয় tempo factors (see LAY_SPEEDS / meta.laySpeeds). */
182
+ laySpeeds?: LaySpeeds;
174
183
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
175
184
  baseOctave?: number;
176
185
  /** Melody instrument id. Default `piano`. */
@@ -252,8 +261,12 @@ interface MidiExportOptions {
252
261
  tonic?: string;
253
262
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
254
263
  baseOctave?: number;
255
- /** Tempo in matra/min written as the file's tempo. Default `meta.bpm`, else 90. */
264
+ /** Tempo in matra/min written as the file's tempo — the **দ্রুত** tempo;
265
+ * লয় marks come out as longer note values against it, not as tempo changes.
266
+ * Default `meta.bpm`, else 90. */
256
267
  bpm?: number;
268
+ /** Override the লয় tempo factors (see LAY_SPEEDS / meta.laySpeeds). */
269
+ laySpeeds?: LaySpeeds;
257
270
  /** Track name meta-event. Default the song title. */
258
271
  name?: string;
259
272
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
@@ -327,4 +340,4 @@ declare function sampleFileName(midi: number): string;
327
340
  */
328
341
  declare function sCurve(x: number, steepness?: number): number;
329
342
 
330
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
343
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, LaySpeeds, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,7 +1,8 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DqBXPGkL.js';
2
- import { T as TaalId } from '../taals-CjzBuZ35.js';
3
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-Tzk01FKj.js';
4
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as createBuffers, m as drumPattern, n as glideTargets, o as instrumentSampleUrl, p as isArcRaw, q as meendContinuations, r as nearestSampleKey, s as nearestTablaBpm, t as normalizeBase, u as probeUrl, v as quantizeLoopEnd, w as santoorUrls, x as subsetUrls, y as tablaBpms, z as tablaUrl, B as triggerGlide } from '../schedule-Tzk01FKj.js';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DGA_bNVZ.js';
2
+ import { a as LaySpeeds, T as TaalId } from '../lay-Ct9ky_EE.js';
3
+ export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-Ct9ky_EE.js';
4
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-CdILTNsk.js';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as TempoSegment, k as ToneModule, l as buildSchedule, m as createBuffers, n as drumPattern, o as glideTargets, p as instrumentSampleUrl, q as isArcRaw, r as meendContinuations, s as nearestSampleKey, t as nearestTablaBpm, u as normalizeBase, v as probeUrl, w as quantizeLoopEnd, x as santoorUrls, y as subsetUrls, z as tablaBpms, B as tablaUrl, C as triggerGlide } from '../schedule-CdILTNsk.js';
5
6
  import 'zod';
6
7
  import 'tone';
7
8
 
@@ -17,8 +18,13 @@ interface PlayerOptions {
17
18
  pianoBaseUrl?: string;
18
19
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
19
20
  tonic?: string;
20
- /** Tempo in matra/min. Default 90. Snapped to the 15-grid while tabla plays. */
21
+ /** Tempo in matra/min — the **দ্রুত** (fast) tempo; লয় marks scale down from
22
+ * it. Default 90. Snapped to the 15-grid while tabla plays. */
21
23
  bpm?: number;
24
+ /** Override the লয় tempo factors, e.g. `{ vilambit: 0.6 }`. Defaults come
25
+ * from the document's `meta.laySpeeds`, else `LAY_SPEEDS` (drut 1, madhya
26
+ * 0.75, vilambit 0.5). */
27
+ laySpeeds?: LaySpeeds;
22
28
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
23
29
  baseOctave?: number;
24
30
  /** Active melody instrument id (a built-in or a `customInstruments` key).
@@ -168,9 +174,12 @@ interface AudioExportOptions {
168
174
  customInstruments?: Record<string, InstrumentDef>;
169
175
  /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
170
176
  tonic?: string;
171
- /** Tempo in matra/min. Default: `meta.bpm`, else 90. Snapped to the tabla grid
172
- * when a sampled tabla actually plays. */
177
+ /** Tempo in matra/min — the **দ্রুত** (fast) tempo; লয় marks scale down from
178
+ * it. Default: `meta.bpm`, else 90. Snapped to the tabla grid when a sampled
179
+ * tabla actually plays. */
173
180
  bpm?: number;
181
+ /** Override the লয় tempo factors (see LAY_SPEEDS / meta.laySpeeds). */
182
+ laySpeeds?: LaySpeeds;
174
183
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
175
184
  baseOctave?: number;
176
185
  /** Melody instrument id. Default `piano`. */
@@ -252,8 +261,12 @@ interface MidiExportOptions {
252
261
  tonic?: string;
253
262
  /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
254
263
  baseOctave?: number;
255
- /** Tempo in matra/min written as the file's tempo. Default `meta.bpm`, else 90. */
264
+ /** Tempo in matra/min written as the file's tempo — the **দ্রুত** tempo;
265
+ * লয় marks come out as longer note values against it, not as tempo changes.
266
+ * Default `meta.bpm`, else 90. */
256
267
  bpm?: number;
268
+ /** Override the লয় tempo factors (see LAY_SPEEDS / meta.laySpeeds). */
269
+ laySpeeds?: LaySpeeds;
257
270
  /** Track name meta-event. Default the song title. */
258
271
  name?: string;
259
272
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
@@ -327,4 +340,4 @@ declare function sampleFileName(midi: number): string;
327
340
  */
328
341
  declare function sCurve(x: number, steepness?: number): number;
329
342
 
330
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
343
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, LaySpeeds, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,9 +1,14 @@
1
- import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, quantizeLoopEnd, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaUrl, tablaShiftSemitones, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-2SAZWTTK.js';
2
- export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, glideTargets, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-2SAZWTTK.js';
3
- import { TAALS } from '../chunk-UF5OSJRP.js';
1
+ import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, quantizeLoopEnd, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, drumPattern, tablaShiftSemitones, tablaUrl, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-4IVUPMRK.js';
2
+ export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, glideTargets, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-4IVUPMRK.js';
3
+ import { TAALS } from '../chunk-4HNT6OAT.js';
4
+ export { LAYS, LAY_SPEEDS, printedLay, resolveLaySpeeds, rowLay } from '../chunk-4HNT6OAT.js';
4
5
 
5
6
  // src/player/player.ts
6
7
  var STOP_FADE_SEC = 0.5;
8
+ var LAY_TAIL_SEC = 0.12;
9
+ var TAAL_MODE_TEMPO = [
10
+ { beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
11
+ ];
7
12
  var MEDIA_LEAD_SEC = 0.25;
8
13
  function midiRange(schedule) {
9
14
  let lo = Infinity;
@@ -48,8 +53,11 @@ var SwaralipiPlayer = class {
48
53
  #tanpura;
49
54
  #tanpuraKey = "";
50
55
  #metronome;
51
- /** The looping tabla — a `GrainPlayer` when the tonic needs it retuned. */
52
- #tabla;
56
+ /** The looping tabla — one loop per লয় segment, each a `GrainPlayer` when
57
+ * the tonic needs retuning or the segment's tempo needs stretching.
58
+ * `fileSpb` is the recorded loop's OWN beat length, which is the unit
59
+ * `quantizeLoopEnd` must snap in (loop points are buffer time). */
60
+ #tabla = [];
53
61
  #drums;
54
62
  #scheduleIds = [];
55
63
  #fadeTimer;
@@ -60,6 +68,7 @@ var SwaralipiPlayer = class {
60
68
  customInstruments: options.customInstruments,
61
69
  tonic: options.tonic,
62
70
  bpm: options.bpm,
71
+ laySpeeds: options.laySpeeds,
63
72
  baseOctave: options.baseOctave ?? 4,
64
73
  instrument: options.instrument ?? "piano",
65
74
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
@@ -113,8 +122,7 @@ var SwaralipiPlayer = class {
113
122
  get durationSec() {
114
123
  if (this.#taal !== void 0) return Infinity;
115
124
  if (this.#doc === void 0) return 0;
116
- const schedule = buildSchedule(this.#doc, { tonic: this.#opts.tonic, baseOctave: this.#opts.baseOctave });
117
- const full = playDurationSec(schedule, 60 / this.#effectiveBpm(), this.#opts.rhythm);
125
+ const full = playDurationSec(buildSchedule(this.#doc, this.#scheduleOpts()), 60 / this.#effectiveBpm(), this.#opts.rhythm);
118
126
  return Math.min(full, this.#opts.previewLimitSec);
119
127
  }
120
128
  /** Seconds played so far, off the Transport's own clock — so it holds while
@@ -218,8 +226,7 @@ var SwaralipiPlayer = class {
218
226
  transport.cancel(0);
219
227
  transport.position = 0;
220
228
  }
221
- this.#tabla?.dispose();
222
- this.#tabla = void 0;
229
+ this.#disposeTabla();
223
230
  this.#restoreMasterGain();
224
231
  this.#scheduleIds = [];
225
232
  this.#emitHighlight(null);
@@ -237,6 +244,17 @@ var SwaralipiPlayer = class {
237
244
  this.#cancelFadeTimer();
238
245
  this.#restoreMasterGain();
239
246
  }
247
+ #disposeTabla() {
248
+ for (const { loop } of this.#tabla) loop.dispose();
249
+ this.#tabla = [];
250
+ }
251
+ /** Snap each loop to a whole number of ITS OWN beats now the buffer length is
252
+ * known, so a mistrimmed file phase-locks instead of drifting. */
253
+ #quantizeTablaLoops() {
254
+ for (const { loop, fileSpb } of this.#tabla) {
255
+ if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
256
+ }
257
+ }
240
258
  #restoreMasterGain() {
241
259
  if (this.#master !== void 0 && this.#tone !== void 0) {
242
260
  this.#master.gain.cancelScheduledValues(this.#tone.now());
@@ -322,10 +340,10 @@ var SwaralipiPlayer = class {
322
340
  this.#glide.clear();
323
341
  this.#tanpura?.dispose();
324
342
  this.#metronome?.dispose();
325
- this.#tabla?.dispose();
343
+ this.#disposeTabla();
326
344
  this.#drums?.dispose();
327
345
  this.#master?.dispose();
328
- this.#tanpura = this.#metronome = this.#tabla = this.#drums = this.#master = void 0;
346
+ this.#tanpura = this.#metronome = this.#drums = this.#master = void 0;
329
347
  }
330
348
  /* ----------------------------------------------------------------- */
331
349
  /* Internals */
@@ -380,6 +398,13 @@ var SwaralipiPlayer = class {
380
398
  if (def.local === true && !this.#opts.soundsBaseUrl) return this.#fallbackInstrument();
381
399
  return id;
382
400
  }
401
+ #scheduleOpts() {
402
+ return {
403
+ tonic: this.#opts.tonic,
404
+ baseOctave: this.#opts.baseOctave,
405
+ laySpeeds: this.#opts.laySpeeds
406
+ };
407
+ }
383
408
  /** Effective tempo: snapped to the taal's available tabla tempos while a
384
409
  * tabla actually plays (so it never snaps to a bpm that taal lacks). */
385
410
  #effectiveBpm() {
@@ -417,23 +442,18 @@ var SwaralipiPlayer = class {
417
442
  const bpm = this.#effectiveBpm();
418
443
  transport.bpm.value = bpm;
419
444
  const spb = 60 / bpm;
420
- const schedule = buildSchedule(this.#doc, {
421
- tonic: this.#opts.tonic,
422
- baseOctave: this.#opts.baseOctave
423
- });
445
+ const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
424
446
  const range = midiRange(schedule);
425
447
  const id = this.#effectiveInstrument();
426
448
  const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
427
449
  const melody = this.#buildMelody(Tone, id, base, range);
428
450
  const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
429
- this.#buildRhythm(Tone, transport, base, spb);
430
- this.#buildTanpura(Tone, transport, base, spb);
451
+ const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
452
+ this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift);
453
+ this.#buildTanpura(Tone, transport, base, spb, schedule.tempo, shift);
431
454
  await loadedOrWarn(Tone);
432
- if (this.#tabla?.buffer?.loaded) {
433
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
434
- }
455
+ this.#quantizeTablaLoops();
435
456
  const draw = Tone.getDraw();
436
- const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
437
457
  const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
438
458
  schedule.events.forEach((ev, i) => {
439
459
  const at = ev.beat * spb + shift;
@@ -496,11 +516,9 @@ var SwaralipiPlayer = class {
496
516
  const bpm = this.#effectiveBpm();
497
517
  transport.bpm.value = bpm;
498
518
  const spb = 60 / bpm;
499
- this.#buildRhythm(Tone, transport, base, spb);
519
+ this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
500
520
  await loadedOrWarn(Tone);
501
- if (this.#tabla?.buffer?.loaded) {
502
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
503
- }
521
+ this.#quantizeTablaLoops();
504
522
  const draw = Tone.getDraw();
505
523
  const n = Math.max(1, this.#taal.numBeats);
506
524
  let beat = 0;
@@ -542,52 +560,92 @@ var SwaralipiPlayer = class {
542
560
  this.#glide.set(id, entry);
543
561
  return entry;
544
562
  }
545
- #buildRhythm(Tone, transport, base, spb) {
546
- this.#tabla?.dispose();
547
- this.#tabla = void 0;
563
+ /**
564
+ * Accompaniment, one লয় segment at a time. A tempo change under the melody
565
+ * is a real tempo change under the drum: the tabla loop is rebuilt at the
566
+ * segment's own bpm — its own recorded file when the set has one at that
567
+ * tempo, else the nearest file time-stretched by `GrainPlayer` (which
568
+ * stretches *without* transposing, unlike a plain playbackRate) — and the
569
+ * click / drum / drone patterns re-space their interval. A document with no
570
+ * লয় marks has exactly one segment, so this is the old single-loop path.
571
+ */
572
+ #buildRhythm(Tone, transport, base, spb, segments, shift) {
573
+ this.#disposeTabla();
548
574
  const master = this.#ensureMaster(Tone);
549
575
  const taalId = this.#taalId();
550
576
  const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
551
- if (wantsTabla) {
552
- const url = tablaUrl(base, taalId, nearestTablaBpm(this.bpm, tablaBpms(taalId)), this.#variation());
553
- const shift = tablaShiftSemitones(this.#tonicSemitone());
554
- this.#tabla = (shift === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
555
- url,
556
- loop: true,
557
- detune: shift * 100,
558
- grainSize: TABLA_GRAIN_SEC,
559
- overlap: TABLA_GRAIN_OVERLAP_SEC
560
- })).connect(master);
561
- this.#tabla.sync().start(0);
562
- return;
563
- }
564
- if (this.#opts.rhythm === "drums") {
565
- this.#drums ??= createDrumKit(Tone).connect(master);
566
- const drums = this.#drums;
567
- const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
568
- const pattern = drumPattern(taal);
569
- let beat2 = 0;
570
- const id2 = transport.scheduleRepeat((time) => {
571
- const voice = pattern[beat2 % pattern.length];
572
- if (drums.has(voice)) drums.player(voice).start(time);
573
- beat2 += 1;
574
- }, spb, 0);
575
- this.#scheduleIds.push(id2);
576
- return;
577
- }
578
- if (this.#opts.rhythm === "none" || base === void 0) return;
579
- this.#metronome ??= createMetronome(Tone, base).connect(master);
580
- const metro = this.#metronome;
577
+ if (!wantsTabla && this.#opts.rhythm !== "drums" && (this.#opts.rhythm === "none" || base === void 0)) return;
578
+ const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
579
+ const pattern = this.#opts.rhythm === "drums" ? drumPattern(taal) : [];
581
580
  const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
582
- let beat = 0;
583
- const id = transport.scheduleRepeat((time) => {
584
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
585
- metro.start(time);
586
- beat += 1;
587
- }, spb, 0);
588
- this.#scheduleIds.push(id);
581
+ segments.forEach((seg, i) => {
582
+ const from = i === 0 ? 0 : seg.beat * spb + shift;
583
+ const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
584
+ const beatSec = spb / seg.scale;
585
+ const span = to === Infinity ? void 0 : to - from;
586
+ if (wantsTabla) {
587
+ const bpm = 60 / beatSec;
588
+ const fileBpm = nearestTablaBpm(bpm, tablaBpms(taalId));
589
+ const rate = bpm / fileBpm;
590
+ const semis = tablaShiftSemitones(this.#tonicSemitone());
591
+ const url = tablaUrl(base, taalId, fileBpm, this.#variation());
592
+ const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
593
+ url,
594
+ loop: true,
595
+ playbackRate: rate,
596
+ detune: semis * 100,
597
+ grainSize: TABLA_GRAIN_SEC,
598
+ overlap: TABLA_GRAIN_OVERLAP_SEC
599
+ })).connect(master);
600
+ loop.sync().start(from);
601
+ if (span !== void 0) {
602
+ this.#scheduleIds.push(
603
+ transport.scheduleOnce((t) => {
604
+ loop.volume.setValueAtTime(0, t);
605
+ loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC);
606
+ }, to)
607
+ );
608
+ loop.stop(to + LAY_TAIL_SEC);
609
+ }
610
+ this.#tabla.push({ loop, fileSpb: 60 / fileBpm });
611
+ return;
612
+ }
613
+ if (this.#opts.rhythm === "drums") {
614
+ this.#drums ??= createDrumKit(Tone).connect(master);
615
+ const drums = this.#drums;
616
+ let beat2 = 0;
617
+ this.#scheduleIds.push(
618
+ transport.scheduleRepeat(
619
+ (time) => {
620
+ const voice = pattern[beat2 % pattern.length];
621
+ if (drums.has(voice)) drums.player(voice).start(time);
622
+ beat2 += 1;
623
+ },
624
+ beatSec,
625
+ from,
626
+ span
627
+ )
628
+ );
629
+ return;
630
+ }
631
+ this.#metronome ??= createMetronome(Tone, base).connect(master);
632
+ const metro = this.#metronome;
633
+ let beat = 0;
634
+ this.#scheduleIds.push(
635
+ transport.scheduleRepeat(
636
+ (time) => {
637
+ metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
638
+ metro.start(time);
639
+ beat += 1;
640
+ },
641
+ beatSec,
642
+ from,
643
+ span
644
+ )
645
+ );
646
+ });
589
647
  }
590
- #buildTanpura(Tone, transport, base, spb) {
648
+ #buildTanpura(Tone, transport, base, spb, segments, shift) {
591
649
  if (!this.#opts.tanpura || base === void 0) return;
592
650
  const tonicPc = (this.#tonicSemitone() % 12 + 12) % 12;
593
651
  const sa = TANPURA_PCS[tonicPc];
@@ -601,13 +659,23 @@ var SwaralipiPlayer = class {
601
659
  const players = this.#tanpura;
602
660
  players.volume.value = -14;
603
661
  const pattern = [pa, sa, sa, sa];
604
- let i = 0;
605
- const id = transport.scheduleRepeat((time) => {
606
- const stem = pattern[i % pattern.length];
607
- if (players.has(stem)) players.player(stem).start(time);
608
- i += 1;
609
- }, spb * 2, 0);
610
- this.#scheduleIds.push(id);
662
+ segments.forEach((seg, k) => {
663
+ const from = k === 0 ? 0 : seg.beat * spb + shift;
664
+ const to = k === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
665
+ let i = 0;
666
+ this.#scheduleIds.push(
667
+ transport.scheduleRepeat(
668
+ (time) => {
669
+ const stem = pattern[i % pattern.length];
670
+ if (players.has(stem)) players.player(stem).start(time);
671
+ i += 1;
672
+ },
673
+ spb * 2 / seg.scale,
674
+ from,
675
+ to === Infinity ? void 0 : to - from
676
+ )
677
+ );
678
+ });
611
679
  }
612
680
  #tonicSemitone() {
613
681
  return parseTonic(this.#opts.tonic ?? this.#doc?.meta.tonic);
@@ -615,6 +683,7 @@ var SwaralipiPlayer = class {
615
683
  };
616
684
 
617
685
  // src/player/export.ts
686
+ var LAY_TAIL_SEC2 = 0.12;
618
687
  function midiRange2(schedule) {
619
688
  let lo = Infinity;
620
689
  let hi = -Infinity;
@@ -652,7 +721,11 @@ async function exportAudio(doc, options = {}) {
652
721
  const rhythm = options.rhythm ?? "none";
653
722
  const withMelody = options.melody ?? true;
654
723
  const withTanpura = options.tanpura ?? false;
655
- const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
724
+ const schedule = buildSchedule(doc, {
725
+ tonic: options.tonic,
726
+ baseOctave: options.baseOctave,
727
+ laySpeeds: options.laySpeeds
728
+ });
656
729
  const range = midiRange2(schedule);
657
730
  const taalId = resolveTaal(doc, options.taal);
658
731
  const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
@@ -705,39 +778,67 @@ async function exportAudio(doc, options = {}) {
705
778
  }
706
779
  });
707
780
  }
708
- let tabla;
709
- if (rhythm === "drums" && base) {
710
- const drums = createDrumKit(Tone).connect(master);
781
+ const tablas = [];
782
+ if (rhythm !== "none" && base) {
711
783
  const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
712
- const pattern = drumPattern(taal);
713
- let beat = 0;
714
- transport.scheduleRepeat((time) => {
715
- const voice = pattern[beat % pattern.length];
716
- if (drums.has(voice)) drums.player(voice).start(time);
717
- beat += 1;
718
- }, spb, 0);
719
- } else if (rhythm !== "none" && base) {
720
- if (usesTabla && taalId) {
721
- const url = tablaUrl(base, taalId, bpm, variation);
722
- const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
723
- tabla = (semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
724
- url,
725
- loop: true,
726
- detune: semis * 100,
727
- grainSize: TABLA_GRAIN_SEC,
728
- overlap: TABLA_GRAIN_OVERLAP_SEC
729
- })).connect(master);
730
- tabla.sync().start(0);
731
- } else {
732
- const metro = createMetronome(Tone, base).connect(master);
733
- const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
734
- let beat = 0;
735
- transport.scheduleRepeat((time) => {
736
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
737
- metro.start(time);
738
- beat += 1;
739
- }, spb, 0);
740
- }
784
+ const pattern = rhythm === "drums" ? drumPattern(taal) : [];
785
+ const drums = rhythm === "drums" ? createDrumKit(Tone).connect(master) : void 0;
786
+ const metro = rhythm !== "drums" && !usesTabla ? createMetronome(Tone, base).connect(master) : void 0;
787
+ const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
788
+ const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
789
+ schedule.tempo.forEach((seg, i) => {
790
+ const from = i === 0 ? 0 : seg.beat * spb + shift;
791
+ const to = i === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
792
+ const beatSec = spb / seg.scale;
793
+ const span = to === Infinity ? void 0 : to - from;
794
+ if (usesTabla && taalId) {
795
+ const segBpm = 60 / beatSec;
796
+ const fileBpm = nearestTablaBpm(segBpm, tablaBpms(taalId));
797
+ const rate = segBpm / fileBpm;
798
+ const url = tablaUrl(base, taalId, fileBpm, variation);
799
+ const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
800
+ url,
801
+ loop: true,
802
+ playbackRate: rate,
803
+ detune: semis * 100,
804
+ grainSize: TABLA_GRAIN_SEC,
805
+ overlap: TABLA_GRAIN_OVERLAP_SEC
806
+ })).connect(master);
807
+ loop.sync().start(from);
808
+ if (span !== void 0) {
809
+ transport.scheduleOnce((t) => {
810
+ loop.volume.setValueAtTime(0, t);
811
+ loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC2);
812
+ }, to);
813
+ loop.stop(to + LAY_TAIL_SEC2);
814
+ }
815
+ tablas.push({ loop, fileSpb: 60 / fileBpm });
816
+ } else if (drums !== void 0) {
817
+ let beat = 0;
818
+ transport.scheduleRepeat(
819
+ (time) => {
820
+ const voice = pattern[beat % pattern.length];
821
+ if (drums.has(voice)) drums.player(voice).start(time);
822
+ beat += 1;
823
+ },
824
+ beatSec,
825
+ from,
826
+ span
827
+ );
828
+ } else if (metro !== void 0) {
829
+ let beat = 0;
830
+ transport.scheduleRepeat(
831
+ (time) => {
832
+ metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
833
+ metro.start(time);
834
+ beat += 1;
835
+ },
836
+ beatSec,
837
+ from,
838
+ span
839
+ );
840
+ }
841
+ });
741
842
  }
742
843
  if (withTanpura && base) {
743
844
  const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
@@ -746,15 +847,26 @@ async function exportAudio(doc, options = {}) {
746
847
  const players = createTanpura(Tone, base, [sa, pa]).connect(master);
747
848
  players.volume.value = -14;
748
849
  const pattern = [pa, sa, sa, sa];
749
- let i = 0;
750
- transport.scheduleRepeat((time) => {
751
- const stem = pattern[i % pattern.length];
752
- if (players.has(stem)) players.player(stem).start(time);
753
- i += 1;
754
- }, spb * 2, 0);
850
+ schedule.tempo.forEach((seg, k) => {
851
+ const from = k === 0 ? 0 : seg.beat * spb + shift;
852
+ const to = k === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
853
+ let i = 0;
854
+ transport.scheduleRepeat(
855
+ (time) => {
856
+ const stem = pattern[i % pattern.length];
857
+ if (players.has(stem)) players.player(stem).start(time);
858
+ i += 1;
859
+ },
860
+ spb * 2 / seg.scale,
861
+ from,
862
+ to === Infinity ? void 0 : to - from
863
+ );
864
+ });
755
865
  }
756
866
  await Tone.loaded();
757
- if (tabla?.buffer?.loaded) tabla.loopEnd = quantizeLoopEnd(tabla.buffer.duration, spb);
867
+ for (const { loop, fileSpb } of tablas) {
868
+ if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
869
+ }
758
870
  if (fadeOutSec > 0) {
759
871
  master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
760
872
  master.gain.linearRampToValueAtTime(0, durationSec);
@@ -975,7 +1087,11 @@ function safeName2(title) {
975
1087
  return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
976
1088
  }
977
1089
  function exportMidi(doc, options = {}) {
978
- const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
1090
+ const schedule = buildSchedule(doc, {
1091
+ tonic: options.tonic,
1092
+ baseOctave: options.baseOctave,
1093
+ laySpeeds: options.laySpeeds
1094
+ });
979
1095
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
980
1096
  const taalId = doc.meta.taal?.id;
981
1097
  const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };