@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
@@ -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.1";
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);
@@ -417,12 +444,15 @@ function buildSchedule(doc, opts = {}) {
417
444
  const path = `${p}.${r}.${i}`;
418
445
  const written = note.durationMatra ?? 1;
419
446
  const dur = written / scale;
447
+ const kan = graceMidis(note.kan, tonicPc, baseOctave);
448
+ const resh = graceMidis(note.resh, tonicPc, baseOctave);
420
449
  if (note.svara !== void 0) {
421
450
  events.push({
422
451
  beat: cursor,
423
452
  durationBeats: dur,
424
453
  midi: svaraToMidi(note, tonicPc, baseOctave),
425
- kan: graceMidis(note.kan, tonicPc, baseOctave),
454
+ kan,
455
+ resh,
426
456
  path,
427
457
  kind: note.sustain === true ? "sustain" : "note",
428
458
  ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
@@ -430,16 +460,16 @@ function buildSchedule(doc, opts = {}) {
430
460
  lastPitched = events.length - 1;
431
461
  } else if (note.sustain === true) {
432
462
  if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
433
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "sustain" });
463
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
434
464
  } else {
435
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
465
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
436
466
  }
437
467
  cursor += dur;
438
468
  matra += written;
439
469
  });
440
470
  });
441
471
  });
442
- const numBeats = doc.meta.taal?.numBeats;
472
+ const numBeats = tempo[0].taal?.numBeats;
443
473
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
444
474
  tempo[tempo.length - 1].endBeat = cursor;
445
475
  return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
