@ugfoundation/swaralipi-js 0.8.2 → 0.9.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 (52) hide show
  1. package/README.md +1 -1
  2. package/dist/{batch-6FPBT5DP.js → batch-NEM7JXHV.js} +5 -5
  3. package/dist/{batch-6FPBT5DP.js.map → batch-NEM7JXHV.js.map} +1 -1
  4. package/dist/{chunk-UMBKIGVQ.js → chunk-DOEFSS66.js} +5 -4
  5. package/dist/chunk-DOEFSS66.js.map +1 -0
  6. package/dist/{chunk-4HNT6OAT.js → chunk-MYVIQU4O.js} +12 -3
  7. package/dist/chunk-MYVIQU4O.js.map +1 -0
  8. package/dist/{chunk-4IVUPMRK.js → chunk-SEN266BY.js} +28 -13
  9. package/dist/chunk-SEN266BY.js.map +1 -0
  10. package/dist/{chunk-FULC22MN.js → chunk-Z6A3CP5V.js} +28 -19
  11. package/dist/chunk-Z6A3CP5V.js.map +1 -0
  12. package/dist/cli.cjs +33 -19
  13. package/dist/cli.cjs.map +1 -1
  14. package/dist/cli.js +3 -3
  15. package/dist/convert/index.cjs +31 -17
  16. package/dist/convert/index.cjs.map +1 -1
  17. package/dist/convert/index.d.cts +1 -1
  18. package/dist/convert/index.d.ts +1 -1
  19. package/dist/convert/index.js +3 -3
  20. package/dist/index.cjs +38 -17
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +2 -2
  25. package/dist/{lay-Csa4CD46.d.cts → lay-CZIzR9cM.d.ts} +16 -2
  26. package/dist/{lay-Ct9ky_EE.d.ts → lay-CfDlPhH2.d.cts} +16 -2
  27. package/dist/player/index.cjs +116 -71
  28. package/dist/player/index.cjs.map +1 -1
  29. package/dist/player/index.d.cts +11 -9
  30. package/dist/player/index.d.ts +11 -9
  31. package/dist/player/index.js +89 -66
  32. package/dist/player/index.js.map +1 -1
  33. package/dist/{schedule-B_UaluBi.d.cts → schedule-BaovCtmf.d.ts} +31 -12
  34. package/dist/{schedule-CdILTNsk.d.ts → schedule-CU-VECDH.d.cts} +31 -12
  35. package/dist/{schema-DGA_bNVZ.d.ts → schema-Cf_epUIC.d.cts} +333 -63
  36. package/dist/{schema-DGA_bNVZ.d.cts → schema-Cf_epUIC.d.ts} +333 -63
  37. package/dist/spec/index.cjs +38 -17
  38. package/dist/spec/index.cjs.map +1 -1
  39. package/dist/spec/index.d.cts +26 -8
  40. package/dist/spec/index.d.ts +26 -8
  41. package/dist/spec/index.js +2 -2
  42. package/dist/viewer/index.cjs +156 -73
  43. package/dist/viewer/index.cjs.map +1 -1
  44. package/dist/viewer/index.d.cts +25 -7
  45. package/dist/viewer/index.d.ts +25 -7
  46. package/dist/viewer/index.js +78 -27
  47. package/dist/viewer/index.js.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/chunk-4HNT6OAT.js.map +0 -1
  50. package/dist/chunk-4IVUPMRK.js.map +0 -1
  51. package/dist/chunk-FULC22MN.js.map +0 -1
  52. package/dist/chunk-UMBKIGVQ.js.map +0 -1
@@ -22,6 +22,11 @@ var TAALS = {
22
22
  dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] }
23
23
  // ধামার
24
24
  };
25
+ function sameTaal(a, b) {
26
+ if (a === b) return true;
27
+ if (!a || !b) return false;
28
+ return a.id === b.id && a.numBeats === b.numBeats && a.variation === b.variation && String(a.bhaags) === String(b.bhaags);
29
+ }
25
30
 
26
31
  // src/spec/grace.ts
27
32
  function graceNotes(grace) {
@@ -173,6 +178,13 @@ function glideTargets(events, i) {
173
178
  }
174
179
  return out;
175
180
  }
