@ugfoundation/swaralipi-js 0.8.2 → 0.9.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 (51) hide show
  1. package/dist/{batch-6FPBT5DP.js → batch-PYBXQ4OM.js} +5 -5
  2. package/dist/{batch-6FPBT5DP.js.map → batch-PYBXQ4OM.js.map} +1 -1
  3. package/dist/{chunk-FULC22MN.js → chunk-3ZQPM2CE.js} +28 -19
  4. package/dist/chunk-3ZQPM2CE.js.map +1 -0
  5. package/dist/{chunk-4IVUPMRK.js → chunk-7SMRG4IS.js} +14 -9
  6. package/dist/chunk-7SMRG4IS.js.map +1 -0
  7. package/dist/{chunk-4HNT6OAT.js → chunk-MYVIQU4O.js} +12 -3
  8. package/dist/chunk-MYVIQU4O.js.map +1 -0
  9. package/dist/{chunk-UMBKIGVQ.js → chunk-RT4SVJMK.js} +5 -4
  10. package/dist/chunk-RT4SVJMK.js.map +1 -0
  11. package/dist/cli.cjs +33 -19
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +3 -3
  14. package/dist/convert/index.cjs +31 -17
  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 +38 -17
  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/{lay-Csa4CD46.d.cts → lay-CZIzR9cM.d.ts} +16 -2
  25. package/dist/{lay-Ct9ky_EE.d.ts → lay-CfDlPhH2.d.cts} +16 -2
  26. package/dist/player/index.cjs +80 -48
  27. package/dist/player/index.cjs.map +1 -1
  28. package/dist/player/index.d.cts +11 -9
  29. package/dist/player/index.d.ts +11 -9
  30. package/dist/player/index.js +68 -46
  31. package/dist/player/index.js.map +1 -1
  32. package/dist/{schedule-B_UaluBi.d.cts → schedule-9uvjnCG_.d.ts} +15 -10
  33. package/dist/{schedule-CdILTNsk.d.ts → schedule-TxSfHL-T.d.cts} +15 -10
  34. package/dist/{schema-DGA_bNVZ.d.ts → schema-Cf_epUIC.d.cts} +333 -63
  35. package/dist/{schema-DGA_bNVZ.d.cts → schema-Cf_epUIC.d.ts} +333 -63
  36. package/dist/spec/index.cjs +38 -17
  37. package/dist/spec/index.cjs.map +1 -1
  38. package/dist/spec/index.d.cts +26 -8
  39. package/dist/spec/index.d.ts +26 -8
  40. package/dist/spec/index.js +2 -2
  41. package/dist/viewer/index.cjs +150 -70
  42. package/dist/viewer/index.cjs.map +1 -1
  43. package/dist/viewer/index.d.cts +25 -7
  44. package/dist/viewer/index.d.ts +25 -7
  45. package/dist/viewer/index.js +78 -27
  46. package/dist/viewer/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-4HNT6OAT.js.map +0 -1
  49. package/dist/chunk-4IVUPMRK.js.map +0 -1
  50. package/dist/chunk-FULC22MN.js.map +0 -1
  51. package/dist/chunk-UMBKIGVQ.js.map +0 -1
@@ -38,30 +38,16 @@ var init_lay = __esm({
38
38
  }
39
39
  });
40
40
 