@@ -447,6 +477,7 @@ function buildSchedule(doc, opts = {}) {
447
477
 
448
478
  // src/viewer/render.ts
449
479
  init_grace();
480
+ init_taals();
450
481
 
451
482
  // src/viewer/glyphs.ts
452
483
  init_svara();
@@ -497,6 +528,11 @@ var LAY_LABELS = {
497
528
  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
529
  en: { drut: "Fast (drut)", madhya: "Medium (madhya)", vilambit: "Slow (vilambit)" }
499
530
  };
531
+ var FREE_TAAL_LABELS = {
532
+ bn: "\u09AE\u09C1\u0995\u09CD\u09A4\u099B\u09A8\u09CD\u09A6",
533
+ hi: "\u092E\u0941\u0915\u094D\u0924\u091B\u0902\u0926",
534
+ en: "Free rhythm"
535
+ };
500
536
  function composeCluster(letter, opts) {
501
537
  const classes = [];
502
538
  if (opts.lang === "bn") {
@@ -969,15 +1005,25 @@ function renderToHtml(doc, opts = {}) {
969
1005
  out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
970
1006
  let n = 0;
971
1007
  let lay;
1008
+ let taal = doc.meta.taal;
1009
+ let named = !isTaalFerta(doc.meta.taalName);
972
1010
  doc.parts.forEach((part, p) => {
973
1011
  out.push('<section class="sw-part">');
974
1012
  if (part.name !== void 0)
975
1013
  out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
976
1014
  part.rows.forEach((row, r) => {
1015
+ const marks = [];
1016
+ if (row.taal !== void 0 && (!named || !sameTaal(row.taal, taal))) {
1017
+ taal = row.taal;
1018
+ named = true;
1019
+ const name = row.taalName ?? (taal === null ? FREE_TAAL_LABELS[ctx.lang] : taal.id);
1020
+ marks.push(`<span class="sw-taal" data-taal="${escapeHtml(taal?.id ?? "free")}">${escapeHtml(ctx.tr(name))}</span>`);
1021
+ }
977
1022
  if (row.lay !== void 0 && row.lay !== lay) {
978
1023
  lay = row.lay;
979
- out.push(`<div class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</div>`);
1024
+ marks.push(`<span class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</span>`);
980
1025
  }
1026
+ if (marks.length > 0) out.push(`<div class="sw-change">${marks.join("")}</div>`);
981
1027
  out.push(renderRow(row, p, r, layout.offsets[n++], ctx));
982
1028
  });
983
1029
  out.push("</section>");
@@ -1071,10 +1117,12 @@ ${FONT_FACES}
1071
1117
 
1072
1118
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
1073
1119
 
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 {
1120
+ /* \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,
1121
+ centred between two systems the way \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 prints "\u099D\u09BE\u0981\u09AA\u09A4\u09BE\u09B2" or
1122
+ "\u09A6\u09CD\u09B0\u09C1\u09A4 \u09B2\u09AF\u09BC". A full-width band rather than a row label \u2014 the label track is
1123
+ what the notation columns are measured against, and a six-em word in it
1124
+ would shove the whole sheet right. */
1125
+ .sw-change {
1078
1126
  grid-column: 1 / -1;
1079
1127
  display: flex;
1080
1128
  align-items: center;
@@ -1088,12 +1136,14 @@ ${FONT_FACES}
1088
1136
  opacity: 0.75;
1089
1137
  break-after: avoid;
1090
1138
  }
1091
- .sw-lay::after {
1139
+ .sw-change::before,
1140
+ .sw-change::after {
1092
1141
  content: '';
1093
1142
  flex: 1;
1094
1143
  height: 1px;
1095
1144
  background: var(--sw-rule, rgba(128, 128, 128, 0.35));
1096
1145
  }
1146
+ .sw-taal + .sw-lay::before { content: '\xB7'; margin-right: 0.6em; }
1097
1147
 
1098
1148
  /* ---- block grid: ONE column set for the whole sheet ----
1099
1149
  * NLTR emits a song as a single html table, which is why its lines align
@@ -1311,7 +1361,7 @@ var SWARALIPI_PRINT_CSS = `
1311
1361
  .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
1312
1362
  .sw-scroll { overflow: visible !important; }
1313
1363
  .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; }
1364
+ .sw-part-name, .sw-header, .sw-change { break-after: avoid-page; page-break-after: avoid; }
1315
1365
  .sw-hl { background: none !important; }
1316
1366
  /* Keep each engraved staff system whole across a page break (the abcjs line
1317
1367
  is a <div> wrapping the <svg>, so the break rule must sit on the div). */
@@ -1487,11 +1537,15 @@ function eventPaths(schedule) {
1487
1537
  }
1488
1538
  return paths;
1489
1539
  }
1540
+ var barLen = (m) => m.beatsPerBar > 0 ? m.beatsPerBar : 4;
1490
1541
  function abcText(s) {
1491
1542
  return s.replace(/[\r\n]+/g, " ").trim();
1492
1543
  }
1493
1544
  function buildAbc(schedule, lyricOf, opts) {
1494
- const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
1545
+ const meters = opts.meters?.length ? opts.meters : [{ beat: 0, beatsPerBar: opts.beatsPerBar, drums: opts.drums }];
1546
+ const withDrums = opts.drums !== void 0 || meters.some((m2) => m2.drums !== void 0);
1547
+ let m = 0;
1548
+ let beatsPerBar = barLen(meters[0]);
1495
1549
  const tokens = [];
1496
1550
  const words = [];
1497
1551
  let barPos = 0;
@@ -1505,6 +1559,17 @@ function buildAbc(schedule, lyricOf, opts) {
1505
1559
  }
1506
1560
  };
1507
1561
  schedule.events.forEach((ev, i) => {
1562
+ while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS2) {
1563
+ m++;
1564
+ if (barPos > EPS2) {
1565
+ tokens.push("|");
1566
+ words.push("|");
1567
+ }
1568
+ barPos = 0;
1569
+ const next = barLen(meters[m]);
1570
+ if (next !== beatsPerBar) tokens.push(`[M:${ratioMeter(next)}]`);
1571
+ beatsPerBar = next;
1572
+ }
1508
1573
  if (ev.midi === null) {
1509
1574
  if (ev.kind === "rest") {
1510
1575
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
@@ -1523,16 +1588,16 @@ function buildAbc(schedule, lyricOf, opts) {
1523
1588
  });
1524
1589
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1525
1590
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1526
- const meter = ratioMeter(beatsPerBar);
1591
+ const meter = ratioMeter(barLen(meters[0]));
1527
1592
  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));
1593
+ if (withDrums) head.push(...percMapLines(meters.flatMap((x) => x.drums ?? [])));
1529
1594
  head.push("K:C treble");
1530
1595
  const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
1531
1596
  const lines = [...head];
1532
- if (opts.drums) {
1597
+ if (withDrums) {
1533
1598
  lines.push("V:1", melodyBody);
1534
1599
  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));
1600
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, meters));
1536
1601
  } else {
1537
1602
  lines.push(melodyBody);
1538
1603
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
@@ -1561,13 +1626,21 @@ function percMapLines(voices) {
1561
1626
  }
1562
1627
  return lines;
1563
1628
  }
1564
- function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
1565
- const n = Math.max(1, Math.ceil(totalBeats));
1629
+ function buildDrumVoice(totalBeats, meters) {
1566
1630
  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
- }
1631
+ meters.forEach((m, i) => {
1632
+ const bpb = barLen(m);
1633
+ if (i > 0) {
1634
+ if (tokens.length > 0 && tokens[tokens.length - 1] !== "|") tokens.push("|");
1635
+ if (bpb !== barLen(meters[i - 1])) tokens.push(`[M:${ratioMeter(bpb)}]`);
1636
+ }
1637
+ const end = meters[i + 1]?.beat ?? Math.max(1, Math.ceil(totalBeats));
1638
+ const n = Math.ceil(end - m.beat - 1e-6);
1639
+ for (let beat = 0; beat < n; beat++) {
1640
+ tokens.push(m.drums?.length ? DRUM_ABC[m.drums[beat % m.drums.length]].note : "z");
1641
+ if ((beat + 1) % bpb === 0) tokens.push("|");
1642
+ }
1643
+ });
1571
1644
  if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
1572
1645
  else tokens[tokens.length - 1] = "|]";
1573
1646
  return tokens.join(" ");
@@ -1578,16 +1651,25 @@ function lyricLookup(doc) {
1578
1651
  return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
1579
1652
  };
1580
1653
  }
1581
- function resolveDrumTaal(doc) {
1582
- const id = doc.meta.taal?.id;
1654
+ function resolveDrumTaal(taal) {
1655
+ const id = taal?.id;
1583
1656
  return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
1584
1657
  }
1585
1658
  function docToAbc(doc, opts = {}) {
1586
1659
  const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave, lay: false });
1587
1660
  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 });
1661
+ const meters = schedule.tempo.map((seg) => ({
1662
+ beat: seg.beat,
1663
+ beatsPerBar: opts.beatsPerBar ?? seg.taal?.numBeats ?? 4,
1664
+ drums: opts.drums && seg.taal !== null ? drumPattern(resolveDrumTaal(seg.taal)) : void 0
1665
+ }));
1666
+ return buildAbc(schedule, lyricLookup(doc), {
1667
+ bpm,
1668
+ lyrics: opts.lyrics !== false,
1669
+ beatsPerBar: meters[0].beatsPerBar,
1670
+ drums: opts.drums ? meters[0].drums ?? [] : void 0,
1671
+ meters
1672
+ });
1591
1673
  }
1592
1674
  async function abcToSvg(abc, opts = {}, notePaths) {
1593
1675
  if (typeof document === "undefined") {
@@ -1666,7 +1748,7 @@ function buildTaalAbc(taal, opts = {}) {
1666
1748
  const voices = Array.from({ length: n }, (_, i) => src[i % src.length]);
1667
1749
  const bpm = Math.round(opts.bpm ?? 90);
1668
1750
  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)];
1751
+ const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, [{ beat: 0, beatsPerBar: n, drums: voices }])];
1670
1752
  if (opts.bols?.length) {
1671
1753
  const w = opts.bols.slice(0, n).map((b) => b.trim() ? abcText(b).replace(/\s+/g, "~") : "*");
1672
1754
  while (w.length < n) w.push("*");
@@ -2057,6 +2139,7 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
2057
2139
  exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
2058
2140
  exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
2059
2141
  exports.DIGITS = DIGITS;
2142
+ exports.FREE_TAAL_LABELS = FREE_TAAL_LABELS;
2060
2143
  exports.LABELS = LABELS;
2061
2144
  exports.LAY_LABELS = LAY_LABELS;
2062
2145
  exports.MARKS = MARKS;