181
+ var GRACE_STEP = 0.12;
182
+ function graceTouches(ev) {
183
+ return [
184
+ ...ev.kan.map((midi, g) => ({ midi, beat: ev.beat - GRACE_STEP * (ev.kan.length - g) })),
185
+ ...ev.resh.map((midi, g) => ({ midi, beat: ev.beat + GRACE_STEP * (g + 1) }))
186
+ ];
187
+ }
176
188
  var SAM_TICK = /^\d+[fF]$/;
177
189
  function buildSchedule(doc, opts = {}) {
178
190
  const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
@@ -184,18 +196,23 @@ function buildSchedule(doc, opts = {}) {
184
196
  let matra = 0;
185
197
  const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
186
198
  const withLay = opts.lay !== false;
187
- const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut" }];
199
+ const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
188
200
  let scale = 1;
201
+ let taal = doc.meta.taal;
189
202
  doc.parts.forEach((part, p) => {
190
203
  part.rows.forEach((row, r) => {
191
204
  const lay = withLay ? rowLay(row) : void 0;
192
- if (lay !== void 0 && speeds[lay] !== scale) {
193
- scale = speeds[lay];
205
+ const layChanged = lay !== void 0 && speeds[lay] !== scale;
206
+ const nextTaal = row.taal !== void 0 ? row.taal : taal;
207
+ if (layChanged || !sameTaal(nextTaal, taal)) {
208
+ if (layChanged) scale = speeds[lay];
209
+ taal = nextTaal;
194
210
  const last = tempo[tempo.length - 1];
195
- if (last.beat === cursor) tempo[tempo.length - 1] = { beat: cursor, endBeat: cursor, scale, lay };
211
+ const seg = { beat: cursor, endBeat: cursor, scale, lay: layChanged ? lay : last.lay, taal };
212
+ if (last.beat === cursor) tempo[tempo.length - 1] = seg;
196
213
  else {
197
214
  last.endBeat = cursor;
198
- tempo.push({ beat: cursor, endBeat: cursor, scale, lay });
215
+ tempo.push(seg);
199
216
  }
200
217
  }
201
218
  const glides = meendContinuations(row.notes);
@@ -212,12 +229,15 @@ function buildSchedule(doc, opts = {}) {
212
229
  const path = `${p}.${r}.${i}`;
213
230
  const written = note.durationMatra ?? 1;
214
231
  const dur = written / scale;
232
+ const kan = graceMidis(note.kan, tonicPc, baseOctave);
233
+ const resh = graceMidis(note.resh, tonicPc, baseOctave);
215
234
  if (note.svara !== void 0) {
216
235
  events.push({
217
236
  beat: cursor,
218
237
  durationBeats: dur,
219
238
  midi: svaraToMidi(note, tonicPc, baseOctave),
220
- kan: graceMidis(note.kan, tonicPc, baseOctave),
239
+ kan,
240
+ resh,
221
241
  path,
222
242
  kind: note.sustain === true ? "sustain" : "note",
223
243
  ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
@@ -225,16 +245,16 @@ function buildSchedule(doc, opts = {}) {
225
245
  lastPitched = events.length - 1;
226
246
  } else if (note.sustain === true) {
227
247
  if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
228
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "sustain" });
248
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
229
249
  } else {
230
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
250
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
231
251
  }
232
252
  cursor += dur;
233
253
  matra += written;
234
254
  });
235
255
  });
236
256
  });
237
- const numBeats = doc.meta.taal?.numBeats;
257
+ const numBeats = tempo[0].taal?.numBeats;
238
258
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
239
259
  tempo[tempo.length - 1].endBeat = cursor;
240
260
  return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
@@ -545,7 +565,7 @@ function midiRange(schedule) {
545
565
  let lo = Infinity;
546
566
  let hi = -Infinity;
547
567
  for (const ev of schedule.events) {
548
- for (const m of [ev.midi, ...ev.kan]) {
568
+ for (const m of [ev.midi, ...ev.kan, ...ev.resh]) {
549
569
  if (m !== null) {
550
570
  if (m < lo) lo = m;
551
571
  if (m > hi) hi = m;
@@ -888,16 +908,26 @@ var SwaralipiPlayer = class {
888
908
  #emitHighlight(path) {
889
909
  this.#opts.onHighlight?.(path);
890
910
  }
891
- /** The tabla variation to play: taal-mode taal's, the doc's own (auto mode),
892
- * else none — an explicit setTaal override plays the taal's default variation. */
893
- #variation() {
911
+ /** The taal one accompaniment segment plays: taal mode's, an explicit
912
+ * setTaal override (pinned across the whole song, তালফেরতা included), else
913
+ * the segment's own. `null` = a মুক্তছন্দ passage. */
914
+ #segmentTaal(seg) {
915
+ if (this.#taal !== void 0) return this.#taal;
916
+ if (!this.#taalAuto && this.#taalOverride !== void 0) return TAALS[this.#taalOverride];
917
+ return seg.taal;
918
+ }
919
+ /** The tabla variation to play: taal-mode taal's, the segment's own (auto
920
+ * mode), else none — an explicit setTaal override plays the default variation. */
921
+ #variation(taal) {
894
922
  if (this.#taal !== void 0) return this.#taal.variation;
895
- return this.#taalAuto ? this.#doc?.meta.taal?.variation : void 0;
923
+ return this.#taalAuto ? taal?.variation : void 0;
896
924
  }
897
925
  /** Resolve the active taal id: taal mode's taal, an explicit override, else
898
- * the doc's — but only when it names a sampled taal. */
926
+ * the doc's opening one (a তালফেরতা doc may carry it on its first row) — but
927
+ * only when it names a sampled taal. */
899
928
  #taalId() {
900
- const id = this.#taal?.id ?? (!this.#taalAuto && this.#taalOverride !== void 0 ? this.#taalOverride : this.#doc?.meta.taal?.id);
929
+ const opening = this.#doc?.parts[0]?.rows[0]?.taal || this.#doc?.meta.taal;
930
+ const id = this.#taal?.id ?? (!this.#taalAuto && this.#taalOverride !== void 0 ? this.#taalOverride : opening?.id);
901
931
  return id !== void 0 && id in TAALS ? id : void 0;
902
932
  }
903
933
  /** Resolve an instrument id to its def (custom overrides built-ins; the
@@ -988,14 +1018,15 @@ var SwaralipiPlayer = class {
988
1018
  const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
989
1019
  schedule.events.forEach((ev, i) => {
990
1020
  const at = ev.beat * spb + shift;
1021
+ if (ev.glide !== true) {
1022
+ for (const g of graceTouches(ev)) {
1023
+ const graceName = midiToSampleName(g.midi);
1024
+ transport.scheduleOnce((time) => melody.triggerAttackRelease(graceName, spb * 0.4, time), Math.max(g.beat * spb + shift, 0));
1025
+ }
1026
+ }
991
1027
  if (ev.midi !== null && ev.glide !== true) {
992
1028
  const name = midiToSampleName(ev.midi);
993
1029
  const dur = Math.max(ev.durationBeats * spb, 0.05);
994
- ev.kan.forEach((k, gi) => {
995
- const kanName = midiToSampleName(k);
996
- const lead = spb * 0.12 * (ev.kan.length - gi);
997
- transport.scheduleOnce((time) => melody.triggerAttackRelease(kanName, spb * 0.4, time), Math.max(at - lead, 0));
998
- });
999
1030
  const targets = glide !== void 0 ? glideTargets(schedule.events, i) : [];
1000
1031
  if (targets.length > 0 && glide !== void 0) {
1001
1032
  const last = targets[targets.length - 1];
@@ -1092,34 +1123,35 @@ var SwaralipiPlayer = class {
1092
1123
  return entry;
1093
1124
  }
1094
1125
  /**
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
1126
+ * Accompaniment, one লয় / তাল segment at a time. A tempo change under the
1127
+ * melody is a real tempo change under the drum: the tabla loop is rebuilt at
1128
+ * the segment's own bpm — its own recorded file when the set has one at that
1098
1129
  * tempo, else the nearest file time-stretched by `GrainPlayer` (which
1099
1130
  * 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.
1131
+ * click / drum / drone patterns re-space their interval. A তালফেরতা swaps the
1132
+ * theka the same way: each segment loops its own taal's file (or pattern, or
1133
+ * click cycle), and a মুক্তছন্দ segment rests. A document with neither mark
1134
+ * has exactly one segment, so this is the old single-loop path.
1102
1135
  */
1103
1136
  #buildRhythm(Tone, transport, base, spb, segments, shift) {
1104
1137
  this.#disposeTabla();
1105
1138
  const master = this.#ensureMaster(Tone);
1106
- const taalId = this.#taalId();
1107
- const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
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) : [];
1111
- const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
1139
+ const rhythm = this.#opts.rhythm;
1140
+ if (rhythm === "none" || base === void 0 && rhythm !== "drums") return;
1112
1141
  segments.forEach((seg, i) => {
1142
+ const own = this.#segmentTaal(seg);
1143
+ if (own === null) return;
1144
+ const taalId = own !== void 0 && own.id in TAALS ? own.id : void 0;
1113
1145
  const from = i === 0 ? 0 : seg.beat * spb + shift;
1114
1146
  const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
1115
1147
  const beatSec = spb / seg.scale;
1116
1148
  const span = to === Infinity ? void 0 : to - from;
1117
- if (wantsTabla) {
1149
+ if (rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0) {
1118
1150
  const bpm = 60 / beatSec;
1119
1151
  const fileBpm = nearestTablaBpm(bpm, tablaBpms(taalId));
1120
1152
  const rate = bpm / fileBpm;
1121
1153
  const semis = tablaShiftSemitones(this.#tonicSemitone());
1122
- const url = tablaUrl(base, taalId, fileBpm, this.#variation());
1154
+ const url = tablaUrl(base, taalId, fileBpm, this.#variation(own));
1123
1155
  const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1124
1156
  url,
1125
1157
  loop: true,
@@ -1141,9 +1173,10 @@ var SwaralipiPlayer = class {
1141
1173
  this.#tabla.push({ loop, fileSpb: 60 / fileBpm });
1142
1174
  return;
1143
1175
  }
1144
- if (this.#opts.rhythm === "drums") {
1176
+ if (rhythm === "drums") {
1145
1177
  this.#drums ??= createDrumKit(Tone).connect(master);
1146
1178
  const drums = this.#drums;
1179
+ const pattern = drumPattern(this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : own) ?? { numBeats: 4, sam: 0 });
1147
1180
  let beat2 = 0;
1148
1181
  this.#scheduleIds.push(
1149
1182
  transport.scheduleRepeat(
@@ -1161,6 +1194,7 @@ var SwaralipiPlayer = class {
1161
1194
  }
1162
1195
  this.#metronome ??= createMetronome(Tone, base).connect(master);
1163
1196
  const metro = this.#metronome;
1197
+ const cycle = (this.#taal ?? seg.taal)?.numBeats ?? 4;
1164
1198
  let beat = 0;
1165
1199
  this.#scheduleIds.push(
1166
1200
  transport.scheduleRepeat(
@@ -1219,7 +1253,7 @@ function midiRange2(schedule) {
1219
1253
  let lo = Infinity;
1220
1254
  let hi = -Infinity;
1221
1255
  for (const ev of schedule.events) {
1222
- for (const m of [ev.midi, ...ev.kan]) {
1256
+ for (const m of [ev.midi, ...ev.kan, ...ev.resh]) {
1223
1257
  if (m !== null) {
1224
1258
  if (m < lo) lo = m;
1225
1259
  if (m > hi) hi = m;
@@ -1236,7 +1270,7 @@ function pickDef(id, o) {
1236
1270
  return def;
1237
1271
  }
1238
1272
  function resolveTaal(doc, taal) {
1239
- const id = taal !== void 0 && taal !== "auto" ? taal : doc.meta.taal?.id;
1273
+ const id = taal !== void 0 && taal !== "auto" ? taal : (doc.parts[0]?.rows[0]?.taal || doc.meta.taal)?.id;
1240
1274
  return id !== void 0 && id in TAALS ? id : void 0;
1241
1275
  }
1242
1276
  function safeName(title) {
@@ -1259,7 +1293,7 @@ async function exportAudio(doc, options = {}) {
1259
1293
  });
1260
1294
  const range = midiRange2(schedule);
1261
1295
  const taalId = resolveTaal(doc, options.taal);
1262
- const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
1296
+ const pinned = options.taal !== void 0 && options.taal !== "auto" && taalId !== void 0 ? TAALS[taalId] : void 0;
1263
1297
  const usesTabla = rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && !!base;
1264
1298
  const reqBpm = options.bpm ?? doc.meta.bpm ?? 90;
1265
1299
  const bpm = usesTabla && taalId ? nearestTablaBpm(reqBpm, tablaBpms(taalId)) : reqBpm;
@@ -1276,14 +1310,14 @@ async function exportAudio(doc, options = {}) {
1276
1310
  const sampler = createSampler(Tone, def, base, range).connect(master);
1277
1311
  const glide = schedule.events.some((ev) => ev.glide === true) ? createBuffers(Tone, def, base, range) : void 0;
1278
1312
  schedule.events.forEach((ev, i) => {
1279
- if (ev.midi === null || ev.glide === true) return;
1313
+ if (ev.glide === true) return;
1314
+ for (const g of graceTouches(ev)) {
1315
+ const name = midiToSampleName(g.midi);
1316
+ transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, spb * 0.4, t), Math.max(g.beat * spb + shift, 0));
1317
+ }
1318
+ if (ev.midi === null) return;
1280
1319
  const at = ev.beat * spb + shift;
1281
1320
  const dur = Math.max(ev.durationBeats * spb, 0.05);
1282
- ev.kan.forEach((k, gi) => {
1283
- const name = midiToSampleName(k);
1284
- const lead = spb * 0.12 * (ev.kan.length - gi);
1285
- transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, spb * 0.4, t), Math.max(at - lead, 0));
1286
- });
1287
1321
  const targets = glide !== void 0 ? glideTargets(schedule.events, i) : [];
1288
1322
  if (targets.length > 0 && glide !== void 0) {
1289
1323
  const last = targets[targets.length - 1];
@@ -1311,22 +1345,22 @@ async function exportAudio(doc, options = {}) {
1311
1345
  }
1312
1346
  const tablas = [];
1313
1347
  if (rhythm !== "none" && base) {
1314
- const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
1315
- const pattern = rhythm === "drums" ? drumPattern(taal) : [];
1316
1348
  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;
1349
+ let metro;
1319
1350
  const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
1320
1351
  schedule.tempo.forEach((seg, i) => {
1352
+ const own = pinned ?? seg.taal;
1353
+ if (own === null) return;
1354
+ const segId = own !== void 0 && own.id in TAALS ? own.id : void 0;
1321
1355
  const from = i === 0 ? 0 : seg.beat * spb + shift;
1322
1356
  const to = i === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
1323
1357
  const beatSec = spb / seg.scale;
1324
1358
  const span = to === Infinity ? void 0 : to - from;
1325
- if (usesTabla && taalId) {
1359
+ if (rhythm === "tabla" && segId !== void 0 && SAMPLED_TAALS.has(segId)) {
1326
1360
  const segBpm = 60 / beatSec;
1327
- const fileBpm = nearestTablaBpm(segBpm, tablaBpms(taalId));
1361
+ const fileBpm = nearestTablaBpm(segBpm, tablaBpms(segId));
1328
1362
  const rate = segBpm / fileBpm;
1329
- const url = tablaUrl(base, taalId, fileBpm, variation);
1363
+ const url = tablaUrl(base, segId, fileBpm, pinned === void 0 ? own?.variation : void 0);
1330
1364
  const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
1331
1365
  url,
1332
1366
  loop: true,
@@ -1345,6 +1379,7 @@ async function exportAudio(doc, options = {}) {
1345
1379
  }
1346
1380
  tablas.push({ loop, fileSpb: 60 / fileBpm });
1347
1381
  } else if (drums !== void 0) {
1382
+ const pattern = drumPattern(segId !== void 0 ? TAALS[segId] : own ?? { numBeats: 4, sam: 0 });
1348
1383
  let beat = 0;
1349
1384
  transport.scheduleRepeat(
1350
1385
  (time) => {
@@ -1356,12 +1391,14 @@ async function exportAudio(doc, options = {}) {
1356
1391
  from,
1357
1392
  span
1358
1393
  );
1359
- } else if (metro !== void 0) {
1394
+ } else {
1395
+ const click = metro ??= createMetronome(Tone, base).connect(master);
1396
+ const cycle = own?.numBeats ?? 4;
1360
1397
  let beat = 0;
1361
1398
  transport.scheduleRepeat(
1362
1399
  (time) => {
1363
- metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1364
- metro.start(time);
1400
+ click.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
1401
+ click.start(time);
1365
1402
  beat += 1;
1366
1403
  },
1367
1404
  beatSec,
@@ -1556,17 +1593,17 @@ function encodeMidi(schedule, opts) {
1556
1593
  const hasGlide = schedule.events.some((ev) => ev.glide === true);
1557
1594
  if (hasGlide) melody.push(...bendRangeSetup());
1558
1595
  schedule.events.forEach((ev, i) => {
1559
- if (ev.midi === null || ev.glide === true) return;
1596
+ if (ev.glide === true) return;
1597
+ for (const g of graceTouches(ev)) {
1598
+ const on = Math.max(Math.round(g.beat * PPQ), 0);
1599
+ melody.push({ tick: on, on: true, note: g.midi, vel: 70, ch: 0 });
1600
+ melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ)), on: false, note: g.midi, vel: 0, ch: 0 });
1601
+ }
1602
+ if (ev.midi === null) return;
1560
1603
  const start = Math.round(ev.beat * PPQ);
1561
1604
  const targets = glideTargets(schedule.events, i);
1562
1605
  const last = targets[targets.length - 1] ?? ev;
1563
1606
  const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ), start + 1);
1564
- ev.kan.forEach((k, gi) => {
1565
- const gap = Math.round(0.12 * PPQ);
1566
- const kOn = Math.max(start - gap * (ev.kan.length - gi), 0);
1567
- melody.push({ tick: kOn, on: true, note: k, vel: 70, ch: 0 });
1568
- melody.push({ tick: Math.max(kOn + 1, start - gap * (ev.kan.length - gi - 1)), on: false, note: k, vel: 0, ch: 0 });
1569
- });
1570
1607
  melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
1571
1608
  melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
1572
1609
  let from = ev;
@@ -1587,14 +1624,18 @@ function encodeMidi(schedule, opts) {
1587
1624
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
1588
1625
  head.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
1589
1626
  const chunks = [trackChunk(melody, head)];
1590
- if (opts.drums && opts.drums.length > 0) {
1627
+ if (typeof opts.drums === "function" || opts.drums && opts.drums.length > 0) {
1591
1628
  const drum = [];
1592
- const totalBeats = Math.ceil(schedule.totalBeats);
1593
- for (let beat = 0; beat < totalBeats; beat++) {
1594
- const note = DRUM_NOTE[opts.drums[beat % opts.drums.length]];
1595
- const t = beat * PPQ;
1596
- drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
1597
- drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
1629
+ for (const seg of schedule.tempo) {
1630
+ const pattern = typeof opts.drums === "function" ? opts.drums(seg) : opts.drums;
1631
+ if (!pattern?.length) continue;
1632
+ const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
1633
+ for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
1634
+ const note = DRUM_NOTE[pattern[k % pattern.length]];
1635
+ const t = Math.round((seg.beat + k / seg.scale) * PPQ);
1636
+ drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
1637
+ drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
1638
+ }
1598
1639
  }
1599
1640
  chunks.push(trackChunk(drum, [...vlq(0), 255, 3, ...vlq(5), 68, 114, 117, 109, 115]));
1600
1641
  }
@@ -1624,9 +1665,11 @@ function exportMidi(doc, options = {}) {
1624
1665
  laySpeeds: options.laySpeeds
1625
1666
  });
1626
1667
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1627
- const taalId = doc.meta.taal?.id;
1628
- const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };
1629
- const drums = options.drums ? drumPattern(taal) : void 0;
1668
+ const drums = options.drums ? (seg) => {
1669
+ const id = seg.taal?.id;
1670
+ if (seg.taal === null) return void 0;
1671
+ return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
1672
+ } : void 0;
1630
1673
  const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums });
1631
1674
  return {
1632
1675
  blob: new Blob([bytes], { type: "audio/midi" }),
@@ -1638,6 +1681,7 @@ function exportMidi(doc, options = {}) {
1638
1681
  exports.ARC_RAW_RE = ARC_RAW_RE;
1639
1682
  exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
1640
1683
  exports.DRUM_KIT_BASE = DRUM_KIT_BASE;
1684
+ exports.GRACE_STEP = GRACE_STEP;
1641
1685
  exports.INSTRUMENTS = INSTRUMENTS;
1642
1686
  exports.LAYS = LAYS;
1643
1687
  exports.LAY_SPEEDS = LAY_SPEEDS;
@@ -1659,6 +1703,7 @@ exports.encodeWav = encodeWav;
1659
1703
  exports.exportAudio = exportAudio;
1660
1704
  exports.exportMidi = exportMidi;
1661
1705
  exports.glideTargets = glideTargets;
1706
+ exports.graceTouches = graceTouches;
1662
1707
  exports.instrumentSampleUrl = instrumentSampleUrl;
1663
1708
  exports.isArcRaw = isArcRaw;
1664
1709
  exports.meendContinuations = meendContinuations;