@ugfoundation/swaralipi-js 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +7 -1
  2. package/dist/{batch-RAM2S4QO.js → batch-BS2AIZOR.js} +6 -5
  3. package/dist/batch-BS2AIZOR.js.map +1 -0
  4. package/dist/{chunk-3EWAKXLM.js → chunk-3ZBNANDF.js} +3 -3
  5. package/dist/{chunk-3EWAKXLM.js.map → chunk-3ZBNANDF.js.map} +1 -1
  6. package/dist/{chunk-LRIAUFKF.js → chunk-5N6HGPFH.js} +8 -168
  7. package/dist/chunk-5N6HGPFH.js.map +1 -0
  8. package/dist/chunk-BSZ3WQUP.js +164 -0
  9. package/dist/chunk-BSZ3WQUP.js.map +1 -0
  10. package/dist/{chunk-SR7NFEGL.js → chunk-KOHSKHXI.js} +5 -3
  11. package/dist/chunk-KOHSKHXI.js.map +1 -0
  12. package/dist/{chunk-CF3JWIHP.js → chunk-ZNJTMQIX.js} +228 -64
  13. package/dist/chunk-ZNJTMQIX.js.map +1 -0
  14. package/dist/cli.cjs +6 -4
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +3 -3
  17. package/dist/convert/index.cjs +6 -4
  18. package/dist/convert/index.cjs.map +1 -1
  19. package/dist/convert/index.js +4 -3
  20. package/dist/index.cjs +4 -2
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/{lay-DypMIIuf.d.ts → lay-CWlEhImP.d.ts} +8 -1
  26. package/dist/{lay-CvZeVon3.d.cts → lay-CXhC-HRO.d.cts} +8 -1
  27. package/dist/player/index.cjs +473 -123
  28. package/dist/player/index.cjs.map +1 -1
  29. package/dist/player/index.d.cts +14 -4
  30. package/dist/player/index.d.ts +14 -4
  31. package/dist/player/index.js +87 -62
  32. package/dist/player/index.js.map +1 -1
  33. package/dist/{schedule-DjcqM6A4.d.ts → schedule-ClyON98C.d.ts} +23 -11
  34. package/dist/{schedule-C4sP7uFu.d.cts → schedule-V-fgepvP.d.cts} +23 -11
  35. package/dist/spec/index.cjs +4 -2
  36. package/dist/spec/index.cjs.map +1 -1
  37. package/dist/spec/index.d.cts +2 -2
  38. package/dist/spec/index.d.ts +2 -2
  39. package/dist/spec/index.js +2 -2
  40. package/dist/viewer/index.cjs +369 -62
  41. package/dist/viewer/index.cjs.map +1 -1
  42. package/dist/viewer/index.d.cts +2 -2
  43. package/dist/viewer/index.d.ts +2 -2
  44. package/dist/viewer/index.js +3 -2
  45. package/dist/viewer/index.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/batch-RAM2S4QO.js.map +0 -1
  48. package/dist/chunk-CF3JWIHP.js.map +0 -1
  49. package/dist/chunk-LRIAUFKF.js.map +0 -1
  50. package/dist/chunk-SR7NFEGL.js.map +0 -1
@@ -70,8 +70,10 @@ var init_taals = __esm({
70
70
  // ঝম্পক
71
71
  dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] },
72
72
  // ধামার
73
- rupakra: { id: "rupakra", numBeats: 8, bhaags: [3, 2, 3], sam: 0 }
73
+ rupakra: { id: "rupakra", numBeats: 8, bhaags: [3, 2, 3], sam: 0 },
74
74
  // রূপকড়া
75
+ khemta: { id: "khemta", numBeats: 12, bhaags: [3, 3, 3, 3], sam: 0, khaali: [6] }
76
+ // খেমটা
75
77
  };
76
78
  TAAL_FERTA = "\u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE";
77
79
  }