41
- // src/spec/grace.ts
42
- function graceNotes(grace) {
43
- if (grace === void 0) return [];
44
- return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
45
- }
46
- var init_grace = __esm({
47
- "src/spec/grace.ts"() {
48
- }
49
- });
50
-
51
- // src/spec/svara.ts
52
- function hasKomal(letter) {
53
- return letter === "r" || letter === "g" || letter === "d" || letter === "n";
41
+ // src/spec/taals.ts
42
+ function isTaalFerta(name) {
43
+ return name !== void 0 && name.normalize("NFC").replace(/[\s-]/g, "") === TAAL_FERTA;
54
44
  }
55
- function hasTivra(letter) {
56
- return letter === "m";
45
+ function sameTaal(a, b) {
46
+ if (a === b) return true;
47
+ if (!a || !b) return false;
48
+ return a.id === b.id && a.numBeats === b.numBeats && a.variation === b.variation && String(a.bhaags) === String(b.bhaags);
57
49
  }
58
- var init_svara = __esm({
59
- "src/spec/svara.ts"() {
60
- }
61
- });
62
-
63
- // src/spec/taals.ts
64
- var TAALS;
50
+ var TAALS, TAAL_FERTA;
65
51
  var init_taals = __esm({
66
52
  "src/spec/taals.ts"() {
67
53
  TAALS = {
@@ -85,9 +71,32 @@ var init_taals = __esm({
85
71
  dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] }
86
72
  // ধামার
87
73
  };
74
+ TAAL_FERTA = "\u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE";
88
75
  }
89
76
  });
90
- var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, graceSchema, noteSchema, laySchema, rowSchema, partSchema, taalSchema, metaSchema, swaralipiSchema;
77
+
78
+ // src/spec/grace.ts
79
+ function graceNotes(grace) {
80
+ if (grace === void 0) return [];
81
+ return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
82
+ }
83
+ var init_grace = __esm({
84
+ "src/spec/grace.ts"() {
85
+ }
86
+ });
87
+
88
+ // src/spec/svara.ts
89
+ function hasKomal(letter) {
90
+ return letter === "r" || letter === "g" || letter === "d" || letter === "n";
91
+ }
92
+ function hasTivra(letter) {
93
+ return letter === "m";
94
+ }
95
+ var init_svara = __esm({
96
+ "src/spec/svara.ts"() {
97
+ }
98
+ });
99
+ var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, graceSchema, noteSchema, laySchema, taalSchema, rowSchema, partSchema, metaSchema, swaralipiSchema;
91
100
  var init_schema = __esm({
92
101
  "src/spec/schema.ts"() {
93
102
  octaveSchema = zod.z.enum([
@@ -140,22 +149,6 @@ var init_schema = __esm({
140
149
  )
141
150
  );
142
151
  laySchema = zod.z.enum(["drut", "madhya", "vilambit"]);
143
- rowSchema = zod.z.object({
144
- /** Optional line label (e.g. a tuk/section marker). */
145
- label: zod.z.string().optional(),
146
- /**
147
- * লয়: the tempo tier this row switches to, held until another row changes
148
- * it (absent on every row = the whole song at `meta.bpm`, i.e. দ্রুত).
149
- * Read it with `rowLay()`, which also picks up the mark NLTR *prints*
150
- * ("বিলম্বিত লয়:") in already-converted documents.
151
- */
152
- lay: laySchema.optional(),
153
- notes: zod.z.array(noteSchema)
154
- });
155
- partSchema = zod.z.object({
156
- name: zod.z.string().optional(),
157
- rows: zod.z.array(rowSchema)
158
- });
159
152
  taalSchema = zod.z.object({
160
153
  /** Player sample key, e.g. `dadra`, `teentaal`. */
161
154
  id: zod.z.string(),
@@ -176,6 +169,31 @@ var init_schema = __esm({
176
169
  }).refine((t) => t.bhaags === void 0 || t.bhaags.reduce((a, b) => a + b, 0) === t.numBeats, {
177
170
  message: "bhaags must sum to numBeats"
178
171
  });
172
+ rowSchema = zod.z.object({
173
+ /** Optional line label (e.g. a tuk/section marker). */
174
+ label: zod.z.string().optional(),
175
+ /**
176
+ * লয়: the tempo tier this row switches to, held until another row changes
177
+ * it (absent on every row = the whole song at `meta.bpm`, i.e. দ্রুত).
178
+ * Read it with `rowLay()`, which also picks up the mark NLTR *prints*
179
+ * ("বিলম্বিত লয়:") in already-converted documents.
180
+ */
181
+ lay: laySchema.optional(),
182
+ /**
183
+ * তালফেরতা: the তাল this row switches to, held until another row changes it
184
+ * (absent on every row = the whole song in `meta.taal`). `null` = মুক্তছন্দ
185
+ * from here on: the accompaniment rests until a later row names a taal.
186
+ * Independent of `lay` — either, both or neither may change on one row.
187
+ */
188
+ taal: taalSchema.nullable().optional(),
189
+ /** That taal's name as printed (e.g. `ষষ্ঠী(২/৪)`) — `meta.taalName`'s twin. */
190
+ taalName: zod.z.string().optional(),
191
+ notes: zod.z.array(noteSchema)
192
+ });
193
+ partSchema = zod.z.object({
194
+ name: zod.z.string().optional(),
195
+ rows: zod.z.array(rowSchema)
196
+ });
179
197
  metaSchema = zod.z.object({
180
198
  title: zod.z.string(),
181
199
  /** Cross-reference to a gitabitan.io song content id (filename, immutable). */
@@ -222,12 +240,14 @@ __export(spec_exports, {
222
240
  LAY_SPEEDS: () => LAY_SPEEDS,
223
241
  SPEC_VERSION: () => SPEC_VERSION,
224
242
  TAALS: () => TAALS,
243
+ TAAL_FERTA: () => TAAL_FERTA,
225
244
  barSchema: () => barSchema,
226
245
  bracketSchema: () => bracketSchema,
227
246
  graceNotes: () => graceNotes,
228
247
  graceSchema: () => graceSchema,
229
248
  hasKomal: () => hasKomal,
230
249
  hasTivra: () => hasTivra,
250
+ isTaalFerta: () => isTaalFerta,
231
251
  laySchema: () => laySchema,
232
252
  metaSchema: () => metaSchema,
233
253
  microtoneSchema: () => microtoneSchema,
@@ -240,6 +260,7 @@ __export(spec_exports, {
240
260
  rowLay: () => rowLay,
241
261
  rowSchema: () => rowSchema,
242
262
  safeParseSwaralipi: () => safeParseSwaralipi,
263
+ sameTaal: () => sameTaal,
243
264
  svaraLetterSchema: () => svaraLetterSchema,
244
265
  svaraSchema: () => svaraSchema,
245
266
  swaralipiSchema: () => swaralipiSchema,
@@ -260,12 +281,13 @@ var init_spec = __esm({
260
281
  init_grace();
261
282
  init_lay();
262
283
  init_svara();
263
- SPEC_VERSION = "0.8.2";
284
+ SPEC_VERSION = "0.9.0";
264
285
  }
265
286
  });
266
287
 
267
288
  // src/player/schedule.ts
268
289
  init_lay();
290
+ init_taals();
269
291
 
270
292
  // src/player/pitch.ts
271
293
  init_grace();
@@ -389,18 +411,23 @@ function buildSchedule(doc, opts = {}) {
389
411
  let matra = 0;
390
412
  const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
391
413
  const withLay = opts.lay !== false;
392
- const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut" }];
414
+ const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
393
415
  let scale = 1;
416
+ let taal = doc.meta.taal;
394
417
  doc.parts.forEach((part, p) => {
395
418
  part.rows.forEach((row, r) => {
396
419
  const lay = withLay ? rowLay(row) : void 0;
397
- if (lay !== void 0 && speeds[lay] !== scale) {
398
- scale = speeds[lay];
420
+ const layChanged = lay !== void 0 && speeds[lay] !== scale;
421
+ const nextTaal = row.taal !== void 0 ? row.taal : taal;
422
+ if (layChanged || !sameTaal(nextTaal, taal)) {
423
+ if (layChanged) scale = speeds[lay];
424
+ taal = nextTaal;
399
425
  const last = tempo[tempo.length - 1];
400
- if (last.beat === cursor) tempo[tempo.length - 1] = { beat: cursor, endBeat: cursor, scale, lay };
426
+ const seg = { beat: cursor, endBeat: cursor, scale, lay: layChanged ? lay : last.lay, taal };
427
+ if (last.beat === cursor) tempo[tempo.length - 1] = seg;
401
428
  else {
402
429
  last.endBeat = cursor;
403
- tempo.push({ beat: cursor, endBeat: cursor, scale, lay });
430
+ tempo.push(seg);
404
431
  }
405
432
  }
406
433
  const glides = meendContinuations(row.notes);
@@ -439,7 +466,7 @@ function buildSchedule(doc, opts = {}) {
439
466
  });
440
467
  });
441
468
  });
442
- const numBeats = doc.meta.taal?.numBeats;
469
+ const numBeats = tempo[0].taal?.numBeats;
443
470
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
444
471
  tempo[tempo.length - 1].endBeat = cursor;
445
472
  return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
@@ -447,6 +474,7 @@ function buildSchedule(doc, opts = {}) {
447
474
 
448
475
  // src/viewer/render.ts
449
476
  init_grace();
477
+ init_taals();
450
478
 
451
479
  // src/viewer/glyphs.ts
452
480
  init_svara();
@@ -497,6 +525,11 @@ var LAY_LABELS = {
497
525
  hi: { drut: "\u0926\u094D\u0930\u0941\u0924 \u0932\u092F", madhya: "\u092E\u0927\u094D\u092F \u0932\u092F", vilambit: "\u0935\u093F\u0932\u0902\u092C\u093F\u0924 \u0932\u092F" },
498
526
  en: { drut: "Fast (drut)", madhya: "Medium (madhya)", vilambit: "Slow (vilambit)" }
499
527
  };
528
+ var FREE_TAAL_LABELS = {
529
+ bn: "\u09AE\u09C1\u0995\u09CD\u09A4\u099B\u09A8\u09CD\u09A6",
530
+ hi: "\u092E\u0941\u0915\u094D\u0924\u091B\u0902\u0926",
531
+ en: "Free rhythm"
532
+ };
500
533
  function composeCluster(letter, opts) {
501
534
  const classes = [];
502
535
  if (opts.lang === "bn") {
@@ -969,15 +1002,25 @@ function renderToHtml(doc, opts = {}) {
969
1002
  out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
970
1003
  let n = 0;
971
1004
  let lay;
1005
+ let taal = doc.meta.taal;
1006
+ let named = !isTaalFerta(doc.meta.taalName);
972
1007
  doc.parts.forEach((part, p) => {
973
1008
  out.push('<section class="sw-part">');
974
1009
  if (part.name !== void 0)
975
1010
  out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
976
1011
  part.rows.forEach((row, r) => {
1012
+ const marks = [];
1013
+ if (row.taal !== void 0 && (!named || !sameTaal(row.taal, taal))) {
1014
+ taal = row.taal;
1015
+ named = true;
1016
+ const name = row.taalName ?? (taal === null ? FREE_TAAL_LABELS[ctx.lang] : taal.id);
1017
+ marks.push(`<span class="sw-taal" data-taal="${escapeHtml(taal?.id ?? "free")}">${escapeHtml(ctx.tr(name))}</span>`);
1018
+ }
977
1019
  if (row.lay !== void 0 && row.lay !== lay) {
978
1020
  lay = row.lay;
979
- out.push(`<div class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</div>`);
1021
+ marks.push(`<span class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</span>`);
980
1022
  }
1023
+ if (marks.length > 0) out.push(`<div class="sw-change">${marks.join("")}</div>`);
981
1024
  out.push(renderRow(row, p, r, layout.offsets[n++], ctx));
982
1025
  });
983
1026
  out.push("</section>");
@@ -1071,10 +1114,12 @@ ${FONT_FACES}
1071
1114
 
1072
1115
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
1073
1116
 
1074
- /* \u09B2\u09AF\u09BC marker: the line where the tempo tier turns over. A full-width rule
1075
- rather than a row label \u2014 the label track is what the notation columns are
1076
- measured against, and a six-em word in it would shove the whole sheet right. */
1077
- .sw-lay {
1117
+ /* \u09A4\u09BE\u09B2 / \u09B2\u09AF\u09BC change: the line where the \u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE or the tempo tier turns over,
1118
+ centred between two systems the way \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 prints "\u099D\u09BE\u0981\u09AA\u09A4\u09BE\u09B2" or
1119
+ "\u09A6\u09CD\u09B0\u09C1\u09A4 \u09B2\u09AF\u09BC". A full-width band rather than a row label \u2014 the label track is
1120
+ what the notation columns are measured against, and a six-em word in it
1121
+ would shove the whole sheet right. */
1122
+ .sw-change {
1078
1123
  grid-column: 1 / -1;
1079
1124
  display: flex;
1080
1125
  align-items: center;
@@ -1088,12 +1133,14 @@ ${FONT_FACES}
1088
1133
  opacity: 0.75;
1089
1134
  break-after: avoid;
1090
1135
  }
1091
- .sw-lay::after {
1136
+ .sw-change::before,
1137
+ .sw-change::after {
1092
1138
  content: '';
1093
1139
  flex: 1;
1094
1140
  height: 1px;
1095
1141
  background: var(--sw-rule, rgba(128, 128, 128, 0.35));
1096
1142
  }
1143
+ .sw-taal + .sw-lay::before { content: '\xB7'; margin-right: 0.6em; }
1097
1144
 
1098
1145
  /* ---- block grid: ONE column set for the whole sheet ----
1099
1146
  * NLTR emits a song as a single html table, which is why its lines align
@@ -1311,7 +1358,7 @@ var SWARALIPI_PRINT_CSS = `
1311
1358
  .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
1312
1359
  .sw-scroll { overflow: visible !important; }
1313
1360
  .sw-row { break-inside: avoid; page-break-inside: avoid; }
1314
- .sw-part-name, .sw-header, .sw-lay { break-after: avoid-page; page-break-after: avoid; }
1361
+ .sw-part-name, .sw-header, .sw-change { break-after: avoid-page; page-break-after: avoid; }
1315
1362
  .sw-hl { background: none !important; }
1316
1363
  /* Keep each engraved staff system whole across a page break (the abcjs line
1317
1364
  is a <div> wrapping the <svg>, so the break rule must sit on the div). */
@@ -1487,11 +1534,15 @@ function eventPaths(schedule) {
1487
1534
  }
1488
1535
  return paths;
1489
1536
  }
1537
+ var barLen = (m) => m.beatsPerBar > 0 ? m.beatsPerBar : 4;
1490
1538
  function abcText(s) {
1491
1539
  return s.replace(/[\r\n]+/g, " ").trim();
1492
1540
  }
1493
1541
  function buildAbc(schedule, lyricOf, opts) {
1494
- const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
1542
+ const meters = opts.meters?.length ? opts.meters : [{ beat: 0, beatsPerBar: opts.beatsPerBar, drums: opts.drums }];
1543
+ const withDrums = opts.drums !== void 0 || meters.some((m2) => m2.drums !== void 0);
1544
+ let m = 0;
1545
+ let beatsPerBar = barLen(meters[0]);
1495
1546
  const tokens = [];
1496
1547
  const words = [];
1497
1548
  let barPos = 0;
@@ -1505,6 +1556,17 @@ function buildAbc(schedule, lyricOf, opts) {
1505
1556
  }
1506
1557
  };
1507
1558
  schedule.events.forEach((ev, i) => {
1559
+ while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS2) {
1560
+ m++;
1561
+ if (barPos > EPS2) {
1562
+ tokens.push("|");
1563
+ words.push("|");
1564
+ }
1565
+ barPos = 0;
1566
+ const next = barLen(meters[m]);
1567
+ if (next !== beatsPerBar) tokens.push(`[M:${ratioMeter(next)}]`);
1568
+ beatsPerBar = next;
1569
+ }
1508
1570
  if (ev.midi === null) {
1509
1571
  if (ev.kind === "rest") {
1510
1572
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
@@ -1523,16 +1585,16 @@ function buildAbc(schedule, lyricOf, opts) {
1523
1585
  });
1524
1586
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1525
1587
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1526
- const meter = ratioMeter(beatsPerBar);
1588
+ const meter = ratioMeter(barLen(meters[0]));
1527
1589
  const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
1528
- if (opts.drums) head.push(...percMapLines(opts.drums));
1590
+ if (withDrums) head.push(...percMapLines(meters.flatMap((x) => x.drums ?? [])));
1529
1591
  head.push("K:C treble");
1530
1592
  const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
1531
1593
  const lines = [...head];
1532
- if (opts.drums) {
1594
+ if (withDrums) {
1533
1595
  lines.push("V:1", melodyBody);
1534
1596
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1535
- lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, beatsPerBar, opts.drums));
1597
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, meters));
1536
1598
  } else {
1537
1599
  lines.push(melodyBody);
1538
1600
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
@@ -1561,13 +1623,21 @@ function percMapLines(voices) {
1561
1623
  }
1562
1624
  return lines;
1563
1625
  }
1564
- function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
1565
- const n = Math.max(1, Math.ceil(totalBeats));
1626
+ function buildDrumVoice(totalBeats, meters) {
1566
1627
  const tokens = [];
1567
- for (let beat = 0; beat < n; beat++) {
1568
- tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
1569
- if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
1570
- }
1628
+ meters.forEach((m, i) => {
1629
+ const bpb = barLen(m);
1630
+ if (i > 0) {
1631
+ if (tokens.length > 0 && tokens[tokens.length - 1] !== "|") tokens.push("|");
1632
+ if (bpb !== barLen(meters[i - 1])) tokens.push(`[M:${ratioMeter(bpb)}]`);
1633
+ }
1634
+ const end = meters[i + 1]?.beat ?? Math.max(1, Math.ceil(totalBeats));
1635
+ const n = Math.ceil(end - m.beat - 1e-6);
1636
+ for (let beat = 0; beat < n; beat++) {
1637
+ tokens.push(m.drums?.length ? DRUM_ABC[m.drums[beat % m.drums.length]].note : "z");
1638
+ if ((beat + 1) % bpb === 0) tokens.push("|");
1639
+ }
1640
+ });
1571
1641
  if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
1572
1642
  else tokens[tokens.length - 1] = "|]";
1573
1643
  return tokens.join(" ");
@@ -1578,16 +1648,25 @@ function lyricLookup(doc) {
1578
1648
  return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
1579
1649
  };
1580
1650
  }
1581
- function resolveDrumTaal(doc) {
1582
- const id = doc.meta.taal?.id;
1651
+ function resolveDrumTaal(taal) {
1652
+ const id = taal?.id;
1583
1653
  return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
1584
1654
  }
1585
1655
  function docToAbc(doc, opts = {}) {
1586
1656
  const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave, lay: false });
1587
1657
  const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1588
- const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
1589
- const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
1590
- return buildAbc(schedule, lyricLookup(doc), { bpm, lyrics: opts.lyrics !== false, beatsPerBar, drums });
1658
+ const meters = schedule.tempo.map((seg) => ({
1659
+ beat: seg.beat,
1660
+ beatsPerBar: opts.beatsPerBar ?? seg.taal?.numBeats ?? 4,
1661
+ drums: opts.drums && seg.taal !== null ? drumPattern(resolveDrumTaal(seg.taal)) : void 0
1662
+ }));
1663
+ return buildAbc(schedule, lyricLookup(doc), {
1664
+ bpm,
1665
+ lyrics: opts.lyrics !== false,
1666
+ beatsPerBar: meters[0].beatsPerBar,
1667
+ drums: opts.drums ? meters[0].drums ?? [] : void 0,
1668
+ meters
1669
+ });
1591
1670
  }
1592
1671
  async function abcToSvg(abc, opts = {}, notePaths) {
1593
1672
  if (typeof document === "undefined") {
@@ -1666,7 +1745,7 @@ function buildTaalAbc(taal, opts = {}) {
1666
1745
  const voices = Array.from({ length: n }, (_, i) => src[i % src.length]);
1667
1746
  const bpm = Math.round(opts.bpm ?? 90);
1668
1747
  const head = ["X:1", `M:${ratioMeter(n)}`, "L:1/4", `Q:1/4=${bpm}`, ...percMapLines(voices), "K:C"];
1669
- const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, n, voices)];
1748
+ const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, [{ beat: 0, beatsPerBar: n, drums: voices }])];
1670
1749
  if (opts.bols?.length) {
1671
1750
  const w = opts.bols.slice(0, n).map((b) => b.trim() ? abcText(b).replace(/\s+/g, "~") : "*");
1672
1751
  while (w.length < n) w.push("*");
@@ -2057,6 +2136,7 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
2057
2136
  exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
2058
2137
  exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
2059
2138
  exports.DIGITS = DIGITS;
2139
+ exports.FREE_TAAL_LABELS = FREE_TAAL_LABELS;
2060
2140
  exports.LABELS = LABELS;
2061
2141
  exports.LAY_LABELS = LAY_LABELS;
2062
2142
  exports.MARKS = MARKS;