@ugfoundation/swaralipi-js 0.7.7 → 0.8.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 (50) hide show
  1. package/dist/{batch-6MCPHHKF.js → batch-NDL6CW7Q.js} +5 -5
  2. package/dist/{batch-6MCPHHKF.js.map → batch-NDL6CW7Q.js.map} +1 -1
  3. package/dist/{chunk-QM4CXKW2.js → chunk-4HNT6OAT.js} +24 -3
  4. package/dist/chunk-4HNT6OAT.js.map +1 -0
  5. package/dist/{chunk-TKTQCUKD.js → chunk-4IVUPMRK.js} +27 -9
  6. package/dist/chunk-4IVUPMRK.js.map +1 -0
  7. package/dist/{chunk-ALN4SWMS.js → chunk-LPCKL3AM.js} +15 -4
  8. package/dist/chunk-LPCKL3AM.js.map +1 -0
  9. package/dist/{chunk-QQC6WVLJ.js → chunk-Z42M5JAA.js} +4 -4
  10. package/dist/{chunk-QQC6WVLJ.js.map → chunk-Z42M5JAA.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 +39 -1
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +2 -2
  22. package/dist/index.d.ts +2 -2
  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 +276 -117
  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.cts → schema-DGA_bNVZ.d.cts} +97 -1
  35. package/dist/{schema-DqBXPGkL.d.ts → schema-DGA_bNVZ.d.ts} +97 -1
  36. package/dist/spec/index.cjs +39 -1
  37. package/dist/spec/index.cjs.map +1 -1
  38. package/dist/spec/index.d.cts +16 -4
  39. package/dist/spec/index.d.ts +16 -4
  40. package/dist/spec/index.js +2 -2
  41. package/dist/viewer/index.cjs +146 -23
  42. package/dist/viewer/index.cjs.map +1 -1
  43. package/dist/viewer/index.d.cts +10 -5
  44. package/dist/viewer/index.d.ts +10 -5
  45. package/dist/viewer/index.js +75 -18
  46. package/dist/viewer/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-ALN4SWMS.js.map +0 -1
  49. package/dist/chunk-QM4CXKW2.js.map +0 -1
  50. package/dist/chunk-TKTQCUKD.js.map +0 -1
@@ -115,6 +115,27 @@ function sCurve(x, steepness = 8) {
115
115
  return (logistic(t) - lo) / (logistic(1) - lo);
116
116
  }
117
117
 
118
+ // src/spec/lay.ts
119
+ var LAYS = ["drut", "madhya", "vilambit"];
120
+ var LAY_SPEEDS = {
121
+ drut: 1,
122
+ madhya: 0.75,
123
+ vilambit: 0.5
124
+ };
125
+ function resolveLaySpeeds(doc, opt) {
126
+ return { ...LAY_SPEEDS, ...doc, ...opt };
127
+ }
128
+ var PRINTED = { \u09A6\u09CD\u09B0\u09C1\u09A4: "drut", \u09AE\u09A7\u09CD\u09AF: "madhya", \u09AC\u09BF\u09B2\u09AE\u09CD\u09AC\u09BF\u09A4: "vilambit" };
129
+ function printedLay(notes) {
130
+ const first = notes[0];
131
+ if (first === void 0 || first.lyric === void 0) return void 0;
132
+ for (const k of Object.keys(first)) if (k !== "lyric") return void 0;
133
+ return PRINTED[first.lyric.normalize("NFC")];
134
+ }
135
+ function rowLay(row) {
136
+ return row.lay ?? printedLay(row.notes);
137
+ }
138
+
118
139
  // src/player/schedule.ts
119
140
  var UNDECODED_CELL = /[srgmpqnvtduka]/i;
120
141
  var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
