@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
@@ -29,6 +29,14 @@ function graceNotes(grace) {
29
29
  return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
30
30
  }
31
31
 
32
+ // src/spec/svara.ts
33
+ function hasKomal(letter) {
34
+ return letter === "r" || letter === "g" || letter === "d" || letter === "n";
35
+ }
36
+ function hasTivra(letter) {
37
+ return letter === "m";
38
+ }
39
+
32
40
  // src/player/pitch.ts
33
41
  var OCTAVE_OFFSET = {
34
42
  "ati-mandra": -2,
@@ -79,8 +87,8 @@ function svaraSemitone(note) {
79
87
  if (note.svara === void 0) return null;
80
88
  const [, letter] = note.svara;
81
89
  let semi = SHUDDHA[letter];
82
- if (note.komal === true || note.microtone !== void 0) semi -= 1;
83
- if (note.tivra === true) semi += 1;
90
+ if ((note.komal === true || note.microtone !== void 0) && hasKomal(letter)) semi -= 1;
91
+ if (note.tivra === true && hasTivra(letter)) semi += 1;
84
92
  return semi;
85
93
  }
86
94
  function svaraToMidi(note, tonicPc, baseOctave = 4) {
@@ -107,6 +115,27 @@ function sCurve(x, steepness = 8) {
107
115
  return (logistic(t) - lo) / (logistic(1) - lo);
108
116
  }
109
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
+
110
139
  // src/player/schedule.ts
111
140
  var UNDECODED_CELL = /[srgmpqnvtduka]/i;
112
141
  var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
@@ -151,22 +180,38 @@ function buildSchedule(doc, opts = {}) {
151
180
  const events = [];
152
181
  let cursor = 0;
153
182
  let lastPitched = -1;
154
- 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;
155
189
  doc.parts.forEach((part, p) => {
156
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
+ }
157
201
  const glides = meendContinuations(row.notes);
158
202
  row.notes.forEach((note, i) => {
159
- if (samBeat === void 0 && note.above !== void 0) {
203
+ if (samMatra === void 0 && note.above !== void 0) {
160
204
  for (const a of note.above) {
161
205
  if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
162
- samBeat = cursor;
206
+ samMatra = matra;
163
207
  break;
164
208
  }
165
209
  }
166
210
  }
167
211
  if (note.bar !== void 0 || isSilent(note)) return;
168
212
  const path = `${p}.${r}.${i}`;
169
- const dur = note.durationMatra ?? 1;
213
+ const written = note.durationMatra ?? 1;
214
+ const dur = written / scale;
170
215
  if (note.svara !== void 0) {
171
216
  events.push({
172
217
  beat: cursor,
@@ -185,12 +230,14 @@ function buildSchedule(doc, opts = {}) {
185
230
  events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
186
231
  }
187
232
  cursor += dur;
233
+ matra += written;
188
234
  });
189
235
  });
190
236
  });
191
237
  const numBeats = doc.meta.taal?.numBeats;
192
- const pickupBeats = samBeat !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samBeat % numBeats) % numBeats : 0;
193
- 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 };
194
241
  }
195
242
 
196
243
  // src/player/instruments.ts
@@ -489,6 +536,10 @@ function createTanpura(Tone, base, stems = TANPURA_PCS) {
489
536
 
490
537
  // src/player/player.ts
491
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
+ ];
492
543
  var MEDIA_LEAD_SEC = 0.25;
