@ugfoundation/swaralipi-js 0.8.1 → 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-NDL6CW7Q.js → batch-PYBXQ4OM.js} +5 -5
  2. package/dist/{batch-NDL6CW7Q.js.map → batch-PYBXQ4OM.js.map} +1 -1
  3. package/dist/{chunk-LPCKL3AM.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-Z42M5JAA.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 +306 -178
  42. package/dist/viewer/index.cjs.map +1 -1
  43. package/dist/viewer/index.d.cts +36 -12
  44. package/dist/viewer/index.d.ts +36 -12
  45. package/dist/viewer/index.js +234 -135
  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-LPCKL3AM.js.map +0 -1
  51. package/dist/chunk-Z42M5JAA.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;
41
+ // src/spec/taals.ts
42
+ function isTaalFerta(name) {
43
+ return name !== void 0 && name.normalize("NFC").replace(/[\s-]/g, "") === TAAL_FERTA;
45
44
  }
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";
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);
54
49
  }
55
- function hasTivra(letter) {
56
- return letter === "m";
57
- }
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.1";
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") {
@@ -642,42 +675,64 @@ function alignRows(rows) {
642
675
  }
643
676
  function resolveArcs(cells) {
644
677
  const arcs = [];
645
- let openStart;
678
+ let open;
646
679
  let sawEnds = false;
647
680
  const fillCells = [];
648
- const openAt = (i) => {
681
+ const openAt = (cell, note) => {
649
682
  sawEnds = true;
650
- if (openStart === void 0) openStart = i;
683
+ open ??= { cell, note };
651
684
  };
652
- const closeAt = (i) => {
685
+ const closeAt = (cell, note) => {
653
686
  sawEnds = true;
654
- if (openStart !== void 0) {
655
- arcs.push({ start: openStart, end: i, openLeft: false, openRight: false });
656
- openStart = void 0;
657
- } else {
658
- arcs.push({ start: 0, end: i, openLeft: true, openRight: false });
659
- }
687
+ const from = open ?? { cell: 0, note: 0 };
688
+ arcs.push({
689
+ start: from.cell,
690
+ startNote: from.note,
691
+ end: cell,
692
+ endNote: note,
693
+ openLeft: open === void 0,
694
+ openRight: false
695
+ });
696
+ open = void 0;
660
697
  };
661
698
  cells.forEach((cell, i) => {
662
- for (const note of cell.notes) {
663
- if (note.meend === "start") openAt(i);
664
- else if (note.meend === "end") closeAt(i);
665
- for (const b of note.below ?? []) {
666
- if (!isArcRaw(b.raw)) continue;
667
- for (const ch of b.raw) {
668
- if (ch === "w" || ch === "W") openAt(i);
669
- else if (ch === "x" || ch === "X") closeAt(i);
670
- else fillCells.push(i);
671
- }
672
- }
699
+ const pitched = cell.notes.flatMap((n, j) => isPitched(n) ? [j] : []);
700
+ const first = pitched[0] ?? 0;
701
+ const last = pitched[pitched.length - 1] ?? cell.notes.length - 1;
702
+ let marks = "";
703
+ cell.notes.forEach((note, j) => {
704
+ if (note.meend === "start") openAt(i, j);
705
+ else if (note.meend === "end") closeAt(i, j);
706
+ for (const b of note.below ?? []) if (isArcRaw(b.raw)) marks += b.raw;
707
+ });
708
+ const reopens = /[xX].*[wW]/.test(marks);
709
+ for (const ch of marks) {
710
+ if (ch === "w" || ch === "W") openAt(i, reopens ? last : first);
711
+ else if (ch === "x" || ch === "X") closeAt(i, reopens ? first : last);
712
+ else if (ch === "$") {
713
+ openAt(i, first);
714
+ closeAt(i, last);
715
+ } else fillCells.push(i);
673
716
  }
674
717
  });
675
- if (openStart !== void 0) {
676
- arcs.push({ start: openStart, end: Math.max(cells.length - 1, 0), openLeft: false, openRight: true });
718
+ const lastCell = Math.max(cells.length - 1, 0);
719
+ const lastNote = (c) => Math.max((cells[c]?.notes.length ?? 1) - 1, 0);
720
+ if (open !== void 0) {
721
+ arcs.push({
722
+ start: open.cell,
723
+ startNote: open.note,
724
+ end: lastCell,
725
+ endNote: lastNote(lastCell),
726
+ openLeft: false,
727
+ openRight: true
728
+ });
677
729
  } else if (!sawEnds && fillCells.length > 0) {
730
+ const end = Math.max(...fillCells);
678
731
  arcs.push({
679
732
  start: Math.min(...fillCells),
680
- end: Math.max(...fillCells),
733
+ startNote: 0,
734
+ end,
735
+ endNote: lastNote(end),
681
736
  openLeft: true,
682
737
  openRight: true,
683
738
  line: true
@@ -685,6 +740,35 @@ function resolveArcs(cells) {
685
740
  }
686
741
  return arcs;
687
742
  }
743
+ function arcInk(cells, arcs) {
744
+ const ink = { cells: cells.map(() => ""), notes: cells.map((c) => c.notes.map(() => "")) };
745
+ const mark = (c, j, cls, clip = false) => {
746
+ ink.notes[c][j] += ` ${cls}`;
747
+ if (clip && !ink.cells[c].includes(" sw-mk")) ink.cells[c] += " sw-mk";
748
+ };
749
+ for (const arc of arcs) {
750
+ const { start: s, end: e, startNote: j, endNote: k } = arc;
751
+ const hookL = arc.line !== true && !arc.openLeft;
752
+ const hookR = arc.line !== true && !arc.openRight;
753
+ if (s === e && hookL && hookR) {
754
+ if (j === k) {
755
+ mark(s, j, "sw-mos");
756
+ mark(s, j, "sw-mcs");
757
+ } else {
758
+ mark(s, j, "sw-mob");
759
+ for (let n = j + 1; n < k; n++) mark(s, n, "sw-mf");
760
+ mark(s, k, "sw-mcb");
761
+ }
762
+ continue;
763
+ }
764
+ for (let c = s; c <= e; c++) {
765
+ if (c === s && hookL) mark(c, j, "sw-mo", true);
766
+ else if (c === e && hookR) mark(c, k, "sw-mc", true);
767
+ else mark(c, 0, "sw-mm", true);
768
+ }
769
+ }
770
+ return ink;
771
+ }
688
772
  var SVARA_CHAR = /[srgmpqndtkuv]/;
689
773
  function rawMarks(raw, count) {
690
774
  if (raw === void 0) return void 0;
@@ -860,25 +944,9 @@ function renderNote(note, kind, opts, ctx) {
860
944
  const raw = rawDisplay(note);
861
945
  return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
862
946
  }
863
- function renderArc(arc, offset) {
864
- const open = arc.line === true;
865
- const openL = arc.openLeft || open;
866
- const openR = arc.openRight || open;
867
- const fill = '<i class="sw-arc-m"></i>';
868
- const hookL = openL ? "" : '<i class="sw-arc-l">w</i>';
869
- const hookR = openR ? "" : '<i class="sw-arc-r">x</i>';
870
- const piece = (col, cls, inner) => `<span class="sw-arc${cls}" style="grid-column:${col}">${inner}</span>`;
871
- const f = offset + arc.start + COL_BASE;
872
- const t = offset + arc.end + COL_BASE;
873
- const openCls = (openL ? " sw-arc--openl" : "") + (openR ? " sw-arc--openr" : "");
874
- if (t === f) return piece(`${f}`, openCls, hookL + fill + hookR);
875
- const head = piece(`${f}`, openL ? " sw-arc--openl" : " sw-arc--head", hookL + fill);
876
- const tail = piece(`${t}`, openR ? " sw-arc--openr" : " sw-arc--tail", fill + hookR);
877
- const mid = t > f + 1 ? piece(`${f + 1} / ${t}`, " sw-arc--mid", fill) : "";
878
- return head + mid + tail;
879
- }
880
947
  function renderRow(row, partIdx, rowIdx, offset, ctx) {
881
948
  const cells = groupRowIntoCells(row.notes);
949
+ const ink = arcInk(cells, resolveArcs(cells));
882
950
  const out = [];
883
951
  if (row.label !== void 0)
884
952
  out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
@@ -888,11 +956,10 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
888
956
  const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
889
957
  const notes = cell.notes.map((note, j) => {
890
958
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
891
- const cls = isPitched(note) ? " sw-p" : "";
892
959
  const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
893
- return `<span class="sw-n${cls}${bar}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
960
+ return `<span class="sw-n${bar}${ink.notes[ci][j]}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
894
961
  }).join("");
895
- out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
962
+ out.push(`<span class="sw-cell${barCls}${ink.cells[ci]}" style="grid-column:${col}">${notes}</span>`);
896
963
  const slices = cellLyricSlices(cell);
897
964
  if (slices !== void 0) {
898
965
  const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
@@ -905,7 +972,6 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
905
972
  if (below.length > 0)
906
973
  out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
907
974
  });
908
- for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
909
975
  return `<div class="sw-row">${out.join("")}</div>`;
910
976
  }
911
977
  function renderHeader(meta, ctx) {
@@ -936,15 +1002,25 @@ function renderToHtml(doc, opts = {}) {
936
1002
  out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
937
1003
  let n = 0;
938
1004
  let lay;
1005
+ let taal = doc.meta.taal;
1006
+ let named = !isTaalFerta(doc.meta.taalName);
939
1007
  doc.parts.forEach((part, p) => {
940
1008
  out.push('<section class="sw-part">');
941
1009
  if (part.name !== void 0)
942
1010
  out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
943
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
+ }
944
1019
  if (row.lay !== void 0 && row.lay !== lay) {
945
1020
  lay = row.lay;
946
- 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>`);
947
1022
  }
1023
+ if (marks.length > 0) out.push(`<div class="sw-change">${marks.join("")}</div>`);
948
1024
  out.push(renderRow(row, p, r, layout.offsets[n++], ctx));
949
1025
  });
950
1026
  out.push("</section>");
@@ -963,7 +1039,7 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
963
1039
  sheet.style.removeProperty("--sw-size");
964
1040
  sheet.style.removeProperty("--sw-tight");
965
1041
  const available = scroll.clientWidth;
966
- let natural = block.scrollWidth;
1042
+ let natural = block.offsetWidth;
967
1043
  if (available === 0 || natural === 0) continue;
968
1044
  if (natural > available && minSpacing < 1) {
969
1045
  let loose = minSpacing;
@@ -971,11 +1047,11 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
971
1047
  for (let i = 0; i < 6; i++) {
972
1048
  const mid = (loose + tight) / 2;
973
1049
  sheet.style.setProperty("--sw-tight", String(mid));
974
- if (block.scrollWidth <= available) loose = mid;
1050
+ if (block.offsetWidth <= available) loose = mid;
975
1051
  else tight = mid;
976
1052
  }
977
1053
  sheet.style.setProperty("--sw-tight", String(loose));
978
- natural = block.scrollWidth;
1054
+ natural = block.offsetWidth;
979
1055
  }
980
1056
  const base = parseFloat(getComputedStyle(sheet).fontSize);
981
1057
  if (!(base > 0)) continue;
@@ -1038,10 +1114,12 @@ ${FONT_FACES}
1038
1114
 
1039
1115
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
1040
1116
 
1041
- /* \u09B2\u09AF\u09BC marker: the line where the tempo tier turns over. A full-width rule
1042
- rather than a row label \u2014 the label track is what the notation columns are
1043
- measured against, and a six-em word in it would shove the whole sheet right. */
1044
- .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 {
1045
1123
  grid-column: 1 / -1;
1046
1124
  display: flex;
1047
1125
  align-items: center;
@@ -1055,12 +1133,14 @@ ${FONT_FACES}
1055
1133
  opacity: 0.75;
1056
1134
  break-after: avoid;
1057
1135
  }
1058
- .sw-lay::after {
1136
+ .sw-change::before,
1137
+ .sw-change::after {
1059
1138
  content: '';
1060
1139
  flex: 1;
1061
1140
  height: 1px;
1062
1141
  background: var(--sw-rule, rgba(128, 128, 128, 0.35));
1063
1142
  }
1143
+ .sw-taal + .sw-lay::before { content: '\xB7'; margin-right: 0.6em; }
1064
1144
 
1065
1145
  /* ---- block grid: ONE column set for the whole sheet ----
1066
1146
  * NLTR emits a song as a single html table, which is why its lines align
@@ -1073,31 +1153,45 @@ ${FONT_FACES}
1073
1153
  column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
1074
1154
  align-items: end;
1075
1155
  width: max-content;
1156
+ /* The last column's cell spacers overhang half a gap past the grid; clip
1157
+ that sideways so it neither counts as width to fit nor raises a scrollbar. */
1158
+ overflow-x: clip;
1076
1159
  }
1077
1160
  .sw-part { display: contents; }
1161
+ /* Four FIXED bands per row \u2014 above / notation / arc / lyric \u2014 so a beat label,
1162
+ * a \u0995\u09A3 or a \u09AE\u09C0\u09A1\u09BC never changes how far a line sits from its words: the
1163
+ * notation hugs its lyric and the next system starts a little further off,
1164
+ * as the printed \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 sets it. Content taller than its band overflows
1165
+ * rather than pushing the rhythm apart. */
1078
1166
  .sw-row {
1079
1167
  grid-column: 1 / -1;
1080
1168
  display: grid;
1081
1169
  grid-template-columns: subgrid;
1082
- grid-template-rows: auto auto auto auto;
1170
+ grid-template-rows: 0.95em 1.35em 0.4em 1.35em;
1083
1171
  align-items: end;
1084
- padding: 0.4em 0;
1172
+ padding-bottom: 0.35em;
1173
+ line-height: 1.35;
1085
1174
  }
1086
1175
  .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
1087
1176
 
1088
- /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
1089
- * flex's min-content floor, else a wide sub-note steals width from its
1090
- * neighbours while the syllable below it does not, and the two bands drift). */
1091
- .sw-cell, .sw-lyr { display: flex; justify-content: center; }
1092
- .sw-cell > .sw-n { white-space: nowrap; }
1093
- .sw-cell > .sw-n.sw-p,
1094
- .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
1095
- .sw-cell { grid-row: 2; }
1096
- .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
1097
- .sw-arc,
1098
- .sw-below { grid-row: 3; }
1099
- .sw-lyr { grid-row: 4; padding-top: 0.3em; }
1100
- .sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
1177
+ /* One column per cell, its notes set tight as one word (\u09B0\u09B8\u09BE) and centred by
1178
+ * two flex spacers. Each spacer overhangs half the column gap (a min-width
1179
+ * paid back by a negative margin, so the column is sized by the notes alone),
1180
+ * which lets neighbouring cells' spacers meet mid-gap: a \u09AE\u09C0\u09A1\u09BC crossing cells is
1181
+ * inked by these boxes with no hole between them. */
1182
+ .sw-cell, .sw-lyr { display: flex; justify-content: center; white-space: nowrap; }
1183
+ .sw-cell {
1184
+ --sw-half-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1) / 2);
1185
+ grid-row: 2;
1186
+ align-self: stretch;
1187
+ }
1188
+ .sw-cell::before, .sw-cell::after { content: ''; flex: 1 0 0; min-width: var(--sw-half-gap); }
1189
+ .sw-cell::before { margin-left: calc(var(--sw-half-gap) * -1); }
1190
+ .sw-cell::after { margin-right: calc(var(--sw-half-gap) * -1); }
1191
+ .sw-cell > .sw-n { position: relative; }
1192
+ .sw-above { grid-row: 1; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
1193
+ .sw-below { grid-row: 3; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
1194
+ .sw-lyr { grid-row: 4; }
1101
1195
 
1102
1196
  /* ---- notation clusters & marks ---- */
1103
1197
  .sw-cl { position: relative; display: inline-block; }
@@ -1118,12 +1212,16 @@ ${FONT_FACES}
1118
1212
  .sw-ati-taar::before { top: -0.3em; }
1119
1213
  .sw-ati-mandra::before { bottom: -0.35em; }
1120
1214
 
1121
- /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
1215
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it.
1216
+ * line-height 0 (here and on .sw-micro): a raised inline box taller than the
1217
+ * strut grows its line box and drops that one note's baseline below its
1218
+ * neighbours' (css2 \xA710.8). */
1122
1219
  .sw-kan,
1123
1220
  .sw-resh {
1124
1221
  position: relative;
1125
1222
  display: inline-block;
1126
1223
  font-size: 0.58em;
1224
+ line-height: 0;
1127
1225
  vertical-align: 0.75em;
1128
1226
  }
1129
1227
  .sw-kan { margin-right: 0.08em; }
@@ -1155,7 +1253,7 @@ ${FONT_FACES}
1155
1253
  .sw-cut::before { top: 0.2em; }
1156
1254
  .sw-cut::after { top: 0.55em; }
1157
1255
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
1158
- .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
1256
+ .sw-micro { font-size: 0.6em; line-height: 0; vertical-align: super; opacity: 0.8; }
1159
1257
 
1160
1258
  /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
1161
1259
  * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
@@ -1175,49 +1273,46 @@ ${FONT_FACES}
1175
1273
  /* a bar owns a whole column but needs no matra of width */
1176
1274
  .sw-bar-cell { min-width: 0; }
1177
1275
 
1178
- /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
1179
- * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
1180
- * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
1181
- * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
1182
- * spans any number of columns. An open side = the arc continues onto the
1183
- * next system: drop that hook and leave the fill squared off. */
1184
- .sw-arc {
1185
- display: flex;
1186
- align-items: baseline;
1187
- line-height: 1;
1188
- font-family: Swarabitan, var(--sw-font, sans-serif);
1189
- /* The 0.9em sits on the children, not here: the end pieces below size
1190
- themselves off the block's own column-gap, which is in the block's em. */
1191
- }
1192
- .sw-arc > i, .sw-arc::before { font-size: 0.9em; }
1193
- .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
1194
- /* Every piece needs the same baseline \u2014 the fill rides it, so a piece holding
1195
- only fill would otherwise drop to the bottom of the row. A zero-width copy
1196
- of the hook glyph is the strut (same font, same metrics). */
1197
- .sw-arc::before { content: 'w'; flex: none; width: 0; overflow: hidden; }
1198
- /* The ends hang from the two notes' CENTRES, as the print sets them, so the
1199
- arc is three grid items: each end piece takes half of its own column plus
1200
- the column-gap it has to bridge, and the negative margin on the same side
1201
- lets that bridge overhang the grid area instead of shifting the piece. */
1202
- .sw-arc--head, .sw-arc--tail {
1203
- --sw-arc-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
1204
- width: calc(50% + var(--sw-arc-gap));
1205
- }
1206
- .sw-arc--head { justify-self: end; margin-right: calc(var(--sw-arc-gap) * -1); }
1207
- .sw-arc--tail { justify-self: start; margin-left: calc(var(--sw-arc-gap) * -1);
1208
- justify-content: flex-end; }
1209
- /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
1210
- tall whose bottom sits 0.01em above the baseline. An empty inline-block
1211
- takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
1212
- exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
1213
- .sw-arc-m {
1214
- flex: 1 1 0;
1215
- min-width: 0;
1216
- align-self: baseline;
1217
- height: 0.11em;
1218
- margin-bottom: -0.01em;
1219
- background: currentColor;
1220
- }
1276
+ /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x/$ segments and spec meend) ----
1277
+ * Swarabitan's own arc, all langs: w is the opening hook, curling down from
1278
+ * its tip into a flat, x its mirror, y that flat alone \u2014 and they join
1279
+ * hole-free, so one run of text is a whole end (tools/align-check/glyph.html).
1280
+ * Each end sets its hook tip on its note's centre and runs the flat toward the
1281
+ * other end (render.ts arcInk): across cells it is cut at the mid-gap, where
1282
+ * the next cell's run carries on (.sw-mk clips a cell half a gap past each
1283
+ * side, as far as its spacers reach); inside one cell, at its note's edge. So
1284
+ * a long arc extends the flat and a short one trims it off. line-height 0 sets
1285
+ * every run's baseline the same distance below its note. The clip reaches
1286
+ * 0.5px further so two cells' anti-aliased edges overlap instead of leaving a
1287
+ * hairline seam between their identical flats. */
1288
+ .sw-mk { clip-path: inset(-3em calc(var(--sw-half-gap) * -1 - 0.5px)); }
1289
+ .sw-mo::after, .sw-mob::after, .sw-mos::after,
1290
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before,
1291
+ .sw-mf::before, .sw-mm::before {
1292
+ content: 'yyyyyyyyyyyy';
1293
+ position: absolute;
1294
+ top: calc(100% + 0.05em);
1295
+ font-family: Swarabitan;
1296
+ font-size: 0.9em;
1297
+ line-height: 0;
1298
+ white-space: pre;
1299
+ overflow-x: clip;
1300
+ }
1301
+ .sw-mo::after, .sw-mob::after, .sw-mos::after { content: 'wyyyyyyyyyyyy'; }
1302
+ /* rtl: a closing run cut short keeps its hook on its right edge */
1303
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before { content: 'yyyyyyyyyyyyx'; direction: rtl; }
1304
+ .sw-mo::after, .sw-mob::after { left: 50%; }
1305
+ .sw-mc::before, .sw-mcb::before { right: 50%; }
1306
+ .sw-mos::after { left: 0; }
1307
+ .sw-mcs::before { right: 0; }
1308
+ .sw-mf::before { left: 0; right: 0; }
1309
+ .sw-mm::before { left: -12em; }
1310
+ /* Inside one cell a hook gets half a note to curl in, about a third of its
1311
+ * curl: squeezed to half width (same stroke) it lands flat at the cut, so the
1312
+ * two flats meet level whatever the notes' widths \u2014 a small arc, not a V. */
1313
+ .sw-mob::after, .sw-mos::after, .sw-mcb::before, .sw-mcs::before { width: 100%; transform: scaleX(0.5); }
1314
+ .sw-mob::after, .sw-mos::after { transform-origin: left; }
1315
+ .sw-mcb::before, .sw-mcs::before { transform-origin: right; }
1221
1316
 
1222
1317
  /* ---- annotation content ---- */
1223
1318
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
@@ -1263,7 +1358,7 @@ var SWARALIPI_PRINT_CSS = `
1263
1358
  .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
1264
1359
  .sw-scroll { overflow: visible !important; }
1265
1360
  .sw-row { break-inside: avoid; page-break-inside: avoid; }
1266
- .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; }
1267
1362
  .sw-hl { background: none !important; }
1268
1363
  /* Keep each engraved staff system whole across a page break (the abcjs line
1269
1364
  is a <div> wrapping the <svg>, so the break rule must sit on the div). */
@@ -1439,11 +1534,15 @@ function eventPaths(schedule) {
1439
1534
  }
1440
1535
  return paths;
1441
1536
  }
1537
+ var barLen = (m) => m.beatsPerBar > 0 ? m.beatsPerBar : 4;
1442
1538
  function abcText(s) {
1443
1539
  return s.replace(/[\r\n]+/g, " ").trim();
1444
1540
  }
1445
1541
  function buildAbc(schedule, lyricOf, opts) {
1446
- 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]);
1447
1546
  const tokens = [];
1448
1547
  const words = [];
1449
1548
  let barPos = 0;
@@ -1457,6 +1556,17 @@ function buildAbc(schedule, lyricOf, opts) {
1457
1556
  }
1458
1557
  };
1459
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
+ }
1460
1570
  if (ev.midi === null) {
1461
1571
  if (ev.kind === "rest") {
1462
1572
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
@@ -1475,16 +1585,16 @@ function buildAbc(schedule, lyricOf, opts) {
1475
1585
  });
1476
1586
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1477
1587
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1478
- const meter = ratioMeter(beatsPerBar);
1588
+ const meter = ratioMeter(barLen(meters[0]));
1479
1589
  const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
1480
- if (opts.drums) head.push(...percMapLines(opts.drums));
1590
+ if (withDrums) head.push(...percMapLines(meters.flatMap((x) => x.drums ?? [])));
1481
1591
  head.push("K:C treble");
1482
1592
  const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
1483
1593
  const lines = [...head];
1484
- if (opts.drums) {
1594
+ if (withDrums) {
1485
1595
  lines.push("V:1", melodyBody);
1486
1596
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1487
- 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));
1488
1598
  } else {
1489
1599
  lines.push(melodyBody);
1490
1600
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
@@ -1513,13 +1623,21 @@ function percMapLines(voices) {
1513
1623
  }
1514
1624
  return lines;
1515
1625
  }
1516
- function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
1517
- const n = Math.max(1, Math.ceil(totalBeats));
1626
+ function buildDrumVoice(totalBeats, meters) {
1518
1627
  const tokens = [];
1519
- for (let beat = 0; beat < n; beat++) {
1520
- tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
1521
- if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
1522
- }
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
+ });
1523
1641
  if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
1524
1642
  else tokens[tokens.length - 1] = "|]";
1525
1643
  return tokens.join(" ");
@@ -1530,16 +1648,25 @@ function lyricLookup(doc) {
1530
1648
  return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
1531
1649
  };
1532
1650
  }
1533
- function resolveDrumTaal(doc) {
1534
- const id = doc.meta.taal?.id;
1651
+ function resolveDrumTaal(taal) {
1652
+ const id = taal?.id;
1535
1653
  return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
1536
1654
  }
1537
1655
  function docToAbc(doc, opts = {}) {
1538
1656
  const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave, lay: false });
1539
1657
  const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1540
- const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
1541
- const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
1542
- 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
+ });
1543
1670
  }
1544
1671
  async function abcToSvg(abc, opts = {}, notePaths) {
1545
1672
  if (typeof document === "undefined") {
@@ -1618,7 +1745,7 @@ function buildTaalAbc(taal, opts = {}) {
1618
1745
  const voices = Array.from({ length: n }, (_, i) => src[i % src.length]);
1619
1746
  const bpm = Math.round(opts.bpm ?? 90);
1620
1747
  const head = ["X:1", `M:${ratioMeter(n)}`, "L:1/4", `Q:1/4=${bpm}`, ...percMapLines(voices), "K:C"];
1621
- 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 }])];
1622
1749
  if (opts.bols?.length) {
1623
1750
  const w = opts.bols.slice(0, n).map((b) => b.trim() ? abcText(b).replace(/\s+/g, "~") : "*");
1624
1751
  while (w.length < n) w.push("*");
@@ -2009,6 +2136,7 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
2009
2136
  exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
2010
2137
  exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
2011
2138
  exports.DIGITS = DIGITS;
2139
+ exports.FREE_TAAL_LABELS = FREE_TAAL_LABELS;
2012
2140
  exports.LABELS = LABELS;
2013
2141
  exports.LAY_LABELS = LAY_LABELS;
2014
2142
  exports.MARKS = MARKS;