@ugfoundation/swaralipi-js 0.5.0 → 0.6.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 (46) hide show
  1. package/dist/{batch-UETOBPHT.js → batch-E76HM4GM.js} +4 -4
  2. package/dist/{batch-UETOBPHT.js.map → batch-E76HM4GM.js.map} +1 -1
  3. package/dist/{chunk-UD2K4UIC.js → chunk-52EPQY47.js} +8 -6
  4. package/dist/chunk-52EPQY47.js.map +1 -0
  5. package/dist/{chunk-C6TRCMPL.js → chunk-GHWURC4U.js} +97 -5
  6. package/dist/chunk-GHWURC4U.js.map +1 -0
  7. package/dist/{chunk-ID44OP2N.js → chunk-MSVF43WQ.js} +7 -6
  8. package/dist/chunk-MSVF43WQ.js.map +1 -0
  9. package/dist/cli.cjs +11 -8
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2 -2
  12. package/dist/convert/index.cjs +10 -7
  13. package/dist/convert/index.cjs.map +1 -1
  14. package/dist/convert/index.d.cts +4 -3
  15. package/dist/convert/index.d.ts +4 -3
  16. package/dist/convert/index.js +2 -2
  17. package/dist/index.cjs +6 -4
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/player/index.cjs +212 -20
  23. package/dist/player/index.cjs.map +1 -1
  24. package/dist/player/index.d.cts +16 -4
  25. package/dist/player/index.d.ts +16 -4
  26. package/dist/player/index.js +111 -20
  27. package/dist/player/index.js.map +1 -1
  28. package/dist/{schedule-BaYIJf8y.d.ts → schedule-8WFUBJqB.d.ts} +78 -3
  29. package/dist/{schedule-bBvy18MO.d.cts → schedule-CjPcD-Hf.d.cts} +78 -3
  30. package/dist/{schema-D3wyNRqM.d.ts → schema-D_86x4oV.d.cts} +21 -6
  31. package/dist/{schema-D3wyNRqM.d.cts → schema-D_86x4oV.d.ts} +21 -6
  32. package/dist/spec/index.cjs +6 -4
  33. package/dist/spec/index.cjs.map +1 -1
  34. package/dist/spec/index.d.cts +3 -3
  35. package/dist/spec/index.d.ts +3 -3
  36. package/dist/spec/index.js +1 -1
  37. package/dist/viewer/index.cjs +264 -166
  38. package/dist/viewer/index.cjs.map +1 -1
  39. package/dist/viewer/index.d.cts +16 -23
  40. package/dist/viewer/index.d.ts +16 -23
  41. package/dist/viewer/index.js +99 -41
  42. package/dist/viewer/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-C6TRCMPL.js.map +0 -1
  45. package/dist/chunk-ID44OP2N.js.map +0 -1
  46. package/dist/chunk-UD2K4UIC.js.map +0 -1