@@ -284,7 +286,7 @@ var init_spec = __esm({
284
286
  init_grace();
285
287
  init_lay();
286
288
  init_svara();
287
- SPEC_VERSION = "0.11.1";
289
+ SPEC_VERSION = "0.13.0";
288
290
  }
289
291
  });
290
292
 
@@ -366,9 +368,305 @@ function sampleFileName(midi) {
366
368
  return midiToSampleName(midi).toLowerCase().replace("#", "s");
367
369
  }
368
370
 
369
- // src/player/schedule.ts
371
+ // src/convert/codec.ts
372
+ var LETTERS = {
373
+ s: { letter: "s" },
374
+ r: { letter: "r" },
375
+ g: { letter: "g" },
376
+ m: { letter: "m" },
377
+ p: { letter: "p" },
378
+ q: { letter: "d" },
379
+ // ধ — shuddha dha
380
+ n: { letter: "n" },
381
+ d: { letter: "d", komal: true },
382
+ // দ
383
+ t: { letter: "g", komal: true },
384
+ // জ্ঞ
385
+ k: { letter: "m", tivra: true },
386
+ // ক্ষ
387
+ u: { letter: "n", komal: true },
388
+ // ণ
389
+ v: { letter: "r", komal: true }
390
+ // ঋ
391
+ };
392
+ var STRUCTURALS = {
393
+ "%": () => ({ svara: ["madhyam", "s"] }),
394
+ // precomposed সা
395
+ "&": () => ({ svara: ["madhyam", "n"] })
396
+ // precomposed নি
397
+ };
398
+ var BAR_RUN_RE = /^(-)?([lLA[\]]+)$/;
399
+ function decodeBarRun(body) {
400
+ const m = body.match(BAR_RUN_RE);
401
+ if (!m || !/[lLA]/.test(m[2])) return void 0;
402
+ const notes = [];
403
+ if (m[1]) notes.push({ sustain: true });
404
+ const run = m[2];
405
+ let i = 0;
406
+ while (i < run.length) {
407
+ const ch = run[i];
408
+ if (ch === "l" || ch === "L") {
409
+ let j = i;
410
+ while (j < run.length && run[j] === ch) j++;
411
+ const strokes = (j - i) * (ch === "L" ? 2 : 1);
412
+ notes.push({ bar: strokes >= 4 ? "quad" : strokes >= 2 ? "double" : "single" });
413
+ i = j;
414
+ } else if (ch === "A") {
415
+ notes.push({ bar: "dari" });
416
+ i++;
417
+ } else {
418
+ notes.push({ brackets: [{ kind: "alt", edge: ch === "[" ? "open" : "close" }] });
419
+ i++;
420
+ }
421
+ }
422
+ return notes;
423
+ }
424
+ var KNOWN_RAW_RE = [
425
+ /^[wxyjEJ$<>=_]+$/,
426
+ // arc & line drawing segments
427
+ /^\|+$/,
428
+ // thin yugal-dari stop mark
429
+ /^[0-9!@#]{1,2}[fF]?:?$/,
430
+ // beat-number labels incl. 10–16 (sam tick = f/F, e.g. 1f / 1F / 2:)
431
+ /^[oOe`+,.;:iI~^*bcBCWXYFHZz'"?]$/
432
+ // dots, ticks, duration marks, legacy forms
433
+ ];
434
+ var OPEN_BRACKETS = {
435
+ "{": "repeat",
436
+ "(": "skip",
437
+ "[": "alt"
438
+ };
439
+ var CLOSE_BRACKETS = {
440
+ "}": "repeat",
441
+ ")": "skip",
442
+ "]": "alt"
443
+ };
444
+ function octaveOf(mark) {
445
+ if (mark === "f" || mark === "F") return "taar";
446
+ if (mark === "h" || mark === "H") return "mandra";
447
+ return "madhyam";
448
+ }
449
+ var BODY_RE = /^([0-9!@][fF]?)?([zj]*)(-)?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?((?:[srgmpqndtkuv][fh]?[i:]?)+)?(a)?([fh])?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?([0-9!@][fF]?)?(-)?$/;
450
+ function decodeChunk(chunk) {
451
+ const opens = [];
452
+ const closes = [];
453
+ let body = chunk;
454
+ while (body.length > 0) {
455
+ const kind = OPEN_BRACKETS[body[0]];
456
+ if (kind === void 0) break;
457
+ opens.push(kind);
458
+ body = body.slice(1);
459
+ }
460
+ while (body.length > 0) {
461
+ const kind = CLOSE_BRACKETS[body[body.length - 1]];
462
+ if (kind === void 0) break;
463
+ closes.unshift(kind);
464
+ body = body.slice(0, -1);
465
+ }
466
+ const finish = (notes, kind) => {
467
+ if (notes.length === 0) notes.push({});
468
+ const first = notes[0];
469
+ const last = notes[notes.length - 1];
470
+ if (opens.length > 0) first.brackets = [...opens.map((kind_) => ({ kind: kind_, edge: "open" })), ...first.brackets ?? []];
471
+ if (closes.length > 0) last.brackets = [...last.brackets ?? [], ...closes.map((kind_) => ({ kind: kind_, edge: "close" }))];
472
+ return { notes, kind };
473
+ };
474
+ if (body === "") return finish([{}], "decoded");
475
+ const barRun = decodeBarRun(body);
476
+ if (barRun) return finish(barRun, "decoded");
477
+ const structural = STRUCTURALS[body];
478
+ if (structural) return finish([structural()], "decoded");
479
+ if (KNOWN_RAW_RE.some((re) => re.test(body))) return finish([{}], "known");
480
+ const m = body.match(BODY_RE);
481
+ if (m) {
482
+ const [, , , hyphen, , kanLetter, kanOct, unitsStr, aakar, postOct, , trailKan, trailKanOct] = m;
483
+ const attachKan = (note, letter, oct) => {
484
+ const def = LETTERS[letter.toLowerCase()];
485
+ note.kan = [octaveOf(oct?.toLowerCase()), def.letter];
486
+ };
487
+ if (unitsStr) {
488
+ const units = unitsStr.match(/[srgmpqndtkuv][fh]?/g) ?? [];
489
+ const notes = units.map((unit) => {
490
+ const def = LETTERS[unit[0]];
491
+ const note = { svara: [octaveOf(unit[1]), def.letter] };
492
+ if (def.komal) note.komal = true;
493
+ if (def.tivra) note.tivra = true;
494
+ if (units.length > 1) note.durationMatra = 1 / units.length;
495
+ return note;
496
+ });
497
+ const last = notes[notes.length - 1];
498
+ if (postOct && last.svara[0] === "madhyam") {
499
+ last.svara = [octaveOf(postOct), last.svara[1]];
500
+ }
501
+ if (hyphen) notes[0].sustain = true;
502
+ if (kanLetter) attachKan(notes[0], kanLetter, kanOct);
503
+ if (trailKan && last.kan === void 0) attachKan(last, trailKan, trailKanOct);
504
+ return finish(notes, "decoded");
505
+ }
506
+ if (aakar) {
507
+ const note = hyphen ? { sustain: true } : { rest: true };
508
+ if (trailKan) attachKan(note, trailKan, trailKanOct);
509
+ else if (kanLetter) attachKan(note, kanLetter, kanOct);
510
+ return finish([note], "decoded");
511
+ }
512
+ if (hyphen && !kanLetter && !trailKan) {
513
+ return finish([{ sustain: true }], "decoded");
514
+ }
515
+ }
516
+ return finish([{}], "unknown");
517
+ }
518
+ function decodeCell(text) {
519
+ const chunks = text.split(/\s+/).filter((c) => c.length > 0);
520
+ const out = { notes: [], unknownTokens: [], knownRawTokens: [], decoded: false };
521
+ for (const chunk of chunks) {
522
+ const { notes, kind } = decodeChunk(chunk);
523
+ notes[0].raw = chunk;
524
+ if (kind === "decoded") out.decoded = true;
525
+ else if (kind === "known") out.knownRawTokens.push(chunk);
526
+ else out.unknownTokens.push(chunk);
527
+ out.notes.push(...notes);
528
+ }
529
+ return out;
530
+ }
531
+
532
+ // src/player/repeats.ts
370
533
  var UNDECODED_CELL = /[srgmpqnvtduka]/i;
371
534
  var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
535
+ var STOP_MARK = /^\|+$/;
536
+ var EPS = 1e-6;
537
+ var edges = (n, kind, edge) => n.brackets?.filter((b) => b.kind === kind && b.edge === edge).length ?? 0;
538
+ var sounds = (n) => n.svara !== void 0 || n.rest === true || n.sustain === true;
539
+ var matra = (notes) => notes.reduce((sum, n) => sum + (n.durationMatra ?? 1), 0);
540
+ function playOrder(doc) {
541
+ const tokens = [];
542
+ doc.parts.forEach(
543
+ (part, p) => part.rows.forEach(
544
+ (row, r) => row.notes.forEach((note, i) => tokens.push({ p, r, i, note, timed: note.bar === void 0 && !isSilent(note) }))
545
+ )
546
+ );
547
+ const { asthayi, stop, returns } = form(tokens);
548
+ const changed = changedNotes(tokens);
549
+ const out = [];
550
+ const sing = (lo, hi, useChanged) => {
551
+ const repeats = [];
552
+ let owed = 0;
553
+ for (let k = lo; k < hi; ) {
554
+ const t = tokens[k];
555
+ if (edges(t.note, "repeat", "open") > 0 && repeats.at(-1)?.open !== k) repeats.push({ open: k, again: false });
556
+ const again = repeats.at(-1)?.again === true;
557
+ const skipTo = again && edges(t.note, "skip", "open") > 0 ? skipEnd(tokens, k, hi) : -1;
558
+ if (skipTo >= 0) {
559
+ if (edges(tokens[skipTo].note, "repeat", "close") > 0) repeats.pop();
560
+ k = skipTo + 1;
561
+ owed = 0;
562
+ continue;
563
+ }
564
+ if (owed > EPS && t.timed) owed -= matra([t.note]);
565
+ else {
566
+ const sub = useChanged || again ? changed.get(k) : void 0;
567
+ const fits = sub !== void 0 && matra(sub) >= matra([t.note]) - EPS;
568
+ for (const note of fits ? sub : [t.note]) out.push({ p: t.p, r: t.r, i: t.i, note });
569
+ owed = fits ? matra(sub) - matra([t.note]) : 0;
570
+ }
571
+ const loop = edges(t.note, "repeat", "close") > 0 ? repeats.at(-1) : void 0;
572
+ if (loop !== void 0 && !loop.again) {
573
+ loop.again = true;
574
+ k = loop.open;
575
+ owed = 0;
576
+ continue;
577
+ }
578
+ if (loop !== void 0) repeats.pop();
579
+ k++;
580
+ }
581
+ };
582
+ let from = 0;
583
+ for (const ret of returns) {
584
+ sing(from, ret.resume, false);
585
+ sing(asthayi, stop + 1, ret.changed);
586
+ from = ret.resume;
587
+ }
588
+ sing(from, tokens.length, false);
589
+ return out;
590
+ }
591
+ function form(tokens) {
592
+ const first = /* @__PURE__ */ new Map();
593
+ const last = /* @__PURE__ */ new Map();
594
+ tokens.forEach((t, k) => {
595
+ if (!t.timed) return;
596
+ const row = `${t.p}.${t.r}`;
597
+ if (!first.has(row)) first.set(row, k);
598
+ last.set(row, k);
599
+ });
600
+ const opens = [];
601
+ let opening = false;
602
+ tokens.forEach((t, k) => {
603
+ if (t.note.bar !== "double") return;
604
+ const row = `${t.p}.${t.r}`;
605
+ const before = (first.get(row) ?? Infinity) < k;
606
+ const after = (last.get(row) ?? -1) > k;
607
+ opening = before !== after ? after : !opening;
608
+ if (opening) opens.push(k);
609
+ });
610
+ const returns = [];
611
+ for (let k = 0; k < tokens.length; k++) {
612
+ let at = -1;
613
+ let open = false;
614
+ let changed = false;
615
+ let end = false;
616
+ for (; k < tokens.length && !tokens[k].timed; k++) {
617
+ const n = tokens[k].note;
618
+ if (n.bar === "quad") end = true;
619
+ if (n.bar === "double" && !opens.includes(k)) at = k;
620
+ if (edges(n, "alt", "open") > 0) open = true;
621
+ if (open && edges(n, "alt", "close") > 0) {
622
+ at = k;
623
+ changed = true;
624
+ }
625
+ }
626
+ if (at >= 0 && !end) returns.push({ resume: at + 1, changed });
627
+ }
628
+ const top = opens[0];
629
+ const asthayi = top !== void 0 && top < (returns[0]?.resume ?? Infinity) ? top : 0;
630
+ const asthayiEnd = Math.min(returns[0]?.resume ?? Infinity, opens.find((k) => k > asthayi) ?? Infinity, tokens.length);
631
+ const lastSung = tokens.reduce((last2, t, k) => t.timed ? k : last2, -1);
632
+ const stop = tokens.findIndex(
633
+ (t, k) => k >= asthayi && k < asthayiEnd && k < lastSung && t.note.above?.some((a) => STOP_MARK.test(a.raw ?? "")) === true
634
+ );
635
+ if (stop < 0) return { asthayi, stop, returns: [] };
636
+ const atClose = returns[0]?.resume === asthayiEnd && !tokens.slice(stop + 1, asthayiEnd).some((t) => t.timed);
637
+ return { asthayi, stop, returns: atClose ? returns.slice(1) : returns };
638
+ }
639
+ function changedNotes(tokens) {
640
+ const out = /* @__PURE__ */ new Map();
641
+ let row = "";
642
+ let inside = false;
643
+ tokens.forEach((t, k) => {
644
+ if (row !== `${t.p}.${t.r}`) {
645
+ row = `${t.p}.${t.r}`;
646
+ inside = false;
647
+ }
648
+ const notes = [];
649
+ for (const above of t.note.above ?? []) {
650
+ const read = above.raw !== void 0 && !sounds(above) && above.brackets === void 0;
651
+ for (const a of read ? decodeCell(above.raw).notes : [above]) {
652
+ if (edges(a, "alt", "open") > 0) inside = true;
653
+ if (inside && sounds(a)) notes.push(a);
654
+ if (edges(a, "alt", "close") > 0) inside = false;
655
+ }
656
+ }
657
+ if (notes.length > 0) out.set(k, notes);
658
+ });
659
+ return out;
660
+ }
661
+ function skipEnd(tokens, k, hi) {
662
+ for (let depth = 0, j = k; j < hi; j++) {
663
+ depth += edges(tokens[j].note, "skip", "open") - edges(tokens[j].note, "skip", "close");
664
+ if (depth <= 0) return j;
665
+ }
666
+ return -1;
667
+ }
668
+
669
+ // src/player/schedule.ts
372
670
  var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
373
671
  var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
374
672
  var MEEND_MARKS = { start: "w", end: "x", both: "xw" };
@@ -412,67 +710,76 @@ function buildSchedule(doc, opts = {}) {
412
710
  let cursor = 0;
413
711
  let lastPitched = -1;
414
712
  let samMatra;
415
- let matra = 0;
713
+ let matra2 = 0;
416
714
  const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
417
715
  const withLay = opts.lay !== false;
418
716
  const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
717
+ const rows = /* @__PURE__ */ new Map();
419
718
  let scale = 1;
719
+ let layNow = "drut";
420
720
  let taal = doc.meta.taal;
421
- doc.parts.forEach((part, p) => {
422
- part.rows.forEach((row, r) => {
721
+ doc.parts.forEach(
722
+ (part, p) => part.rows.forEach((row, r) => {
423
723
  const lay = withLay ? rowLay(row) : void 0;
424
- const layChanged = lay !== void 0 && speeds[lay] !== scale;
425
- const nextTaal = row.taal !== void 0 ? row.taal : taal;
426
- if (layChanged || !sameTaal(nextTaal, taal)) {
427
- if (layChanged) scale = speeds[lay];
428
- taal = nextTaal;
429
- const last = tempo[tempo.length - 1];
430
- const seg = { beat: cursor, endBeat: cursor, scale, lay: layChanged ? lay : last.lay, taal };
724
+ if (lay !== void 0 && speeds[lay] !== scale) {
725
+ scale = speeds[lay];
726
+ layNow = lay;
727
+ }
728
+ if (row.taal !== void 0) taal = row.taal;
729
+ rows.set(`${p}.${r}`, { scale, lay: layNow, taal, glides: meendContinuations(row.notes) });
730
+ })
731
+ );
732
+ const visits = opts.repeats === true ? playOrder(doc) : doc.parts.flatMap((part, p) => part.rows.flatMap((row, r) => row.notes.map((note, i) => ({ p, r, i, note }))));
733
+ let rowNow = "";
734
+ for (const { p, r, i, note } of visits) {
735
+ const row = rows.get(`${p}.${r}`);
736
+ if (rowNow !== `${p}.${r}`) {
737
+ rowNow = `${p}.${r}`;
738
+ const last = tempo[tempo.length - 1];
739
+ if (row.scale !== last.scale || !sameTaal(row.taal, last.taal)) {
740
+ const seg = { beat: cursor, endBeat: cursor, scale: row.scale, lay: row.lay, taal: row.taal };
431
741
  if (last.beat === cursor) tempo[tempo.length - 1] = seg;
432
742
  else {
433
743
  last.endBeat = cursor;
434
744
  tempo.push(seg);
435
745
  }
436
746
  }
437
- const glides = meendContinuations(row.notes);
438
- row.notes.forEach((note, i) => {
439
- if (samMatra === void 0 && note.above !== void 0) {
440
- for (const a of note.above) {
441
- if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
442
- samMatra = matra;
443
- break;
444
- }
445
- }
446
- }
447
- if (note.bar !== void 0 || isSilent(note)) return;
448
- const path = `${p}.${r}.${i}`;
449
- const written = note.durationMatra ?? 1;
450
- const dur = written / scale;
451
- const kan = graceMidis(note.kan, tonicPc, baseOctave);
452
- const resh = graceMidis(note.resh, tonicPc, baseOctave);
453
- if (note.svara !== void 0) {
454
- events.push({
455
- beat: cursor,
456
- durationBeats: dur,
457
- midi: svaraToMidi(note, tonicPc, baseOctave),
458
- kan,
459
- resh,
460
- path,
461
- kind: note.sustain === true ? "sustain" : "note",
462
- ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
463
- });
464
- lastPitched = events.length - 1;
465
- } else if (note.sustain === true) {
466
- if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
467
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
468
- } else {
469
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
747
+ }
748
+ if (samMatra === void 0 && note.above !== void 0) {
749
+ for (const a of note.above) {
750
+ if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
751
+ samMatra = matra2;
752
+ break;
470
753
  }
471
- cursor += dur;
472
- matra += written;
754
+ }
755
+ }
756
+ if (note.bar !== void 0 || isSilent(note)) continue;
757
+ const path = `${p}.${r}.${i}`;
758
+ const written = note.durationMatra ?? 1;
759
+ const dur = written / row.scale;
760
+ const kan = graceMidis(note.kan, tonicPc, baseOctave);
761
+ const resh = graceMidis(note.resh, tonicPc, baseOctave);
762
+ if (note.svara !== void 0) {
763
+ events.push({
764
+ beat: cursor,
765
+ durationBeats: dur,
766
+ midi: svaraToMidi(note, tonicPc, baseOctave),
767
+ kan,
768
+ resh,
769
+ path,
770
+ kind: note.sustain === true ? "sustain" : "note",
771
+ ...row.glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
473
772
  });
474
- });
475
- });
773
+ lastPitched = events.length - 1;
774
+ } else if (note.sustain === true) {
775
+ if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
776
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
777
+ } else {
778
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
779
+ }
780
+ cursor += dur;
781
+ matra2 += written;
782
+ }
476
783
  const numBeats = tempo[0].taal?.numBeats;
477
784
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
478
785
  tempo[tempo.length - 1].endBeat = cursor;
@@ -564,8 +871,8 @@ function localizeDigits(s, lang) {
564
871
  }
565
872
 
566
873
  // src/viewer/render.ts
567
- var EPS = 1e-6;
568
- var isIntegral = (x) => Math.abs(x - Math.round(x)) < EPS;
874
+ var EPS2 = 1e-6;
875
+ var isIntegral = (x) => Math.abs(x - Math.round(x)) < EPS2;
569
876
  function escapeHtml(s) {
570
877
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
571
878
  }
@@ -606,7 +913,7 @@ function groupRowIntoCells(notes) {
606
913
  cur ??= { kind: "group", start: i, notes: [] };
607
914
  cur.notes.push(note);
608
915
  acc += d;
609
- if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
916
+ if (note.halfMatra === true || acc > EPS2 && isIntegral(acc)) close();
610
917
  });
611
918
  close();
612
919
  return mergeStructural(absorbBrackets(cells));
@@ -629,15 +936,15 @@ function absorbBrackets(cells) {
629
936
  const out = [];
630
937
  for (let i = 0; i < cells.length; i++) {
631
938
  const cell = cells[i];
632
- const edges = bracketEdges(cell);
633
- if (edges !== void 0 && edges.open !== edges.close) {
939
+ const edges2 = bracketEdges(cell);
940
+ if (edges2 !== void 0 && edges2.open !== edges2.close) {
634
941
  const prev = out[out.length - 1];
635
942
  const next = cells[i + 1];
636
- if (edges.close && prev?.kind === "group") {
943
+ if (edges2.close && prev?.kind === "group") {
637
944
  prev.notes = [...prev.notes, ...cell.notes];
638
945
  continue;
639
946
  }
640
- if (edges.open && next?.kind === "group") {
947
+ if (edges2.open && next?.kind === "group") {
641
948
  next.notes = [...cell.notes, ...next.notes];
642
949
  next.start = cell.start;
643
950
  continue;
@@ -838,7 +1145,7 @@ function cellMatra(cell) {
838
1145
  const pitched = cell.notes.filter(isPitched);
839
1146
  const total = pitched.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
840
1147
  const half = pitched.some((n) => n.halfMatra === true);
841
- const whole = Math.floor(total + EPS);
1148
+ const whole = Math.floor(total + EPS2);
842
1149
  return { matras: half ? whole : Math.max(1, whole), half };
843
1150
  }
844
1151
  function cellPitchedOpts(cell) {
@@ -1604,19 +1911,19 @@ function buildAbc(schedule, lyricOf, opts) {
1604
1911
  const tokens = [];
1605
1912
  const words = [];
1606
1913
  let barPos = 0;
1607
- const EPS2 = 1e-6;
1914
+ const EPS3 = 1e-6;
1608
1915
  const advance = (dur) => {
1609
1916
  barPos += dur;
1610
- while (barPos >= beatsPerBar - EPS2) {
1917
+ while (barPos >= beatsPerBar - EPS3) {
1611
1918
  tokens.push("|");
1612
1919
  words.push("|");
1613
1920
  barPos -= beatsPerBar;
1614
1921
  }
1615
1922
  };
1616
1923
  schedule.events.forEach((ev, i) => {
1617
- while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS2) {
1924
+ while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS3) {
1618
1925
  m++;
1619
- if (barPos > EPS2) {
1926
+ if (barPos > EPS3) {
1620
1927
  tokens.push("|");
1621
1928
  words.push("|");
1622
1929
  }