@ugfoundation/swaralipi-js 0.4.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 (47) hide show
  1. package/README.md +8 -8
  2. package/dist/{batch-V4X3GN4N.js → batch-RUZGM2NR.js} +4 -4
  3. package/dist/{batch-V4X3GN4N.js.map → batch-RUZGM2NR.js.map} +1 -1
  4. package/dist/{chunk-6QW7MSJO.js → chunk-GHWURC4U.js} +98 -6
  5. package/dist/chunk-GHWURC4U.js.map +1 -0
  6. package/dist/{chunk-RTKWFJFI.js → chunk-ULK7NVKH.js} +8 -6
  7. package/dist/chunk-ULK7NVKH.js.map +1 -0
  8. package/dist/{chunk-ZS43BDP7.js → chunk-ZNVTOY7J.js} +7 -6
  9. package/dist/chunk-ZNVTOY7J.js.map +1 -0
  10. package/dist/cli.cjs +11 -8
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +2 -2
  13. package/dist/convert/index.cjs +10 -7
  14. package/dist/convert/index.cjs.map +1 -1
  15. package/dist/convert/index.d.cts +4 -3
  16. package/dist/convert/index.d.ts +4 -3
  17. package/dist/convert/index.js +2 -2
  18. package/dist/index.cjs +6 -4
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/player/index.cjs +222 -23
  24. package/dist/player/index.cjs.map +1 -1
  25. package/dist/player/index.d.cts +16 -4
  26. package/dist/player/index.d.ts +16 -4
  27. package/dist/player/index.js +120 -22
  28. package/dist/player/index.js.map +1 -1
  29. package/dist/{schedule-CAns78TD.d.ts → schedule-8WFUBJqB.d.ts} +82 -7
  30. package/dist/{schedule-D3JyEhuO.d.cts → schedule-CjPcD-Hf.d.cts} +82 -7
  31. package/dist/{schema-D3wyNRqM.d.ts → schema-D_86x4oV.d.cts} +21 -6
  32. package/dist/{schema-D3wyNRqM.d.cts → schema-D_86x4oV.d.ts} +21 -6
  33. package/dist/spec/index.cjs +6 -4
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.d.cts +3 -3
  36. package/dist/spec/index.d.ts +3 -3
  37. package/dist/spec/index.js +1 -1
  38. package/dist/viewer/index.cjs +253 -172
  39. package/dist/viewer/index.cjs.map +1 -1
  40. package/dist/viewer/index.d.cts +16 -23
  41. package/dist/viewer/index.d.ts +16 -23
  42. package/dist/viewer/index.js +88 -47
  43. package/dist/viewer/index.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunk-6QW7MSJO.js.map +0 -1
  46. package/dist/chunk-RTKWFJFI.js.map +0 -1
  47. package/dist/chunk-ZS43BDP7.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.4.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",