@@ -159,22 +180,38 @@ function buildSchedule(doc, opts = {}) {
159
180
  const events = [];
160
181
  let cursor = 0;
161
182
  let lastPitched = -1;
162
- let samBeat;
183
+ let samMatra;
184
+ let matra = 0;
185
+ const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
186
+ const withLay = opts.lay !== false;
187
+ const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut" }];
188
+ let scale = 1;
163
189
  doc.parts.forEach((part, p) => {
164
190
  part.rows.forEach((row, r) => {
191
+ const lay = withLay ? rowLay(row) : void 0;
192
+ if (lay !== void 0 && speeds[lay] !== scale) {
193
+ scale = speeds[lay];
194
+ const last = tempo[tempo.length - 1];
195
+ if (last.beat === cursor) tempo[tempo.length - 1] = { beat: cursor, endBeat: cursor, scale, lay };
196
+ else {
197
+ last.endBeat = cursor;
198
+ tempo.push({ beat: cursor, endBeat: cursor, scale, lay });
199
+ }
200
+ }
165
201
  const glides = meendContinuations(row.notes);
166
202
  row.notes.forEach((note, i) => {
167
- if (samBeat === void 0 && note.above !== void 0) {
203
+ if (samMatra === void 0 && note.above !== void 0) {
168
204
  for (const a of note.above) {
169
205
  if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
170
- samBeat = cursor;
206
+ samMatra = matra;
171
207
  break;
172
208
  }
173
209
  }
174
210
  }
175
211
  if (note.bar !== void 0 || isSilent(note)) return;
176
212
  const path = `${p}.${r}.${i}`;
177
- const dur = note.durationMatra ?? 1;
213
+ const written = note.durationMatra ?? 1;
214
+ const dur = written / scale;
178
215
  if (note.svara !== void 0) {
179
216
  events.push({
180
217
  beat: cursor,
@@ -193,12 +230,14 @@ function buildSchedule(doc, opts = {}) {
193
230
  events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
194
231
  }
195
232
  cursor += dur;
233
+ matra += written;
196
234
  });
197
235
  });
198
236
  });
199
237
  const numBeats = doc.meta.taal?.numBeats;
200
- const pickupBeats = samBeat !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samBeat % numBeats) % numBeats : 0;
201
- return { events, totalBeats: cursor, pickupBeats };
238
+ const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
239
+ tempo[tempo.length - 1].endBeat = cursor;
240
+ return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
202
241
  }
203
242
 
204
243
  // src/player/instruments.ts
@@ -497,6 +536,10 @@ function createTanpura(Tone, base, stems = TANPURA_PCS) {
497
536
 
498
537
  // src/player/player.ts
499
538
  var STOP_FADE_SEC = 0.5;
539
+ var LAY_TAIL_SEC = 0.12;
540
+ var TAAL_MODE_TEMPO = [
541
+ { beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
542
+ ];
500
543
  var MEDIA_LEAD_SEC = 0.25;
501
544
  function midiRange(schedule) {
502
545
  let lo = Infinity;
@@ -541,8 +584,11 @@ var SwaralipiPlayer = class {
541
584
  #tanpura;
542
585
  #tanpuraKey = "";
543
586
  #metronome;
544
- /** The looping tabla — a `GrainPlayer` when the tonic needs it retuned. */
545
- #tabla;
587
+ /** The looping tabla — one loop per লয় segment, each a `GrainPlayer` when
588
+ * the tonic needs retuning or the segment's tempo needs stretching.
589
+ * `fileSpb` is the recorded loop's OWN beat length, which is the unit
590
+ * `quantizeLoopEnd` must snap in (loop points are buffer time). */
591
+ #tabla = [];
546
592
  #drums;
547
593
  #scheduleIds = [];
548
594
  #fadeTimer;
@@ -553,6 +599,7 @@ var SwaralipiPlayer = class {
553
599
  customInstruments: options.customInstruments,
554
600
  tonic: options.tonic,
555
601
  bpm: options.bpm,
602
+ laySpeeds: options.laySpeeds,
556
603
  baseOctave: options.baseOctave ?? 4,
557
604
  instrument: options.instrument ?? "piano",
558
605
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
@@ -606,8 +653,7 @@ var SwaralipiPlayer = class {
606
653
  get durationSec() {
607
654
  if (this.#taal !== void 0) return Infinity;
608
655
  if (this.#doc === void 0) return 0;
609
- const schedule = buildSchedule(this.#doc, { tonic: this.#opts.tonic, baseOctave: this.#opts.baseOctave });
610
- const full = playDurationSec(schedule, 60 / this.#effectiveBpm(), this.#opts.rhythm);
656
+ const full = playDurationSec(buildSchedule(this.#doc, this.#scheduleOpts()), 60 / this.#effectiveBpm(), this.#opts.rhythm);
611
657
  return Math.min(full, this.#opts.previewLimitSec);
612
658
  }
613
659
  /** Seconds played so far, off the Transport's own clock — so it holds while
@@ -711,8 +757,7 @@ var SwaralipiPlayer = class {
711
757
  transport.cancel(0);
712
758
  transport.position = 0;
713
759
  }
714
- this.#tabla?.dispose();
715
- this.#tabla = void 0;
760
+ this.#disposeTabla();
716
761
  this.#restoreMasterGain();
717
762
  this.#scheduleIds = [];
718
763
  this.#emitHighlight(null);
@@ -730,6 +775,17 @@ var SwaralipiPlayer = class {
730
775
  this.#cancelFadeTimer();
731
776
  this.#restoreMasterGain();
732
777
  }
778
+ #disposeTabla() {
779
+ for (const { loop } of this.#tabla) loop.dispose();
780
+ this.#tabla = [];
781
+ }
782
+ /** Snap each loop to a whole number of ITS OWN beats now the buffer length is
783
+ * known, so a mistrimmed file phase-locks instead of drifting. */
784
+ #quantizeTablaLoops() {
785
+ for (const { loop, fileSpb } of this.#tabla) {
786
+ if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
787
+ }
788
+ }
733
789
  #restoreMasterGain() {
734
790
  if (this.#master !== void 0 && this.#tone !== void 0) {
735
791
  this.#master.gain.cancelScheduledValues(this.#tone.now());
@@ -815,10 +871,10 @@ var SwaralipiPlayer = class {
815
871
  this.#glide.clear();
816
872
  this.#tanpura?.dispose();
817
873
  this.#metronome?.dispose();
818
- this.#tabla?.dispose();
874
+ this.#disposeTabla();
819
875
  this.#drums?.dispose();
820
876
  this.#master?.dispose();
821
- this.#tanpura = this.#metronome = this.#tabla = this.#drums = this.#master = void 0;
877
+ this.#tanpura = this.#metronome = this.#drums = this.#master = void 0;
822
878
  }
823
879
  /* ----------------------------------------------------------------- */
824
880
  /* Internals */
@@ -873,6 +929,13 @@ var SwaralipiPlayer = class {
873
929
  if (def.local === true && !this.#opts.soundsBaseUrl) return this.#fallbackInstrument();
874
930
  return id;
875
931
  }
932
+ #scheduleOpts() {
933
+ return {
934
+ tonic: this.#opts.tonic,
935
+ baseOctave: this.#opts.baseOctave,
936
+ laySpeeds: this.#opts.laySpeeds
937
+ };
938
+ }
876
939
  /** Effective tempo: snapped to the taal's available tabla tempos while a
877
940
  * tabla actually plays (so it never snaps to a bpm that taal lacks). */
878
941
  #effectiveBpm() {
@@ -910,23 +973,18 @@ var SwaralipiPlayer = class {
910
973
  const bpm = this.#effectiveBpm();
911
974
  transport.bpm.value = bpm;
912
975
  const spb = 60 / bpm;
913
- const schedule = buildSchedule(this.#doc, {
914
- tonic: this.#opts.tonic,
915
- baseOctave: this.#opts.baseOctave
916
- });
976
+ const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
917
977
  const range = midiRange(schedule);
918
978
  const id = this.#effectiveInstrument();
919
979
  const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
920
980
  const melody = this.#buildMelody(Tone, id, base, range);
921
981
  const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
922
- this.#buildRhythm(Tone, transport, base, spb);
923
- this.#buildTanpura(Tone, transport, base, spb);
982
+ const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
983
+ this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift);
984
+ this.#buildTanpura(Tone, transport, base, spb, schedule.tempo, shift);
924
985
  await loadedOrWarn(Tone);
925
- if (this.#tabla?.buffer?.loaded) {
926
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
927
- }
986
+ this.#quantizeTablaLoops();
928
987
  const draw = Tone.getDraw();
929
- const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
930
988
  const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
931
989
  schedule.events.forEach((ev, i) => {
932
990
  const at = ev.beat * spb + shift;
@@ -989,11 +1047,9 @@ var SwaralipiPlayer = class {
989
1047
  const bpm = this.#effectiveBpm();
990
1048
  transport.bpm.value = bpm;
991
1049
  const spb = 60 / bpm;
992
- this.#buildRhythm(Tone, transport, base, spb);
1050
+ this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
993
1051
  await loadedOrWarn(Tone);
994
- if (this.#tabla?.buffer?.loaded) {
995
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
996
- }
1052
+ this.#quantizeTablaLoops();
997
1053
  const draw = Tone.getDraw();
998
1054
  const n = Math.max(1, this.#taal.numBeats);
999
1055
  let beat = 0;
@@ -1035,52 +1091,92 @@ var SwaralipiPlayer = class {
1035
1091
  this.#glide.set(id, entry);
1036
1092
  return entry;
1037
1093
  }
1038
- #buildRhythm(Tone, transport, base, spb) {
1039
- this.#tabla?.dispose();
1040
- this.#tabla = void 0;
1094
+ /**
1095
+ * Accompaniment, one লয় segment at a time. A tempo change under the melody
1096
+ * is a real tempo change under the drum: the tabla loop is rebuilt at the
1097
+ * segment's own bpm — its own recorded file when the set has one at that
1098
+ * tempo, else the nearest file time-stretched by `GrainPlayer` (which
1099
+ * stretches *without* transposing, unlike a plain playbackRate) — and the
1100
+ * click / drum / drone patterns re-space their interval. A document with no
1101
+ * লয় marks has exactly one segment, so this is the old single-loop path.
1102
+ */
1103
+ #buildRhythm(Tone, transport, base, spb, segments, shift) {
1104
+ this.#disposeTabla();
1041
1105
  const master = this.#ensureMaster(Tone);
1042
1106
  const taalId = this.#taalId();
1043
1107
  const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
1044
- if (wantsTabla) {
1045
- const url = tablaUrl(base, taalId, nearestTablaBpm(this.bpm, tablaBpms(taalId)), this.#variation());
1046
- const shift = tablaShiftSemitones(this.#tonicSemitone());
1047
- this.#tabla = (shift === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1048
- url,
1049
- loop: true,
1050
- detune: shift * 100,
1051
- grainSize: TABLA_GRAIN_SEC,
1052
- overlap: TABLA_GRAIN_OVERLAP_SEC
1053
- })).connect(master);
1054
- this.#tabla.sync().start(0);
1055
- return;
1056
- }
1057
- if (this.#opts.rhythm === "drums") {
1058
- this.#drums ??= createDrumKit(Tone).connect(master);
1059
- const drums = this.#drums;
1060
- const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
1061
- const pattern = drumPattern(taal);
1062
- let beat2 = 0;
1063
- const id2 = transport.scheduleRepeat((time) => {
1064
- const voice = pattern[beat2 % pattern.length];
1065
- if (drums.has(voice)) drums.player(voice).start(time);
1066
- beat2 += 1;
1067
- }, spb, 0);
1068
- this.#scheduleIds.push(id2);
1069
- return;
1070
- }
1071
- if (this.#opts.rhythm === "none" || base === void 0) return;
1072
- this.#metronome ??= createMetronome(Tone, base).connect(master);
1073
- const metro = this.#metronome;
1108
+ if (!wantsTabla && this.#opts.rhythm !== "drums" && (this.#opts.rhythm === "none" || base === void 0)) return;
1109
+ const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
1110
+ const pattern = this.#opts.rhythm === "drums" ? drumPattern(taal) : [];
1074
1111
  const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
1075
- let beat = 0;
1076
- const id = transport.scheduleRepeat((time) => {
1077
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1078
- metro.start(time);
1079
- beat += 1;
1080
- }, spb, 0);
1081
- this.#scheduleIds.push(id);
1112
+ segments.forEach((seg, i) => {
1113
+ const from = i === 0 ? 0 : seg.beat * spb + shift;
1114
+ const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
1115
+ const beatSec = spb / seg.scale;
1116
+ const span = to === Infinity ? void 0 : to - from;
1117
+ if (wantsTabla) {
1118
+ const bpm = 60 / beatSec;
1119
+ const fileBpm = nearestTablaBpm(bpm, tablaBpms(taalId));
1120
+ const rate = bpm / fileBpm;
1121
+ const semis = tablaShiftSemitones(this.#tonicSemitone());
1122
+ const url = tablaUrl(base, taalId, fileBpm, this.#variation());
1123
+ const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1124
+ url,
1125
+ loop: true,
1126
+ playbackRate: rate,
1127
+ detune: semis * 100,
1128
+ grainSize: TABLA_GRAIN_SEC,
1129
+ overlap: TABLA_GRAIN_OVERLAP_SEC
1130
+ })).connect(master);
1131
+ loop.sync().start(from);
1132
+ if (span !== void 0) {
1133
+ this.#scheduleIds.push(
1134
+ transport.scheduleOnce((t) => {
1135
+ loop.volume.setValueAtTime(0, t);
1136
+ loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC);
1137
+ }, to)
1138
+ );
1139
+ loop.stop(to + LAY_TAIL_SEC);
1140
+ }
1141
+ this.#tabla.push({ loop, fileSpb: 60 / fileBpm });
1142
+ return;
1143
+ }
1144
+ if (this.#opts.rhythm === "drums") {
1145
+ this.#drums ??= createDrumKit(Tone).connect(master);
1146
+ const drums = this.#drums;
1147
+ let beat2 = 0;
1148
+ this.#scheduleIds.push(
1149
+ transport.scheduleRepeat(
1150
+ (time) => {
1151
+ const voice = pattern[beat2 % pattern.length];
1152
+ if (drums.has(voice)) drums.player(voice).start(time);
1153
+ beat2 += 1;
1154
+ },
1155
+ beatSec,
1156
+ from,
1157
+ span
1158
+ )
1159
+ );
1160
+ return;
1161
+ }
1162
+ this.#metronome ??= createMetronome(Tone, base).connect(master);
1163
+ const metro = this.#metronome;
1164
+ let beat = 0;
1165
+ this.#scheduleIds.push(
1166
+ transport.scheduleRepeat(
1167
+ (time) => {
1168
+ metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1169
+ metro.start(time);
1170
+ beat += 1;
1171
+ },
1172
+ beatSec,
1173
+ from,
1174
+ span
1175
+ )
1176
+ );
1177
+ });
1082
1178
  }
1083
- #buildTanpura(Tone, transport, base, spb) {
1179
+ #buildTanpura(Tone, transport, base, spb, segments, shift) {
1084
1180
  if (!this.#opts.tanpura || base === void 0) return;
1085
1181
  const tonicPc = (this.#tonicSemitone() % 12 + 12) % 12;
1086
1182
  const sa = TANPURA_PCS[tonicPc];
@@ -1094,13 +1190,23 @@ var SwaralipiPlayer = class {
1094
1190
  const players = this.#tanpura;
1095
1191
  players.volume.value = -14;
1096
1192
  const pattern = [pa, sa, sa, sa];
1097
- let i = 0;
1098
- const id = transport.scheduleRepeat((time) => {
1099
- const stem = pattern[i % pattern.length];
1100
- if (players.has(stem)) players.player(stem).start(time);
1101
- i += 1;
1102
- }, spb * 2, 0);
1103
- this.#scheduleIds.push(id);
1193
+ segments.forEach((seg, k) => {
1194
+ const from = k === 0 ? 0 : seg.beat * spb + shift;
1195
+ const to = k === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
1196
+ let i = 0;
1197
+ this.#scheduleIds.push(
1198
+ transport.scheduleRepeat(
1199
+ (time) => {
1200
+ const stem = pattern[i % pattern.length];
1201
+ if (players.has(stem)) players.player(stem).start(time);
1202
+ i += 1;
1203
+ },
1204
+ spb * 2 / seg.scale,
1205
+ from,
1206
+ to === Infinity ? void 0 : to - from
1207
+ )
1208
+ );
1209
+ });
1104
1210
  }
1105
1211
  #tonicSemitone() {
1106
1212
  return parseTonic(this.#opts.tonic ?? this.#doc?.meta.tonic);
@@ -1108,6 +1214,7 @@ var SwaralipiPlayer = class {
1108
1214
  };
1109
1215
 
1110
1216
  // src/player/export.ts
1217
+ var LAY_TAIL_SEC2 = 0.12;
1111
1218
  function midiRange2(schedule) {
1112
1219
  let lo = Infinity;
1113
1220
  let hi = -Infinity;
@@ -1145,7 +1252,11 @@ async function exportAudio(doc, options = {}) {
1145
1252
  const rhythm = options.rhythm ?? "none";
1146
1253
  const withMelody = options.melody ?? true;
1147
1254
  const withTanpura = options.tanpura ?? false;
1148
- const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
1255
+ const schedule = buildSchedule(doc, {
1256
+ tonic: options.tonic,
1257
+ baseOctave: options.baseOctave,
1258
+ laySpeeds: options.laySpeeds
1259
+ });
1149
1260
  const range = midiRange2(schedule);
1150
1261
  const taalId = resolveTaal(doc, options.taal);
1151
1262
  const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
@@ -1198,39 +1309,67 @@ async function exportAudio(doc, options = {}) {
1198
1309
  }
1199
1310
  });
1200
1311
  }
1201
- let tabla;
1202
- if (rhythm === "drums" && base) {
1203
- const drums = createDrumKit(Tone).connect(master);
1312
+ const tablas = [];
1313
+ if (rhythm !== "none" && base) {
1204
1314
  const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
1205
- const pattern = drumPattern(taal);
1206
- let beat = 0;
1207
- transport.scheduleRepeat((time) => {
1208
- const voice = pattern[beat % pattern.length];
1209
- if (drums.has(voice)) drums.player(voice).start(time);
1210
- beat += 1;
1211
- }, spb, 0);
1212
- } else if (rhythm !== "none" && base) {
1213
- if (usesTabla && taalId) {
1214
- const url = tablaUrl(base, taalId, bpm, variation);
1215
- const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
1216
- tabla = (semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1217
- url,
1218
- loop: true,
1219
- detune: semis * 100,
1220
- grainSize: TABLA_GRAIN_SEC,
1221
- overlap: TABLA_GRAIN_OVERLAP_SEC
1222
- })).connect(master);
1223
- tabla.sync().start(0);
1224
- } else {
1225
- const metro = createMetronome(Tone, base).connect(master);
1226
- const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
1227
- let beat = 0;
1228
- transport.scheduleRepeat((time) => {
1229
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1230
- metro.start(time);
1231
- beat += 1;
1232
- }, spb, 0);
1233
- }
1315
+ const pattern = rhythm === "drums" ? drumPattern(taal) : [];
1316
+ const drums = rhythm === "drums" ? createDrumKit(Tone).connect(master) : void 0;
1317
+ const metro = rhythm !== "drums" && !usesTabla ? createMetronome(Tone, base).connect(master) : void 0;
1318
+ const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
1319
+ const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
1320
+ schedule.tempo.forEach((seg, i) => {
1321
+ const from = i === 0 ? 0 : seg.beat * spb + shift;
1322
+ const to = i === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
1323
+ const beatSec = spb / seg.scale;
1324
+ const span = to === Infinity ? void 0 : to - from;
1325
+ if (usesTabla && taalId) {
1326
+ const segBpm = 60 / beatSec;
1327
+ const fileBpm = nearestTablaBpm(segBpm, tablaBpms(taalId));
1328
+ const rate = segBpm / fileBpm;
1329
+ const url = tablaUrl(base, taalId, fileBpm, variation);
1330
+ const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1331
+ url,
1332
+ loop: true,
1333
+ playbackRate: rate,
1334
+ detune: semis * 100,
1335
+ grainSize: TABLA_GRAIN_SEC,
1336
+ overlap: TABLA_GRAIN_OVERLAP_SEC
1337
+ })).connect(master);
1338
+ loop.sync().start(from);
1339
+ if (span !== void 0) {
1340
+ transport.scheduleOnce((t) => {
1341
+ loop.volume.setValueAtTime(0, t);
1342
+ loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC2);
1343
+ }, to);
1344
+ loop.stop(to + LAY_TAIL_SEC2);
1345
+ }
1346
+ tablas.push({ loop, fileSpb: 60 / fileBpm });
1347
+ } else if (drums !== void 0) {
1348
+ let beat = 0;
1349
+ transport.scheduleRepeat(
1350
+ (time) => {
1351
+ const voice = pattern[beat % pattern.length];
1352
+ if (drums.has(voice)) drums.player(voice).start(time);
1353
+ beat += 1;
1354
+ },
1355
+ beatSec,
1356
+ from,
1357
+ span
1358
+ );
1359
+ } else if (metro !== void 0) {
1360
+ let beat = 0;
1361
+ transport.scheduleRepeat(
1362
+ (time) => {
1363
+ metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1364
+ metro.start(time);
1365
+ beat += 1;
1366
+ },
1367
+ beatSec,
1368
+ from,
1369
+ span
1370
+ );
1371
+ }
1372
+ });
1234
1373
  }
1235
1374
  if (withTanpura && base) {
1236
1375
  const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
@@ -1239,15 +1378,26 @@ async function exportAudio(doc, options = {}) {
1239
1378
  const players = createTanpura(Tone, base, [sa, pa]).connect(master);
1240
1379
  players.volume.value = -14;
1241
1380
  const pattern = [pa, sa, sa, sa];
1242
- let i = 0;
1243
- transport.scheduleRepeat((time) => {
1244
- const stem = pattern[i % pattern.length];
1245
- if (players.has(stem)) players.player(stem).start(time);
1246
- i += 1;
1247
- }, spb * 2, 0);
1381
+ schedule.tempo.forEach((seg, k) => {
1382
+ const from = k === 0 ? 0 : seg.beat * spb + shift;
1383
+ const to = k === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
1384
+ let i = 0;
1385
+ transport.scheduleRepeat(
1386
+ (time) => {
1387
+ const stem = pattern[i % pattern.length];
1388
+ if (players.has(stem)) players.player(stem).start(time);
1389
+ i += 1;
1390
+ },
1391
+ spb * 2 / seg.scale,
1392
+ from,
1393
+ to === Infinity ? void 0 : to - from
1394
+ );
1395
+ });
1248
1396
  }
1249
1397
  await Tone.loaded();
1250
- if (tabla?.buffer?.loaded) tabla.loopEnd = quantizeLoopEnd(tabla.buffer.duration, spb);
1398
+ for (const { loop, fileSpb } of tablas) {
1399
+ if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
1400
+ }
1251
1401
  if (fadeOutSec > 0) {
1252
1402
  master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
1253
1403
  master.gain.linearRampToValueAtTime(0, durationSec);
@@ -1468,7 +1618,11 @@ function safeName2(title) {
1468
1618
  return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
1469
1619
  }
1470
1620
  function exportMidi(doc, options = {}) {
1471
- const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
1621
+ const schedule = buildSchedule(doc, {
1622
+ tonic: options.tonic,
1623
+ baseOctave: options.baseOctave,
1624
+ laySpeeds: options.laySpeeds
1625
+ });
1472
1626
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1473
1627
  const taalId = doc.meta.taal?.id;
1474
1628
  const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };
@@ -1485,6 +1639,8 @@ exports.ARC_RAW_RE = ARC_RAW_RE;
1485
1639
  exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
1486
1640
  exports.DRUM_KIT_BASE = DRUM_KIT_BASE;
1487
1641
  exports.INSTRUMENTS = INSTRUMENTS;
1642
+ exports.LAYS = LAYS;
1643
+ exports.LAY_SPEEDS = LAY_SPEEDS;
1488
1644
  exports.MEEND_HOLD = MEEND_HOLD;
1489
1645
  exports.OCTAVE_OFFSET = OCTAVE_OFFSET;
1490
1646
  exports.SALAMANDER_BASE = SALAMANDER_BASE;
@@ -1511,8 +1667,11 @@ exports.nearestSampleKey = nearestSampleKey;
1511
1667
  exports.nearestTablaBpm = nearestTablaBpm;
1512
1668
  exports.normalizeBase = normalizeBase;
1513
1669
  exports.parseTonic = parseTonic;
1670
+ exports.printedLay = printedLay;
1514
1671
  exports.probeUrl = probeUrl;
1515
1672
  exports.quantizeLoopEnd = quantizeLoopEnd;
1673
+ exports.resolveLaySpeeds = resolveLaySpeeds;
1674
+ exports.rowLay = rowLay;
1516
1675
  exports.sCurve = sCurve;
1517
1676
  exports.sampleFileName = sampleFileName;
1518
1677
  exports.santoorUrls = santoorUrls;