493
544
  function midiRange(schedule) {
494
545
  let lo = Infinity;
@@ -533,8 +584,11 @@ var SwaralipiPlayer = class {
533
584
  #tanpura;
534
585
  #tanpuraKey = "";
535
586
  #metronome;
536
- /** The looping tabla — a `GrainPlayer` when the tonic needs it retuned. */
537
- #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 = [];
538
592
  #drums;
539
593
  #scheduleIds = [];
540
594
  #fadeTimer;
@@ -545,6 +599,7 @@ var SwaralipiPlayer = class {
545
599
  customInstruments: options.customInstruments,
546
600
  tonic: options.tonic,
547
601
  bpm: options.bpm,
602
+ laySpeeds: options.laySpeeds,
548
603
  baseOctave: options.baseOctave ?? 4,
549
604
  instrument: options.instrument ?? "piano",
550
605
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
@@ -598,8 +653,7 @@ var SwaralipiPlayer = class {
598
653
  get durationSec() {
599
654
  if (this.#taal !== void 0) return Infinity;
600
655
  if (this.#doc === void 0) return 0;
601
- const schedule = buildSchedule(this.#doc, { tonic: this.#opts.tonic, baseOctave: this.#opts.baseOctave });
602
- 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);
603
657
  return Math.min(full, this.#opts.previewLimitSec);
604
658
  }
605
659
  /** Seconds played so far, off the Transport's own clock — so it holds while
@@ -703,8 +757,7 @@ var SwaralipiPlayer = class {
703
757
  transport.cancel(0);
704
758
  transport.position = 0;
705
759
  }
706
- this.#tabla?.dispose();
707
- this.#tabla = void 0;
760
+ this.#disposeTabla();
708
761
  this.#restoreMasterGain();
709
762
  this.#scheduleIds = [];
710
763
  this.#emitHighlight(null);
@@ -722,6 +775,17 @@ var SwaralipiPlayer = class {
722
775
  this.#cancelFadeTimer();
723
776
  this.#restoreMasterGain();
724
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
+ }
725
789
  #restoreMasterGain() {
726
790
  if (this.#master !== void 0 && this.#tone !== void 0) {
727
791
  this.#master.gain.cancelScheduledValues(this.#tone.now());
@@ -807,10 +871,10 @@ var SwaralipiPlayer = class {
807
871
  this.#glide.clear();
808
872
  this.#tanpura?.dispose();
809
873
  this.#metronome?.dispose();
810
- this.#tabla?.dispose();
874
+ this.#disposeTabla();
811
875
  this.#drums?.dispose();
812
876
  this.#master?.dispose();
813
- this.#tanpura = this.#metronome = this.#tabla = this.#drums = this.#master = void 0;
877
+ this.#tanpura = this.#metronome = this.#drums = this.#master = void 0;
814
878
  }
815
879
  /* ----------------------------------------------------------------- */
816
880
  /* Internals */
@@ -865,6 +929,13 @@ var SwaralipiPlayer = class {
865
929
  if (def.local === true && !this.#opts.soundsBaseUrl) return this.#fallbackInstrument();
866
930
  return id;
867
931
  }
932
+ #scheduleOpts() {
933
+ return {
934
+ tonic: this.#opts.tonic,
935
+ baseOctave: this.#opts.baseOctave,
936
+ laySpeeds: this.#opts.laySpeeds
937
+ };
938
+ }
868
939
  /** Effective tempo: snapped to the taal's available tabla tempos while a
869
940
  * tabla actually plays (so it never snaps to a bpm that taal lacks). */
870
941
  #effectiveBpm() {
@@ -902,23 +973,18 @@ var SwaralipiPlayer = class {
902
973
  const bpm = this.#effectiveBpm();
903
974
  transport.bpm.value = bpm;
904
975
  const spb = 60 / bpm;
905
- const schedule = buildSchedule(this.#doc, {
906
- tonic: this.#opts.tonic,
907
- baseOctave: this.#opts.baseOctave
908
- });
976
+ const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
909
977
  const range = midiRange(schedule);
910
978
  const id = this.#effectiveInstrument();
911
979
  const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
912
980
  const melody = this.#buildMelody(Tone, id, base, range);
913
981
  const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
914
- this.#buildRhythm(Tone, transport, base, spb);
915
- 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);
916
985
  await loadedOrWarn(Tone);
917
- if (this.#tabla?.buffer?.loaded) {
918
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
919
- }
986
+ this.#quantizeTablaLoops();
920
987
  const draw = Tone.getDraw();
921
- const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
922
988
  const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
923
989
  schedule.events.forEach((ev, i) => {
924
990
  const at = ev.beat * spb + shift;
@@ -981,11 +1047,9 @@ var SwaralipiPlayer = class {
981
1047
  const bpm = this.#effectiveBpm();
982
1048
  transport.bpm.value = bpm;
983
1049
  const spb = 60 / bpm;
984
- this.#buildRhythm(Tone, transport, base, spb);
1050
+ this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
985
1051
  await loadedOrWarn(Tone);
986
- if (this.#tabla?.buffer?.loaded) {
987
- this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
988
- }
1052
+ this.#quantizeTablaLoops();
989
1053
  const draw = Tone.getDraw();
990
1054
  const n = Math.max(1, this.#taal.numBeats);
991
1055
  let beat = 0;
@@ -1027,52 +1091,92 @@ var SwaralipiPlayer = class {
1027
1091
  this.#glide.set(id, entry);
1028
1092
  return entry;
1029
1093
  }
1030
- #buildRhythm(Tone, transport, base, spb) {
1031
- this.#tabla?.dispose();
1032
- 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();
1033
1105
  const master = this.#ensureMaster(Tone);
1034
1106
  const taalId = this.#taalId();
1035
1107
  const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
1036
- if (wantsTabla) {
1037
- const url = tablaUrl(base, taalId, nearestTablaBpm(this.bpm, tablaBpms(taalId)), this.#variation());
1038
- const shift = tablaShiftSemitones(this.#tonicSemitone());
1039
- this.#tabla = (shift === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1040
- url,
1041
- loop: true,
1042
- detune: shift * 100,
1043
- grainSize: TABLA_GRAIN_SEC,
1044
- overlap: TABLA_GRAIN_OVERLAP_SEC
1045
- })).connect(master);
1046
- this.#tabla.sync().start(0);
1047
- return;
1048
- }
1049
- if (this.#opts.rhythm === "drums") {
1050
- this.#drums ??= createDrumKit(Tone).connect(master);
1051
- const drums = this.#drums;
1052
- const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
1053
- const pattern = drumPattern(taal);
1054
- let beat2 = 0;
1055
- const id2 = transport.scheduleRepeat((time) => {
1056
- const voice = pattern[beat2 % pattern.length];
1057
- if (drums.has(voice)) drums.player(voice).start(time);
1058
- beat2 += 1;
1059
- }, spb, 0);
1060
- this.#scheduleIds.push(id2);
1061
- return;
1062
- }
1063
- if (this.#opts.rhythm === "none" || base === void 0) return;
1064
- this.#metronome ??= createMetronome(Tone, base).connect(master);
1065
- 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) : [];
1066
1111
  const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
1067
- let beat = 0;
1068
- const id = transport.scheduleRepeat((time) => {
1069
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1070
- metro.start(time);
1071
- beat += 1;
1072
- }, spb, 0);
1073
- 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
+ });
1074
1178
  }
1075
- #buildTanpura(Tone, transport, base, spb) {
1179
+ #buildTanpura(Tone, transport, base, spb, segments, shift) {
1076
1180
  if (!this.#opts.tanpura || base === void 0) return;
1077
1181
  const tonicPc = (this.#tonicSemitone() % 12 + 12) % 12;
1078
1182
  const sa = TANPURA_PCS[tonicPc];
@@ -1086,13 +1190,23 @@ var SwaralipiPlayer = class {
1086
1190
  const players = this.#tanpura;
1087
1191
  players.volume.value = -14;
1088
1192
  const pattern = [pa, sa, sa, sa];
1089
- let i = 0;
1090
- const id = transport.scheduleRepeat((time) => {
1091
- const stem = pattern[i % pattern.length];
1092
- if (players.has(stem)) players.player(stem).start(time);
1093
- i += 1;
1094
- }, spb * 2, 0);
1095
- 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
+ });
1096
1210
  }
1097
1211
  #tonicSemitone() {
1098
1212
  return parseTonic(this.#opts.tonic ?? this.#doc?.meta.tonic);
@@ -1100,6 +1214,7 @@ var SwaralipiPlayer = class {
1100
1214
  };
1101
1215
 
1102
1216
  // src/player/export.ts
1217
+ var LAY_TAIL_SEC2 = 0.12;
1103
1218
  function midiRange2(schedule) {
1104
1219
  let lo = Infinity;
1105
1220
  let hi = -Infinity;
@@ -1137,7 +1252,11 @@ async function exportAudio(doc, options = {}) {
1137
1252
  const rhythm = options.rhythm ?? "none";
1138
1253
  const withMelody = options.melody ?? true;
1139
1254
  const withTanpura = options.tanpura ?? false;
1140
- 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
+ });
1141
1260
  const range = midiRange2(schedule);
1142
1261
  const taalId = resolveTaal(doc, options.taal);
1143
1262
  const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
@@ -1190,39 +1309,67 @@ async function exportAudio(doc, options = {}) {
1190
1309
  }
1191
1310
  });
1192
1311
  }
1193
- let tabla;
1194
- if (rhythm === "drums" && base) {
1195
- const drums = createDrumKit(Tone).connect(master);
1312
+ const tablas = [];
1313
+ if (rhythm !== "none" && base) {
1196
1314
  const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
1197
- const pattern = drumPattern(taal);
1198
- let beat = 0;
1199
- transport.scheduleRepeat((time) => {
1200
- const voice = pattern[beat % pattern.length];
1201
- if (drums.has(voice)) drums.player(voice).start(time);
1202
- beat += 1;
1203
- }, spb, 0);
1204
- } else if (rhythm !== "none" && base) {
1205
- if (usesTabla && taalId) {
1206
- const url = tablaUrl(base, taalId, bpm, variation);
1207
- const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
1208
- tabla = (semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1209
- url,
1210
- loop: true,
1211
- detune: semis * 100,
1212
- grainSize: TABLA_GRAIN_SEC,
1213
- overlap: TABLA_GRAIN_OVERLAP_SEC
1214
- })).connect(master);
1215
- tabla.sync().start(0);
1216
- } else {
1217
- const metro = createMetronome(Tone, base).connect(master);
1218
- const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
1219
- let beat = 0;
1220
- transport.scheduleRepeat((time) => {
1221
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1222
- metro.start(time);
1223
- beat += 1;
1224
- }, spb, 0);
1225
- }
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
+ });
1226
1373
  }
1227
1374
  if (withTanpura && base) {
1228
1375
  const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
@@ -1231,15 +1378,26 @@ async function exportAudio(doc, options = {}) {
1231
1378
  const players = createTanpura(Tone, base, [sa, pa]).connect(master);
1232
1379
  players.volume.value = -14;
1233
1380
  const pattern = [pa, sa, sa, sa];
1234
- let i = 0;
1235
- transport.scheduleRepeat((time) => {
1236
- const stem = pattern[i % pattern.length];
1237
- if (players.has(stem)) players.player(stem).start(time);
1238
- i += 1;
1239
- }, 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
+ });
1240
1396
  }
1241
1397
  await Tone.loaded();
1242
- 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
+ }
1243
1401
  if (fadeOutSec > 0) {
1244
1402
  master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
1245
1403
  master.gain.linearRampToValueAtTime(0, durationSec);
@@ -1460,7 +1618,11 @@ function safeName2(title) {
1460
1618
  return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
1461
1619
  }
1462
1620
  function exportMidi(doc, options = {}) {
1463
- 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
+ });
1464
1626
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1465
1627
  const taalId = doc.meta.taal?.id;