@@ -227,7 +389,9 @@ var MARKS = {
227
389
  var LABELS = {
228
390
  bn: { title: "\u09A8\u09BE\u09AE", taal: "\u09A4\u09BE\u09B2", raag: "\u09B0\u09BE\u0997", avartan: "\u0986\u09AC\u09B0\u09CD\u09A4\u09A8", notationBy: "\u09B8\u09CD\u09AC\u09B0\u09B2\u09BF\u09AA\u09BF\u0995\u09BE\u09B0" },
229
391
  hi: { title: "\u0928\u093E\u092E", taal: "\u0924\u093E\u0932", raag: "\u0930\u093E\u0917", avartan: "\u0906\u0935\u0930\u094D\u0924\u0928", notationBy: "\u0938\u094D\u0935\u0930\u0932\u093F\u092A\u093F\u0915\u093E\u0930" },
230
- en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notator" }
392
+ // "Cycle", not "Avartan": the English sheet is read by people who do not know the word, and
393
+ // one taal cycle is exactly what the number counts.
394
+ en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Cycles", notationBy: "Notator" }
231
395
  };
232
396
  function composeCluster(letter, opts) {
233
397
  const classes = [];
@@ -251,7 +415,6 @@ function localizeDigits(s, lang) {
251
415
  }
252
416
 
253
417
  // src/viewer/render.ts
254
- var NBSP = "\xA0";
255
418
  var EPS = 1e-6;
256
419
  var isIntegral = (x) => Math.abs(x - Math.round(x)) < EPS;
257
420
  function escapeHtml(s) {
@@ -286,7 +449,7 @@ function groupRowIntoCells(notes) {
286
449
  return;
287
450
  }
288
451
  const d = note.durationMatra ?? 1;
289
- if (isIntegral(d)) {
452
+ if (isIntegral(d) && note.halfMatra !== true) {
290
453
  close();
291
454
  cells.push({ kind: "group", start: i, notes: [note] });
292
455
  return;
@@ -294,13 +457,11 @@ function groupRowIntoCells(notes) {
294
457
  cur ??= { kind: "group", start: i, notes: [] };
295
458
  cur.notes.push(note);
296
459
  acc += d;
297
- if (acc > EPS && isIntegral(acc)) close();
460
+ if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
298
461
  });
299
462
  close();
300
463
  return cells;
301
464
  }
302
- var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
303
- var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
304
465
  function resolveArcs(cells, noteCount) {
305
466
  const arcs = [];
306
467
  let openStart;
@@ -369,15 +530,21 @@ function bracketGlyphs(note, edge) {
369
530
  function matraGlyph(ctx) {
370
531
  return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
371
532
  }
372
- function renderKan(note, ctx) {
373
- if (!note.kan) return "";
374
- const [octave, letter] = note.kan;
533
+ function renderGrace(svara, cls, ctx) {
534
+ if (svara === void 0) return "";
535
+ const [octave, letter] = svara;
375
536
  const c = composeCluster(letter, { octave, withMatra: false, lang: ctx.lang });
376
- 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 };
377
544
  }
378
545
  function renderPitchedInner(note, opts, ctx) {
379
546
  let inner = bracketGlyphs(note, "open");
380
- inner += renderKan(note, ctx);
547
+ inner += renderGrace(note.kan, "sw-kan", ctx);
381
548
  if (note.sustain === true) inner += `<span class="sw-hy">${ctx.marks.hyphen}</span>`;
382
549
  if (note.svara !== void 0) {
383
550
  const [octave, letter] = note.svara;
@@ -385,22 +552,27 @@ function renderPitchedInner(note, opts, ctx) {
385
552
  komal: note.komal,
386
553
  tivra: note.tivra,
387
554
  octave,
388
- 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,
389
558
  lang: ctx.lang
390
559
  });
391
560
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
392
561
  } else if (note.rest === true) {
393
- inner += matraGlyph(ctx);
394
- } else if (note.sustain === true && opts.withMatra) {
395
- 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);
396
565
  }
397
566
  if (note.microtone !== void 0) {
398
567
  const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
399
568
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
400
569
  }
401
- const d = note.durationMatra ?? 1;
402
- if (isIntegral(d) && d > 1 + EPS) inner += matraGlyph(ctx).repeat(Math.round(d) - 1);
403
- 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);
404
576
  inner += bracketGlyphs(note, "close");
405
577
  return inner;
406
578
  }
@@ -417,12 +589,12 @@ function renderAnnotation(notes, ctx) {
417
589
  continue;
418
590
  }
419
591
  if (cell.kind === "group") {
420
- const cuts = cutMarkCount(cell.notes[0].raw);
592
+ const { matras, half } = cellMatra(cell);
593
+ const cuts = half ? 0 : cutMarkCount(cell.notes[0].raw);
421
594
  cell.notes.forEach((note2, j) => {
422
595
  const last = j === cell.notes.length - 1;
423
- parts.push(
424
- `<span class="sw-n">${renderPitchedInner(note2, { withMatra: last, cuts: last ? cuts : 0 }, ctx)}</span>`
425
- );
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>`);
426
598
  });
427
599
  continue;
428
600
  }
@@ -460,7 +632,8 @@ function renderRow(row, partIdx, rowIdx, ctx) {
460
632
  const firstCol = cell.start + 1;
461
633
  const lastCol = cell.start + cell.notes.length;
462
634
  const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
463
- 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;
464
637
  cell.notes.forEach((note, j) => {
465
638
  const col = cell.start + j + 1;
466
639
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
@@ -471,8 +644,9 @@ function renderRow(row, partIdx, rowIdx, ctx) {
471
644
  );
472
645
  } else if (cell.kind === "group") {
473
646
  const last = j === cell.notes.length - 1;
647
+ const pOpts = last ? { matras, half, cuts } : { matras: 0, half: false, cuts: 0 };
474
648
  notation.push(
475
- `<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>`
476
650
  );
477
651
  } else {
478
652
  const raw = rawDisplay(note);
@@ -545,9 +719,9 @@ function renderToHtml(doc, opts = {}) {
545
719
 
546
720
  // src/viewer/styles.ts
547
721
  var NOTATION_FONT_URLS = {
548
- Swarabitan: "https://webfonts-4qq.pages.dev/swarabitan/Swarabitan.ttf",
549
- ome_bhatkhande_hindi: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
550
- ome_bhatkhande_english: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
722
+ Swarabitan: "https://lipighor.pages.dev/swarabitan/Swarabitan.ttf",
723
+ ome_bhatkhande_hindi: "https://lipighor.pages.dev/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
724
+ ome_bhatkhande_english: "https://lipighor.pages.dev/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
551
725
  };
552
726
  NOTATION_FONT_URLS.Swarabitan;
553
727
  var FONT_FACES = Object.entries(NOTATION_FONT_URLS).map(
@@ -563,9 +737,9 @@ ${FONT_FACES}
563
737
  }
564
738
 
565
739
  /* ---- notation font, per language (the note clusters + grace notes) ---- */
566
- .sw-sheet[lang="bn"] .sw-cl, .sw-sheet[lang="bn"] .sw-kan { font-family: Swarabitan, var(--sw-font, sans-serif); }
567
- .sw-sheet[lang="hi"] .sw-cl, .sw-sheet[lang="hi"] .sw-kan { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
568
- .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); }
569
743
 
570
744
  /* ---- header ---- */
571
745
  .sw-header {
@@ -624,18 +798,44 @@ ${FONT_FACES}
624
798
  .sw-ati-taar::before { top: -0.3em; }
625
799
  .sw-ati-mandra::before { bottom: -0.35em; }
626
800
 
627
- /* kan grace: small raised cluster left of the main note */
628
- .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 {
629
804
  position: relative;
630
805
  display: inline-block;
631
806
  font-size: 0.58em;
632
807
  vertical-align: 0.75em;
633
- margin-right: 0.08em;
634
808
  }
809
+ .sw-kan { margin-right: 0.08em; }
810
+ .sw-resh { margin-left: 0.08em; }
635
811
 
636
812
  .sw-hy { padding-right: 0.02em; }
637
813
  .sw-iso { display: inline-block; margin-left: -0.18em; }
638
- .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; }
639
839
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
640
840
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
641
841
 
@@ -746,129 +946,6 @@ var SWARALIPI_STAFF_CSS = `
746
946
  .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
747
947
  `;
748
948
 
749
- // src/player/pitch.ts
750
- var OCTAVE_OFFSET = {
751
- "ati-mandra": -2,
752
- mandra: -1,
753
- madhyam: 0,
754
- taar: 1,
755
- "ati-taar": 2
756
- };
757
- var SHUDDHA = {
758
- s: 0,
759
- r: 2,
760
- g: 4,
761
- m: 5,
762
- p: 7,
763
- d: 9,
764
- n: 11
765
- };
766
- var SHARP_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
767
- var PITCH_CLASS = {
768
- c: 0,
769
- "c#": 1,
770
- db: 1,
771
- d: 2,
772
- "d#": 3,
773
- eb: 3,
774
- e: 4,
775
- "e#": 5,
776
- fb: 4,
777
- f: 5,
778
- "f#": 6,
779
- gb: 6,
780
- g: 7,
781
- "g#": 8,
782
- ab: 8,
783
- a: 9,
784
- "a#": 10,
785
- bb: 10,
786
- b: 11,
787
- "b#": 0,
788
- cb: 11
789
- };
790
- function parseTonic(tonic) {
791
- if (!tonic) return 0;
792
- const key = tonic.trim().toLowerCase().replace(/s/g, "#");
793
- return PITCH_CLASS[key] ?? 0;
794
- }
795
- function svaraSemitone(note) {
796
- if (note.svara === void 0) return null;
797
- const [, letter] = note.svara;
798
- let semi = SHUDDHA[letter];
799
- if (note.komal === true || note.microtone !== void 0) semi -= 1;
800
- if (note.tivra === true) semi += 1;
801
- return semi;
802
- }
803
- function svaraToMidi(note, tonicPc, baseOctave = 4) {
804
- const semi = svaraSemitone(note);
805
- if (semi === null || note.svara === void 0) return null;
806
- const [octave] = note.svara;
807
- return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + semi;
808
- }
809
- function kanToMidi(svara, tonicPc, baseOctave = 4) {
810
- if (svara === void 0) return null;
811
- const [octave, letter] = svara;
812
- return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + SHUDDHA[letter];
813
- }
814
- function midiToSampleName(midi) {
815
- const pc = (midi % 12 + 12) % 12;
816
- const octave = Math.floor(midi / 12) - 1;
817
- return `${SHARP_NAMES[pc]}${octave}`;
818
- }
819
- function sampleFileName(midi) {
820
- return midiToSampleName(midi).toLowerCase().replace("#", "s");
821
- }
822
-
823
- // src/player/schedule.ts
824
- var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true;
825
- var SAM_TICK = /^\d+[fF]$/;
826
- function buildSchedule(doc, opts = {}) {
827
- const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
828
- const baseOctave = opts.baseOctave ?? 4;
829
- const events = [];
830
- let cursor = 0;
831
- let lastPitched = -1;
832
- let samBeat;
833
- doc.parts.forEach((part, p) => {
834
- part.rows.forEach((row, r) => {
835
- row.notes.forEach((note, i) => {
836
- if (samBeat === void 0 && note.above !== void 0) {
837
- for (const a of note.above) {
838
- if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
839
- samBeat = cursor;
840
- break;
841
- }
842
- }
843
- }
844
- if (note.bar !== void 0 || isSilent(note)) return;
845
- const path = `${p}.${r}.${i}`;
846
- const dur = note.durationMatra ?? 1;
847
- if (note.svara !== void 0) {
848
- events.push({
849
- beat: cursor,
850
- durationBeats: dur,
851
- midi: svaraToMidi(note, tonicPc, baseOctave),
852
- kan: kanToMidi(note.kan, tonicPc, baseOctave),
853
- path,
854
- kind: note.sustain === true ? "sustain" : "note"
855
- });
856
- lastPitched = events.length - 1;
857
- } else if (note.sustain === true) {
858
- if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
859
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "sustain" });
860
- } else {
861
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "rest" });
862
- }
863
- cursor += dur;
864
- });
865
- });
866
- });
867
- const numBeats = doc.meta.taal?.numBeats;
868
- const pickupBeats = samBeat !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samBeat % numBeats) % numBeats : 0;
869
- return { events, totalBeats: cursor, pickupBeats };
870
- }
871
-
872
949
  // src/viewer/staff.ts
873
950
  init_taals();
874
951
 
@@ -1034,20 +1111,23 @@ function buildAbc(schedule, lyricOf, opts) {
1034
1111
  barPos -= beatsPerBar;
1035
1112
  }
1036
1113
  };
1037
- for (const ev of schedule.events) {
1114
+ schedule.events.forEach((ev, i) => {
1038
1115
  if (ev.midi === null) {
1039
1116
  if (ev.kind === "rest") {
1040
1117
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
1041
1118
  words.push("*");
1042
1119
  advance(ev.durationBeats);
1043
1120
  }
1044
- continue;
1121
+ return;
1045
1122
  }
1046
- 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}`);
1047
1127
  const lyr = lyricOf(ev);
1048
1128
  words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
1049
1129
  advance(ev.durationBeats);
1050
- }
1130
+ });
1051
1131
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1052
1132
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
1053
1133
  const meter = ratioMeter(beatsPerBar);
@@ -1218,26 +1298,26 @@ function escapeHtml2(s) {
1218
1298
  }
1219
1299
  function watermarkCss(text) {
1220
1300
  if (!text) return "";
1301
+ const span = 92;
1302
+ const size = Math.min(18, span / (0.62 * text.length));
1303
+ const len = Math.min(span, 0.62 * text.length * size);
1304
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><text x="50" y="50" dy=".35em" text-anchor="middle" font-family="sans-serif" font-weight="700" fill="rgba(0,0,0,0.07)" font-size="${size.toFixed(2)}" textLength="${len.toFixed(2)}" lengthAdjust="spacingAndGlyphs" transform="rotate(-35 50 50)">${escapeHtml2(text)}</text></svg>`;
1221
1305
  return `
1222
1306
  .sw-watermark {
1223
1307
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
1224
- display: flex; align-items: center; justify-content: center;
1225
- overflow: hidden;
1308
+ background: url("data:image/svg+xml,${encodeURIComponent(svg)}") center / contain no-repeat;
1226
1309
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
1227
1310
  }
1228
- .sw-watermark span {
1229
- font-size: 11vw; font-weight: 700; letter-spacing: .08em;
1230
- color: rgba(0,0,0,0.07); transform: rotate(-35deg); white-space: nowrap;
1231
- }
1232
1311
  body > .sw-sheet, body > .sw-footer { position: relative; z-index: 1; }
1233
1312
  `;
1234
1313
  }
1235
1314
  function chrome(text, notator) {
1236
- const top = text ? `<div class="sw-watermark" aria-hidden="true"><span>${escapeHtml2(text)}</span></div>` : "";
1315
+ const top = text ? '<div class="sw-watermark" aria-hidden="true"></div>' : "";
1237
1316
  const bottom = notator ? `<footer class="sw-footer">${escapeHtml2(notator)}</footer>` : "";
1238
1317
  return { top, bottom };
1239
1318
  }
1240
1319
  function resolveNotator(doc, opts) {
1320
+ if (opts.notator === "") return "";
1241
1321
  return doc.meta.notationBy ?? opts.notator ?? DEFAULT_NOTATOR;
1242
1322
  }
1243
1323
  function pageHtml(lang, title, bodyHtml, opts, doc) {
@@ -1516,6 +1596,7 @@ exports.abcDuration = abcDuration;
1516
1596
  exports.abcToSvg = abcToSvg;
1517
1597
  exports.buildAbc = buildAbc;
1518
1598
  exports.buildTaalAbc = buildTaalAbc;
1599
+ exports.cellMatra = cellMatra;
1519
1600
  exports.composeCluster = composeCluster;
1520
1601
  exports.defineSwaralipiSheet = defineSwaralipiSheet;
1521
1602
  exports.docToAbc = docToAbc;