@@ -72,7 +72,9 @@ var init_schema = __esm({
72
72
  sustain: zod.z.boolean().optional(),
73
73
  rest: zod.z.boolean().optional(),
74
74
  kan: svaraSchema.optional(),
75
+ resh: svaraSchema.optional(),
75
76
  meend: zod.z.enum(["start", "end"]).optional(),
77
+ halfMatra: zod.z.boolean().optional(),
76
78
  bar: barSchema.optional(),
77
79
  brackets: zod.z.array(bracketSchema).optional(),
78
80
  lyric: zod.z.string().optional(),
@@ -123,9 +125,9 @@ var init_schema = __esm({
123
125
  nltrId: zod.z.number().int().optional(),
124
126
  taal: taalSchema.optional(),
125
127
  /**
126
- * The taal name exactly as printed in the source (e.g. `তালমুক্ত`,
127
- * `ষষ্ঠী(২/৪)`). Always set by the NLTR converter; `taal` is set only
128
- * when the name resolves to a known definition.
128
+ * The taal name as printed in the source (e.g. `ষষ্ঠী(২/৪)`), except
129
+ * free rhythm which is normalised to `মুক্তছন্দ`. Always set by the NLTR
130
+ * converter; `taal` is set only when the name resolves to a definition.
129
131
  */
130
132
  taalName: zod.z.string().optional(),
131
133
  /** Base pitch for playback, e.g. `C`, `C#`, or the player's `cs` form. */
@@ -183,10 +185,170 @@ var init_spec = __esm({
183
185
  init_schema();
184
186
  init_schema();
185
187
  init_taals();
186
- SPEC_VERSION = "0.5.0";
188
+ SPEC_VERSION = "0.6.1";
187
189
  }
188
190
  });
189
191
 
192
+ // src/player/pitch.ts
193
+ var OCTAVE_OFFSET = {
194
+ "ati-mandra": -2,
195
+ mandra: -1,
196
+ madhyam: 0,
197
+ taar: 1,
198
+ "ati-taar": 2
199
+ };
200
+ var SHUDDHA = {
201
+ s: 0,
202
+ r: 2,
203
+ g: 4,
204
+ m: 5,
205
+ p: 7,
206
+ d: 9,
207
+ n: 11
208
+ };
209
+ var SHARP_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
210
+ var PITCH_CLASS = {
211
+ c: 0,
212
+ "c#": 1,
213
+ db: 1,
214
+ d: 2,
215
+ "d#": 3,
216
+ eb: 3,
217
+ e: 4,
218
+ "e#": 5,
219
+ fb: 4,
220
+ f: 5,
221
+ "f#": 6,
222
+ gb: 6,
223
+ g: 7,
224
+ "g#": 8,
225
+ ab: 8,
226
+ a: 9,
227
+ "a#": 10,
228
+ bb: 10,
229
+ b: 11,
230
+ "b#": 0,
231
+ cb: 11
232
+ };
233
+ function parseTonic(tonic) {
234
+ if (!tonic) return 0;
235
+ const key = tonic.trim().toLowerCase().replace(/s/g, "#");
236
+ return PITCH_CLASS[key] ?? 0;
237
+ }
238
+ function svaraSemitone(note) {
239
+ if (note.svara === void 0) return null;
240
+ const [, letter] = note.svara;
241
+ let semi = SHUDDHA[letter];
242
+ if (note.komal === true || note.microtone !== void 0) semi -= 1;
243
+ if (note.tivra === true) semi += 1;
244
+ return semi;
245
+ }
246
+ function svaraToMidi(note, tonicPc, baseOctave = 4) {
247
+ const semi = svaraSemitone(note);
248
+ if (semi === null || note.svara === void 0) return null;
249
+ const [octave] = note.svara;
250
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + semi;
251
+ }
252
+ function kanToMidi(svara, tonicPc, baseOctave = 4) {
253
+ if (svara === void 0) return null;
254
+ const [octave, letter] = svara;
255
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + SHUDDHA[letter];
256
+ }
257
+ function midiToSampleName(midi) {
258
+ const pc = (midi % 12 + 12) % 12;
259
+ const octave = Math.floor(midi / 12) - 1;
260
+ return `${SHARP_NAMES[pc]}${octave}`;
261
+ }
262
+ function sampleFileName(midi) {
263
+ return midiToSampleName(midi).toLowerCase().replace("#", "s");
264
+ }
265
+
266
+ // src/player/schedule.ts
267
+ var UNDECODED_CELL = /[srgmpqnvtduka]/i;
268
+ var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
269
+ var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
270
+ var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
271
+ function meendContinuations(notes) {
272
+ const out = /* @__PURE__ */ new Set();
273
+ let pending;
274
+ notes.forEach((note, i) => {
275
+ let marks = note.meend === "start" ? "w" : note.meend === "end" ? "x" : "";
276
+ for (const b of note.below ?? []) if (isArcRaw(b.raw)) marks += b.raw;
277
+ let marked = false;
278
+ for (const ch of marks) {
279
+ if (ch === "x" || ch === "X") {
280
+ if (pending !== void 0) for (const k of [...pending, i]) out.add(k);
281
+ pending = void 0;
282
+ marked = true;
283
+ } else if (ch === "w" || ch === "W") {
284
+ pending = [];
285
+ marked = true;
286
+ }
287
+ }
288
+ if (!marked && pending !== void 0) pending.push(i);
289
+ });
290
+ return out;
291
+ }
292
+ function glideTargets(events, i) {
293
+ const out = [];
294
+ for (let j = i + 1; j < events.length; j++) {
295
+ const ev = events[j];
296
+ if (ev.kind === "rest") break;
297
+ if (ev.midi === null) continue;
298
+ if (ev.glide !== true) break;
299
+ out.push(ev);
300
+ }
301
+ return out;
302
+ }
303
+ var SAM_TICK = /^\d+[fF]$/;
304
+ function buildSchedule(doc, opts = {}) {
305
+ const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
306
+ const baseOctave = opts.baseOctave ?? 4;
307
+ const events = [];
308
+ let cursor = 0;
309
+ let lastPitched = -1;
310
+ let samBeat;
311
+ doc.parts.forEach((part, p) => {
312
+ part.rows.forEach((row, r) => {
313
+ const glides = meendContinuations(row.notes);
314
+ row.notes.forEach((note, i) => {
315
+ if (samBeat === void 0 && note.above !== void 0) {
316
+ for (const a of note.above) {
317
+ if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
318
+ samBeat = cursor;
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ if (note.bar !== void 0 || isSilent(note)) return;
324
+ const path = `${p}.${r}.${i}`;
325
+ const dur = note.durationMatra ?? 1;
326
+ if (note.svara !== void 0) {
327
+ events.push({
328
+ beat: cursor,
329
+ durationBeats: dur,
330
+ midi: svaraToMidi(note, tonicPc, baseOctave),
331
+ kan: kanToMidi(note.kan, tonicPc, baseOctave),
332
+ path,
333
+ kind: note.sustain === true ? "sustain" : "note",
334
+ ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
335
+ });
336
+ lastPitched = events.length - 1;
337
+ } else if (note.sustain === true) {
338
+ if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
339
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "sustain" });
340
+ } else {
341
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "rest" });
342
+ }
343
+ cursor += dur;
344
+ });
345
+ });
346
+ });
347
+ const numBeats = doc.meta.taal?.numBeats;
348
+ const pickupBeats = samBeat !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samBeat % numBeats) % numBeats : 0;
349
+ return { events, totalBeats: cursor, pickupBeats };
350
+ }
351
+
190
352
  // src/viewer/glyphs.ts
