@ugfoundation/swaralipi-js 0.5.0 → 0.6.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 (46) hide show
  1. package/dist/{batch-UETOBPHT.js → batch-RUZGM2NR.js} +4 -4
  2. package/dist/{batch-UETOBPHT.js.map → batch-RUZGM2NR.js.map} +1 -1
  3. package/dist/{chunk-C6TRCMPL.js → chunk-GHWURC4U.js} +97 -5
  4. package/dist/chunk-GHWURC4U.js.map +1 -0
  5. package/dist/{chunk-UD2K4UIC.js → chunk-ULK7NVKH.js} +8 -6
  6. package/dist/chunk-ULK7NVKH.js.map +1 -0
  7. package/dist/{chunk-ID44OP2N.js → chunk-ZNVTOY7J.js} +7 -6
  8. package/dist/chunk-ZNVTOY7J.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 +240 -161
  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 +75 -36
  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.0";
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;
@@ -371,15 +530,21 @@ function bracketGlyphs(note, edge) {
371
530
  function matraGlyph(ctx) {
372
531
  return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
373
532
  }
374
- function renderKan(note, ctx) {
375
- if (!note.kan) return "";
376
- const [octave, letter] = note.kan;
533
+ function renderGrace(svara, cls, ctx) {
534
+ if (svara === void 0) return "";
535
+ const [octave, letter] = svara;
377
536
  const c = composeCluster(letter, { octave, withMatra: false, lang: ctx.lang });
378
- return `<span class="${["sw-kan", ...c.classes].join(" ")}">${c.text}</span>`;
537
+ return `<span class="${[cls, ...c.classes].join(" ")}">${c.text}</span>`;
538
+ }
539
+ function cellMatra(cell) {
540
+ const total = cell.notes.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
541
+ const half = cell.notes.some((n) => n.halfMatra === true);
542
+ const whole = Math.floor(total + EPS);
543
+ return { matras: half ? whole : Math.max(1, whole), half };
379
544
  }
380
545
  function renderPitchedInner(note, opts, ctx) {
381
546
  let inner = bracketGlyphs(note, "open");
382
- inner += renderKan(note, ctx);
547
+ inner += renderGrace(note.kan, "sw-kan", ctx);
383
548
  if (note.sustain === true) inner += `<span class="sw-hy">${ctx.marks.hyphen}</span>`;
384
549
  if (note.svara !== void 0) {
385
550
  const [octave, letter] = note.svara;
@@ -387,22 +552,27 @@ function renderPitchedInner(note, opts, ctx) {
387
552
  komal: note.komal,
388
553
  tivra: note.tivra,
389
554
  octave,
390
- withMatra: opts.withMatra,
555
+ // The first matra is composed into the cluster itself (bn: the font's
556
+ // trailing `a`); any further ones are separate glyphs below.
557
+ withMatra: opts.matras >= 1,
391
558
  lang: ctx.lang
392
559
  });
393
560
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
394
561
  } else if (note.rest === true) {
395
- inner += matraGlyph(ctx);
396
- } else if (note.sustain === true && opts.withMatra) {
397
- inner += matraGlyph(ctx);
562
+ inner += matraGlyph(ctx).repeat(Math.max(1, opts.matras));
563
+ } else if (note.sustain === true) {
564
+ inner += matraGlyph(ctx).repeat(opts.matras);
398
565
  }
399
566
  if (note.microtone !== void 0) {
400
567
  const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
401
568
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
402
569
  }
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);
570
+ inner += renderGrace(note.resh, "sw-resh", ctx);
571
+ if (note.svara !== void 0 && opts.matras > 1) {
572
+ inner += matraGlyph(ctx).repeat(opts.matras - 1);
573
+ }
574
+ if (opts.half) inner += `<span class="sw-half" title="${ctx.marks.cut}"></span>`;
575
+ if (opts.cuts > 0) inner += `<span class="sw-cut" title="${ctx.marks.cut}"></span>`.repeat(opts.cuts);
406
576
  inner += bracketGlyphs(note, "close");
407
577
  return inner;
408
578
  }
@@ -419,12 +589,12 @@ function renderAnnotation(notes, ctx) {
419
589
  continue;
420
590
  }
421
591
  if (cell.kind === "group") {
422
- const cuts = cutMarkCount(cell.notes[0].raw);
592
+ const { matras, half } = cellMatra(cell);
593
+ const cuts = half ? 0 : cutMarkCount(cell.notes[0].raw);
423
594
  cell.notes.forEach((note2, j) => {
424
595
  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
- );
596
+ const opts = last ? { matras, half, cuts } : { matras: 0, half: false, cuts: 0 };
597
+ parts.push(`<span class="sw-n">${renderPitchedInner(note2, opts, ctx)}</span>`);
428
598
  });
429
599
  continue;
430
600
  }