1466
1628
  const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };
@@ -1477,6 +1639,8 @@ exports.ARC_RAW_RE = ARC_RAW_RE;
1477
1639
  exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
1478
1640
  exports.DRUM_KIT_BASE = DRUM_KIT_BASE;
1479
1641
  exports.INSTRUMENTS = INSTRUMENTS;
1642
+ exports.LAYS = LAYS;
1643
+ exports.LAY_SPEEDS = LAY_SPEEDS;
1480
1644
  exports.MEEND_HOLD = MEEND_HOLD;
1481
1645
  exports.OCTAVE_OFFSET = OCTAVE_OFFSET;
1482
1646
  exports.SALAMANDER_BASE = SALAMANDER_BASE;
@@ -1503,8 +1667,11 @@ exports.nearestSampleKey = nearestSampleKey;
1503
1667
  exports.nearestTablaBpm = nearestTablaBpm;
1504
1668
  exports.normalizeBase = normalizeBase;
1505
1669
  exports.parseTonic = parseTonic;
1670
+ exports.printedLay = printedLay;
1506
1671
  exports.probeUrl = probeUrl;
1507
1672
  exports.quantizeLoopEnd = quantizeLoopEnd;
1673
+ exports.resolveLaySpeeds = resolveLaySpeeds;
1674
+ exports.rowLay = rowLay;
1508
1675
  exports.sCurve = sCurve;
1509
1676
  exports.sampleFileName = sampleFileName;
1510
1677
  exports.santoorUrls = santoorUrls;