191
353
  var SWARABITAN_SHUDDHA = {
192
354
  s: "s",
@@ -253,7 +415,6 @@ function localizeDigits(s, lang) {
253
415
  }
254
416
 
255
417
  // src/viewer/render.ts
256
- var NBSP = "\xA0";
257
418
  var EPS = 1e-6;
258
419
  var isIntegral = (x) => Math.abs(x - Math.round(x)) < EPS;
259
420
  function escapeHtml(s) {
@@ -288,7 +449,7 @@ function groupRowIntoCells(notes) {
288
449
  return;
289
450
  }
290
451
  const d = note.durationMatra ?? 1;
291
- if (isIntegral(d)) {
452
+ if (isIntegral(d) && note.halfMatra !== true) {
292
453
  close();
293
454
  cells.push({ kind: "group", start: i, notes: [note] });
294
455
  return;
@@ -296,13 +457,11 @@ function groupRowIntoCells(notes) {
296
457
  cur ??= { kind: "group", start: i, notes: [] };
297
458
  cur.notes.push(note);
298
459
  acc += d;
299
- if (acc > EPS && isIntegral(acc)) close();
460
+ if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
300
461
  });
301
462
  close();
302
463
  return cells;
303
464
  }
304
- var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
305
- var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
306
465
  function resolveArcs(cells, noteCount) {
307
466
  const arcs = [];
308
467
  let openStart;
@@ -351,9 +510,22 @@ function resolveArcs(cells, noteCount) {
351
510
  }
352
511
  return arcs;
353
512
  }
354
- function cutMarkCount(raw) {
355
- if (raw === void 0) return 0;
356
- return (raw.match(/[i:]/g) ?? []).length;
513
+ var SVARA_CHAR = /[srgmpqndtkuv]/;
514
+ function rawMarks(raw, count) {
515
+ if (raw === void 0) return void 0;
516
+ const marks = { aakar: false, pre: "", post: new Array(count).fill("") };
517
+ const leads = SVARA_CHAR.test(raw);
518
+ let seen = 0;
519
+ for (const ch of raw) {
520
+ if (SVARA_CHAR.test(ch)) seen = Math.min(seen + 1, count);
521
+ else if (ch === "a") marks.aakar = true;
522
+ else if (ch === "i" || ch === ":") {
523
+ if (seen > 0) marks.post[seen - 1] += ch;
524
+ else if (leads) marks.pre += ch;
525
+ else marks.post[0] += ch;
526
+ }
527
+ }
528
+ return marks;
357
529
  }
358
530
  function parseBeatLabel(raw) {
359
531
  const m = raw.match(/^([0-9]{1,2})([fF])?:?$/);
@@ -371,38 +543,63 @@ function bracketGlyphs(note, edge) {
371
543
  function matraGlyph(ctx) {
372
544
  return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
373
545
  }
374
- function renderKan(note, ctx) {
375
- if (!note.kan) return "";
376
- const [octave, letter] = note.kan;
546
+ function renderGrace(svara, cls, ctx) {
547
+ if (svara === void 0) return "";
548
+ const [octave, letter] = svara;
377
549
  const c = composeCluster(letter, { octave, withMatra: false, lang: ctx.lang });
378
- return `<span class="${["sw-kan", ...c.classes].join(" ")}">${c.text}</span>`;
550
+ return `<span class="${[cls, ...c.classes].join(" ")}">${c.text}</span>`;
551
+ }
552
+ function cutGlyphs(marks, ctx) {
553
+ if (marks === "") return "";
554
+ return ctx.lang === "bn" ? `<span class="sw-cl" title="${ctx.marks.cut}">${marks}</span>` : `<span class="sw-cut" title="${ctx.marks.cut}"></span>`.repeat(marks.length);
555
+ }
556
+ function cellMatra(cell) {
557
+ const total = cell.notes.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
558
+ const half = cell.notes.some((n) => n.halfMatra === true);
559
+ const whole = Math.floor(total + EPS);
560
+ return { matras: half ? whole : Math.max(1, whole), half };
561
+ }
562
+ function cellPitchedOpts(cell) {
563
+ const { matras, half } = cellMatra(cell);
564
+ const marks = rawMarks(cell.notes[0].raw, cell.notes.length);
565
+ const aakars = marks !== void 0 && !marks.aakar ? 0 : matras;
566
+ return cell.notes.map((_, j) => ({
567
+ matras: j === cell.notes.length - 1 ? aakars : 0,
568
+ pre: j === 0 ? marks?.pre ?? "" : "",
569
+ post: marks !== void 0 ? marks.post[j] : half && j === cell.notes.length - 1 ? "i" : ""
570
+ }));
379
571
  }
380
572
  function renderPitchedInner(note, opts, ctx) {
381
573
  let inner = bracketGlyphs(note, "open");
382
- inner += renderKan(note, ctx);
574
+ inner += renderGrace(note.kan, "sw-kan", ctx);
383
575
  if (note.sustain === true) inner += `<span class="sw-hy">${ctx.marks.hyphen}</span>`;
576
+ inner += cutGlyphs(opts.pre, ctx);
384
577
  if (note.svara !== void 0) {
385
578
  const [octave, letter] = note.svara;
386
579
  const c = composeCluster(letter, {
387
580
  komal: note.komal,
388
581
  tivra: note.tivra,
389
582
  octave,
390
- withMatra: opts.withMatra,
583
+ // The first matra is composed into the cluster itself (bn: the font's
584
+ // trailing `a`); any further ones are separate glyphs below.
585
+ withMatra: opts.matras >= 1,
391
586
  lang: ctx.lang
392
587
  });
393
588
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
394
589
  } else if (note.rest === true) {
395
- inner += matraGlyph(ctx);
396
- } else if (note.sustain === true && opts.withMatra) {
397
- inner += matraGlyph(ctx);
590
+ inner += matraGlyph(ctx).repeat(Math.max(1, opts.matras));
591
+ } else if (note.sustain === true) {
592
+ inner += matraGlyph(ctx).repeat(opts.matras);
398
593
  }
399
594
  if (note.microtone !== void 0) {
400
595
  const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
401
596
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
402
597
  }
403
- const d = note.durationMatra ?? 1;
404
- if (isIntegral(d) && d > 1 + EPS) inner += matraGlyph(ctx).repeat(Math.round(d) - 1);
405
- if (opts.cuts > 0) inner += `<span class="sw-cut">${NBSP}${ctx.marks.cut}</span>`.repeat(opts.cuts);
598
+ inner += renderGrace(note.resh, "sw-resh", ctx);
599
+ if (note.svara !== void 0 && opts.matras > 1) {
600
+ inner += matraGlyph(ctx).repeat(opts.matras - 1);
601
+ }
602
+ inner += cutGlyphs(opts.post, ctx);
406
603
  inner += bracketGlyphs(note, "close");
407
604
  return inner;
408
605
  }
@@ -419,12 +616,9 @@ function renderAnnotation(notes, ctx) {
419
616
  continue;
420
617
  }
421
618
  if (cell.kind === "group") {
422
- const cuts = cutMarkCount(cell.notes[0].raw);
619
+ const opts = cellPitchedOpts(cell);
423
620
  cell.notes.forEach((note2, j) => {
424
- const last = j === cell.notes.length - 1;
425
- parts.push(
426
- `<span class="sw-n">${renderPitchedInner(note2, { withMatra: last, cuts: last ? cuts : 0 }, ctx)}</span>`
427
- );
621
+ parts.push(`<span class="sw-n">${renderPitchedInner(note2, opts[j], ctx)}</span>`);
428
622
  });
429
623
  continue;
430
624
  }
@@ -462,7 +656,7 @@ function renderRow(row, partIdx, rowIdx, ctx) {
462
656
  const firstCol = cell.start + 1;
463
657
  const lastCol = cell.start + cell.notes.length;
464
658
  const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
465
- const cuts = cell.kind === "group" ? cutMarkCount(cell.notes[0].raw) : 0;
659
+ const opts = cell.kind === "group" ? cellPitchedOpts(cell) : [];
466
660
  cell.notes.forEach((note, j) => {
467
661
  const col = cell.start + j + 1;
468
662
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
@@ -472,9 +666,8 @@ function renderRow(row, partIdx, rowIdx, ctx) {
472
666
  `<span class="sw-bar sw-bar--${note.bar}${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderBarInner(note)}</span>`
473
667
  );
474
668
  } else if (cell.kind === "group") {
475
- const last = j === cell.notes.length - 1;
476
669
  notation.push(
477
- `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, { withMatra: last, cuts: last ? cuts : 0 }, ctx)}</span>`
670
+ `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, opts[j], ctx)}</span>`
478
671
  );
479
672
  } else {
480
673
  const raw = rawDisplay(note);
@@ -565,9 +758,9 @@ ${FONT_FACES}
565
758
  }
566
759
 
567
760
  /* ---- notation font, per language (the note clusters + grace notes) ---- */
568
- .sw-sheet[lang="bn"] .sw-cl, .sw-sheet[lang="bn"] .sw-kan { font-family: Swarabitan, var(--sw-font, sans-serif); }
569
- .sw-sheet[lang="hi"] .sw-cl, .sw-sheet[lang="hi"] .sw-kan { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
570
- .sw-sheet[lang="en"] .sw-cl, .sw-sheet[lang="en"] .sw-kan { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
761
+ .sw-sheet[lang="bn"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: Swarabitan, var(--sw-font, sans-serif); }
762
+ .sw-sheet[lang="hi"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
763
+ .sw-sheet[lang="en"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
571
764
 
572
765
  /* ---- header ---- */
573
766
  .sw-header {
@@ -626,18 +819,42 @@ ${FONT_FACES}
626
819
  .sw-ati-taar::before { top: -0.3em; }
627
820
  .sw-ati-mandra::before { bottom: -0.35em; }
628
821
 
629
- /* kan grace: small raised cluster left of the main note */
630
- .sw-kan {
822
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
823
+ .sw-kan,
824
+ .sw-resh {
631
825
  position: relative;
632
826
  display: inline-block;
633
827
  font-size: 0.58em;
634
828
  vertical-align: 0.75em;
635
- margin-right: 0.08em;
636
829
  }
830
+ .sw-kan { margin-right: 0.08em; }
831
+ .sw-resh { margin-left: 0.08em; }
637
832
 
638
833
  .sw-hy { padding-right: 0.02em; }
639
834
  .sw-iso { display: inline-block; margin-left: -0.18em; }
640
- .sw-cut { font-size: 0.8em; display: inline-block; margin-left: -0.15em; }
835
+ /* The \u0985\u09B0\u09CD\u09A7\u09AE\u09BE\u09A4\u09CD\u09B0\u09BE / \u099B\u09C7\u09A6 mark: two stacked rings (legend p. \u09E7\u09E9\u09EB). bn types
836
+ * Swarabitan's own glyph (the ASCII i key) \u2014 an isolated Unicode \u0983 has no base and
837
+ * every Bengali text font shapes it with a dotted circle (an NBSP carrier
838
+ * does not help), but the notation font has the mark itself. hi/en fall back
839
+ * to these drawn rings: Bhatkhande has no such sign. */
840
+ .sw-cut {
841
+ position: relative;
842
+ display: inline-block;
843
+ width: 0.3em;
844
+ height: 1em;
845
+ vertical-align: baseline;
846
+ }
847
+ .sw-cut::before, .sw-cut::after {
848
+ content: '';
849
+ position: absolute;
850
+ left: 0.08em;
851
+ width: 0.15em;
852
+ height: 0.15em;
853
+ border-radius: 50%;
854
+ background: currentColor;
855
+ }
856
+ .sw-cut::before { top: 0.2em; }
857
+ .sw-cut::after { top: 0.55em; }
641
858
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
642
859
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
643
860
 
@@ -748,129 +965,6 @@ var SWARALIPI_STAFF_CSS = `
748
965
  .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
749
966
  `;
750
967
 
751
- // src/player/pitch.ts
752
- var OCTAVE_OFFSET = {
753
- "ati-mandra": -2,
754
- mandra: -1,
755
- madhyam: 0,
756
- taar: 1,
757
- "ati-taar": 2
758
- };
759
- var SHUDDHA = {
760
- s: 0,
761
- r: 2,
762
- g: 4,
763
- m: 5,
764
- p: 7,
765
- d: 9,
766
- n: 11
767
- };
768
- var SHARP_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
769
- var PITCH_CLASS = {
770
- c: 0,
771
- "c#": 1,
772
- db: 1,
773
- d: 2,
774
- "d#": 3,
775
- eb: 3,
776
- e: 4,
777
- "e#": 5,
778
- fb: 4,
779
- f: 5,
780
- "f#": 6,
781
- gb: 6,
782
- g: 7,
783
- "g#": 8,
784
- ab: 8,
785
- a: 9,
786
- "a#": 10,
787
- bb: 10,
788
- b: 11,
789
- "b#": 0,
790
- cb: 11
791
- };
792
- function parseTonic(tonic) {
793
- if (!tonic) return 0;
794
- const key = tonic.trim().toLowerCase().replace(/s/g, "#");
795
- return PITCH_CLASS[key] ?? 0;
796
- }
797
- function svaraSemitone(note) {
798
- if (note.svara === void 0) return null;
799
- const [, letter] = note.svara;
800
- let semi = SHUDDHA[letter];
801
- if (note.komal === true || note.microtone !== void 0) semi -= 1;
802
- if (note.tivra === true) semi += 1;
803
- return semi;
804
- }
805
- function svaraToMidi(note, tonicPc, baseOctave = 4) {
806
- const semi = svaraSemitone(note);
807
- if (semi === null || note.svara === void 0) return null;
808
- const [octave] = note.svara;
809
- return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + semi;
810
- }
811
- function kanToMidi(svara, tonicPc, baseOctave = 4) {
812
- if (svara === void 0) return null;
813
- const [octave, letter] = svara;
814
- return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + SHUDDHA[letter];
815
- }
816
- function midiToSampleName(midi) {
817
- const pc = (midi % 12 + 12) % 12;
818
- const octave = Math.floor(midi / 12) - 1;
819
- return `${SHARP_NAMES[pc]}${octave}`;
820
- }
821
- function sampleFileName(midi) {
822
- return midiToSampleName(midi).toLowerCase().replace("#", "s");
823
- }
824
-
825
- // src/player/schedule.ts
826
- var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true;
827
- var SAM_TICK = /^\d+[fF]$/;
828
- function buildSchedule(doc, opts = {}) {
829
- const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
830
- const baseOctave = opts.baseOctave ?? 4;
831
- const events = [];
832
- let cursor = 0;
833
- let lastPitched = -1;
834
- let samBeat;
835
- doc.parts.forEach((part, p) => {
836
- part.rows.forEach((row, r) => {
837
- row.notes.forEach((note, i) => {
838
- if (samBeat === void 0 && note.above !== void 0) {
839
- for (const a of note.above) {
840
- if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
841
- samBeat = cursor;
842
- break;
843
- }
844
- }
845
- }
846
- if (note.bar !== void 0 || isSilent(note)) return;
847
- const path = `${p}.${r}.${i}`;
848
- const dur = note.durationMatra ?? 1;
849
- if (note.svara !== void 0) {
850
- events.push({
851
- beat: cursor,
852
- durationBeats: dur,
853
- midi: svaraToMidi(note, tonicPc, baseOctave),
854
- kan: kanToMidi(note.kan, tonicPc, baseOctave),
855
- path,
856
- kind: note.sustain === true ? "sustain" : "note"
857
- });
858
- lastPitched = events.length - 1;
859
- } else if (note.sustain === true) {
860
- if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
861
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "sustain" });
862
- } else {
863
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "rest" });
864
- }
865
- cursor += dur;
866
- });
867
- });
868
- });
869
- const numBeats = doc.meta.taal?.numBeats;
870
- const pickupBeats = samBeat !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samBeat % numBeats) % numBeats : 0;
871
- return { events, totalBeats: cursor, pickupBeats };
872
- }
873
-
874
968
  // src/viewer/staff.ts
875
969
  init_taals();
876
970
 
@@ -1036,20 +1130,23 @@ function buildAbc(schedule, lyricOf, opts) {
1036
1130
  barPos -= beatsPerBar;
1037
1131
  }
1038
1132
  };
1039
- for (const ev of schedule.events) {
1133
+ schedule.events.forEach((ev, i) => {
1040
1134
  if (ev.midi === null) {
1041
1135
  if (ev.kind === "rest") {
1042
1136
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
1043
1137
  words.push("*");
1044
1138
  advance(ev.durationBeats);
1045
1139
  }
1046
- continue;
1140
+ return;
1047
1141
  }
1048
- tokens.push(abcNote(ev));
1142
+ const more = glideTargets(schedule.events, i).length > 0;
1143
+ const slurOpen = ev.glide !== true && more ? "(" : "";
1144
+ const slurClose = ev.glide === true && !more ? ")" : "";
1145
+ tokens.push(`${slurOpen}${abcNote(ev)}${slurClose}`);
1049
1146
  const lyr = lyricOf(ev);
1050
1147
  words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
1051
1148
  advance(ev.durationBeats);
1052
- }
1149
+ });
1053
1150
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1054
1151
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1055
1152
  const meter = ratioMeter(beatsPerBar);
@@ -1518,6 +1615,7 @@ exports.abcDuration = abcDuration;
1518
1615
  exports.abcToSvg = abcToSvg;
1519
1616
  exports.buildAbc = buildAbc;
1520
1617
  exports.buildTaalAbc = buildTaalAbc;
1618
+ exports.cellMatra = cellMatra;
1521
1619
  exports.composeCluster = composeCluster;
1522
1620
  exports.defineSwaralipiSheet = defineSwaralipiSheet;
1523
1621
  exports.docToAbc = docToAbc;