@@ -462,7 +632,8 @@ function renderRow(row, partIdx, rowIdx, ctx) {
462
632
  const firstCol = cell.start + 1;
463
633
  const lastCol = cell.start + cell.notes.length;
464
634
  const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
465
- const cuts = cell.kind === "group" ? cutMarkCount(cell.notes[0].raw) : 0;
635
+ const { matras, half } = cellMatra(cell);
636
+ const cuts = cell.kind === "group" && !half ? cutMarkCount(cell.notes[0].raw) : 0;
466
637
  cell.notes.forEach((note, j) => {
467
638
  const col = cell.start + j + 1;
468
639
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
@@ -473,8 +644,9 @@ function renderRow(row, partIdx, rowIdx, ctx) {
473
644
  );
474
645
  } else if (cell.kind === "group") {
475
646
  const last = j === cell.notes.length - 1;
647
+ const pOpts = last ? { matras, half, cuts } : { matras: 0, half: false, cuts: 0 };
476
648
  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>`
649
+ `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, pOpts, ctx)}</span>`
478
650
  );
479
651
  } else {
480
652
  const raw = rawDisplay(note);
@@ -565,9 +737,9 @@ ${FONT_FACES}
565
737
  }
566
738
 
567
739
  /* ---- 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); }
740
+ .sw-sheet[lang="bn"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: Swarabitan, var(--sw-font, sans-serif); }
741
+ .sw-sheet[lang="hi"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
742
+ .sw-sheet[lang="en"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
571
743
 
572
744
  /* ---- header ---- */
573
745
  .sw-header {
@@ -626,18 +798,44 @@ ${FONT_FACES}
626
798
  .sw-ati-taar::before { top: -0.3em; }
627
799
  .sw-ati-mandra::before { bottom: -0.35em; }
628
800
 
629
- /* kan grace: small raised cluster left of the main note */
630
- .sw-kan {
801
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
802
+ .sw-kan,
803
+ .sw-resh {
631
804
  position: relative;
632
805
  display: inline-block;
633
806
  font-size: 0.58em;
634
807
  vertical-align: 0.75em;
635
- margin-right: 0.08em;
636
808
  }
809
+ .sw-kan { margin-right: 0.08em; }
810
+ .sw-resh { margin-left: 0.08em; }
637
811
 
638
812
  .sw-hy { padding-right: 0.02em; }
639
813
  .sw-iso { display: inline-block; margin-left: -0.18em; }
640
- .sw-cut { font-size: 0.8em; display: inline-block; margin-left: -0.15em; }
814
+ /* The \u0985\u09B0\u09CD\u09A7\u09AE\u09BE\u09A4\u09CD\u09B0\u09BE / \u099B\u09C7\u09A6 mark: two stacked rings (legend p. \u09E7\u09E9\u09EB), drawn rather
815
+ * than typed \u2014 an isolated \u0983 has no base, so every Bengali font shapes it with
816
+ * a dotted circle (an NBSP carrier does not help). sw-half = semantic
817
+ * (halfMatra), sw-cut = decoded from a source cell's raw: one look, two
818
+ * provenances. */
819
+ .sw-cut,
820
+ .sw-half {
821
+ position: relative;
822
+ display: inline-block;
823
+ width: 0.3em;
824
+ height: 1em;
825
+ vertical-align: baseline;
826
+ }
827
+ .sw-cut::before, .sw-cut::after,
828
+ .sw-half::before, .sw-half::after {
829
+ content: '';
830
+ position: absolute;
831
+ left: 0.08em;
832
+ width: 0.15em;
833
+ height: 0.15em;
834
+ border-radius: 50%;
835
+ background: currentColor;
836
+ }
837
+ .sw-cut::before, .sw-half::before { top: 0.2em; }
838
+ .sw-cut::after, .sw-half::after { top: 0.55em; }
641
839
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
642
840
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
643
841
 
@@ -748,129 +946,6 @@ var SWARALIPI_STAFF_CSS = `
748
946
  .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
749
947
  `;
750
948
 
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
949
  // src/viewer/staff.ts
875
950
  init_taals();
876
951
 
@@ -1036,20 +1111,23 @@ function buildAbc(schedule, lyricOf, opts) {
1036
1111
  barPos -= beatsPerBar;
1037
1112
  }
1038
1113
  };
1039
- for (const ev of schedule.events) {
1114
+ schedule.events.forEach((ev, i) => {
1040
1115
  if (ev.midi === null) {
1041
1116
  if (ev.kind === "rest") {
1042
1117
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
1043
1118
  words.push("*");
1044
1119
  advance(ev.durationBeats);
1045
1120
  }
1046
- continue;
1121
+ return;
1047
1122
  }
1048
- tokens.push(abcNote(ev));
1123
+ const more = glideTargets(schedule.events, i).length > 0;
1124
+ const slurOpen = ev.glide !== true && more ? "(" : "";
1125
+ const slurClose = ev.glide === true && !more ? ")" : "";
1126
+ tokens.push(`${slurOpen}${abcNote(ev)}${slurClose}`);
1049
1127
  const lyr = lyricOf(ev);
1050
1128
  words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
1051
1129
  advance(ev.durationBeats);
1052
- }
1130
+ });
1053
1131
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1054
1132
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1055
1133
  const meter = ratioMeter(beatsPerBar);
@@ -1518,6 +1596,7 @@ exports.abcDuration = abcDuration;
1518
1596
  exports.abcToSvg = abcToSvg;
1519
1597
  exports.buildAbc = buildAbc;
1520
1598
  exports.buildTaalAbc = buildTaalAbc;
1599
+ exports.cellMatra = cellMatra;
1521
1600
  exports.composeCluster = composeCluster;
1522
1601
  exports.defineSwaralipiSheet = defineSwaralipiSheet;
1523
1602
  exports.docToAbc = docToAbc;