@ugfoundation/swaralipi-js 0.2.1 → 0.3.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 (37) hide show
  1. package/dist/{batch-SCSIUDB3.js → batch-TVEIT2OJ.js} +4 -4
  2. package/dist/{batch-SCSIUDB3.js.map → batch-TVEIT2OJ.js.map} +1 -1
  3. package/dist/{chunk-KYNCQ7OF.js → chunk-HDN3YZ6K.js} +3 -3
  4. package/dist/{chunk-KYNCQ7OF.js.map → chunk-HDN3YZ6K.js.map} +1 -1
  5. package/dist/chunk-V5ZSUJCA.js +354 -0
  6. package/dist/chunk-V5ZSUJCA.js.map +1 -0
  7. package/dist/{chunk-DINRYGRI.js → chunk-WBSIKYWB.js} +3 -3
  8. package/dist/{chunk-DINRYGRI.js.map → chunk-WBSIKYWB.js.map} +1 -1
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2 -2
  12. package/dist/convert/index.cjs +1 -1
  13. package/dist/convert/index.cjs.map +1 -1
  14. package/dist/convert/index.js +2 -2
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/player/index.cjs +117 -20
  19. package/dist/player/index.cjs.map +1 -1
  20. package/dist/player/index.d.cts +24 -164
  21. package/dist/player/index.d.ts +24 -164
  22. package/dist/player/index.js +91 -343
  23. package/dist/player/index.js.map +1 -1
  24. package/dist/schedule-BhiP_maG.d.ts +167 -0
  25. package/dist/schedule-Dsb2paUE.d.cts +167 -0
  26. package/dist/spec/index.cjs +1 -1
  27. package/dist/spec/index.cjs.map +1 -1
  28. package/dist/spec/index.d.cts +1 -1
  29. package/dist/spec/index.d.ts +1 -1
  30. package/dist/spec/index.js +1 -1
  31. package/dist/viewer/index.cjs +584 -54
  32. package/dist/viewer/index.cjs.map +1 -1
  33. package/dist/viewer/index.d.cts +177 -18
  34. package/dist/viewer/index.d.ts +177 -18
  35. package/dist/viewer/index.js +322 -30
  36. package/dist/viewer/index.js.map +1 -1
  37. package/package.json +7 -2
@@ -11,6 +11,30 @@ var __export = (target, all) => {
11
11
  for (var name in all)
12
12
  __defProp(target, name, { get: all[name], enumerable: true });
13
13
  };
14
+
15
+ // src/spec/taals.ts
16
+ var TAALS;
17
+ var init_taals = __esm({
18
+ "src/spec/taals.ts"() {
19
+ TAALS = {
20
+ dadra: { id: "dadra", numBeats: 6, bhaags: [3, 3], sam: 0, khaali: [3] },
21
+ kehrwa: { id: "kehrwa", numBeats: 8, bhaags: [4, 4], sam: 0, khaali: [4] },
22
+ rupak: { id: "rupak", numBeats: 7, bhaags: [3, 2, 2], sam: 0, khaali: [0] },
23
+ jhaptaal: { id: "jhaptaal", numBeats: 10, bhaags: [2, 3, 2, 3], sam: 0, khaali: [5] },
24
+ ektaal: { id: "ektaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
25
+ adachautaal: { id: "adachautaal", numBeats: 14, sam: 0 },
26
+ choutaal: { id: "choutaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
27
+ // চৌতাল
28
+ shashthi: { id: "shashthi", numBeats: 6, bhaags: [2, 4], sam: 0 },
29
+ // ষষ্ঠী
30
+ teora: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
31
+ // টেওরা
32
+ teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] },
33
+ aratheka: { id: "aratheka", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
34
+ // আড়াঠেকা
35
+ };
36
+ }
37
+ });
14
38
  var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, noteSchema, rowSchema, partSchema, taalSchema, metaSchema, swaralipiSchema;
15
39
  var init_schema = __esm({
16
40
  "src/spec/schema.ts"() {
@@ -123,30 +147,6 @@ var init_schema = __esm({
123
147
  }
124
148
  });
125
149
 
126
- // src/spec/taals.ts
127
- var TAALS;
128
- var init_taals = __esm({
129
- "src/spec/taals.ts"() {
130
- TAALS = {
131
- dadra: { id: "dadra", numBeats: 6, bhaags: [3, 3], sam: 0, khaali: [3] },
132
- kehrwa: { id: "kehrwa", numBeats: 8, bhaags: [4, 4], sam: 0, khaali: [4] },
133
- rupak: { id: "rupak", numBeats: 7, bhaags: [3, 2, 2], sam: 0, khaali: [0] },
134
- jhaptaal: { id: "jhaptaal", numBeats: 10, bhaags: [2, 3, 2, 3], sam: 0, khaali: [5] },
135
- ektaal: { id: "ektaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
136
- adachautaal: { id: "adachautaal", numBeats: 14, sam: 0 },
137
- choutaal: { id: "choutaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
138
- // চৌতাল
139
- shashthi: { id: "shashthi", numBeats: 6, bhaags: [2, 4], sam: 0 },
140
- // ষষ্ঠী
141
- teora: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
142
- // টেওরা
143
- teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] },
144
- aratheka: { id: "aratheka", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
145
- // আড়াঠেকা
146
- };
147
- }
148
- });
149
-
150
150
  // src/spec/index.ts
151
151
  var spec_exports = {};
152
152
  __export(spec_exports, {
@@ -179,7 +179,7 @@ var init_spec = __esm({
179
179
  init_schema();
180
180
  init_schema();
181
181
  init_taals();
182
- SPEC_VERSION = "0.2.1";
182
+ SPEC_VERSION = "0.3.1";
183
183
  }
184
184
  });
185
185
 
@@ -221,9 +221,9 @@ var MARKS = {
221
221
  en: { hyphen: "-", melisma: "\xB7", cut: ":" }
222
222
  };
223
223
  var LABELS = {
224
- 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" },
225
- 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" },
226
- en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notation" }
224
+ 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" },
225
+ 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" },
226
+ en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notator" }
227
227
  };
228
228
  function composeCluster(letter, opts) {
229
229
  const classes = [];
@@ -506,21 +506,24 @@ function renderRow(row, partIdx, rowIdx, ctx) {
506
506
  return `<div class="sw-row">${label}<div class="sw-grid">${above.join("")}${notation.join("")}${below.join("")}${lyric.join("")}</div></div>`;
507
507
  }
508
508
  function renderHeader(meta, ctx) {
509
- const L = LABELS[ctx.lang];
509
+ return renderMetaHeader(meta, ctx.lang, ctx.tr);
510
+ }
511
+ function renderMetaHeader(meta, lang, tr = (s) => s) {
512
+ const L = LABELS[lang];
510
513
  const rows = [];
511
514
  const add = (label, value) => rows.push(
512
515
  `<div class="sw-meta-row"><span class="sw-meta-label">${escapeHtml(label)}</span><span class="sw-meta-value">${escapeHtml(value)}</span></div>`
513
516
  );
514
- add(L.title, ctx.tr(meta.title));
517
+ add(L.title, tr(meta.title));
515
518
  const taal = meta.taalName ?? meta.taal?.id;
516
- if (taal !== void 0) add(L.taal, ctx.tr(taal));
517
- if (meta.raag !== void 0) add(L.raag, ctx.tr(meta.raag));
518
- if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), ctx.lang));
519
- if (meta.notationBy !== void 0) add(L.notationBy, ctx.tr(meta.notationBy));
519
+ if (taal !== void 0) add(L.taal, tr(taal));
520
+ if (meta.raag !== void 0) add(L.raag, tr(meta.raag));
521
+ if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), lang));
522
+ if (meta.notationBy !== void 0) add(L.notationBy, tr(meta.notationBy));
520
523
  return `<header class="sw-header">${rows.join("")}</header>`;
521
524
  }
522
525
  function renderToHtml(doc, opts = {}) {
523
- const lang = opts.lang ?? "bn";
526
+ const lang = opts.lang === "hi" || opts.lang === "en" ? opts.lang : "bn";
524
527
  const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
525
528
  const ctx = { lang, marks: MARKS[lang], tr };
526
529
  const out = [`<div class="sw-sheet" lang="${lang}">`];
@@ -538,9 +541,9 @@ function renderToHtml(doc, opts = {}) {
538
541
 
539
542
  // src/viewer/styles.ts
540
543
  var NOTATION_FONT_URLS = {
541
- Swarabitan: "https://cdn.daserve.in/webfonts/swarabitan/Swarabitan.ttf",
542
- ome_bhatkhande_hindi: "https://cdn.daserve.in/webfonts/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
543
- ome_bhatkhande_english: "https://cdn.daserve.in/webfonts/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
544
+ Swarabitan: "https://webfonts-4qq.pages.dev/swarabitan/Swarabitan.ttf",
545
+ ome_bhatkhande_hindi: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
546
+ ome_bhatkhande_english: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
544
547
  };
545
548
  NOTATION_FONT_URLS.Swarabitan;
546
549
  var FONT_FACES = Object.entries(NOTATION_FONT_URLS).map(
@@ -690,6 +693,9 @@ ${FONT_FACES}
690
693
 
691
694
  /* ---- player sync (P2.3) ---- */
692
695
  .sw-hl { background: var(--sw-hl, rgba(255, 196, 64, 0.45)); border-radius: 0.15em; }
696
+ /* Staff notes are SVG <g> \u2014 background doesn't paint there, so recolour the
697
+ glyph fill/stroke instead (abcjs draws note heads/stems as filled paths). */
698
+ .sw-staff .sw-hl, .sw-staff .sw-hl * { fill: var(--sw-hl-note, #d68400); stroke: var(--sw-hl-note, #d68400); }
693
699
 
694
700
  /* ---- element error box ---- */
695
701
  .sw-error {
@@ -705,27 +711,500 @@ ${FONT_FACES}
705
711
  var SWARALIPI_PRINT_CSS = `
706
712
  @page { margin: 16mm; }
707
713
  @media print {
708
- .sw-sheet { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
714
+ .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
709
715
  .sw-row { overflow: visible !important; break-inside: avoid; page-break-inside: avoid; }
710
716
  .sw-part { break-inside: auto; }
711
717
  .sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
712
718
  .sw-hl { background: none !important; }
719
+ /* Keep each engraved staff system whole across a page break (the abcjs line
720
+ is a <div> wrapping the <svg>, so the break rule must sit on the div). */
721
+ .sw-staff-lines > div { break-inside: avoid; page-break-inside: avoid; }
722
+ .sw-staff-meta { break-after: avoid-page; page-break-after: avoid; }
713
723
  }
714
724
  `;
725
+ var SWARALIPI_STAFF_CSS = `
726
+ .sw-staff { color: currentColor; }
727
+ .sw-staff-meta { display: flex; gap: 1.25em; align-items: baseline; margin: 0 0 .5em; font-size: .95em; opacity: .85; }
728
+ .sw-staff-instrument { font-weight: 600; }
729
+ .sw-staff-drums { opacity: .75; }
730
+ .sw-staff-tempo { font-variant-numeric: tabular-nums; }
731
+ /* abcjs (oneSvgPerLine) wraps the music into one <div><svg> per system; stack
732
+ them as short, swaralipi-style lines. abcjs sets a fixed pixel height +
733
+ overflow:hidden on each wrapper div \u2014 neutralise it so the viewBox'd SVG can
734
+ scale to the container (and to the print page) instead of being clipped. */
735
+ .sw-staff-lines > div { margin: 0 0 .35em; height: auto !important; overflow: visible !important; }
736
+ /* Fill up to the engraved (staffwidth) size \u2014 shrink to fit a narrow container,
737
+ but never stretch past natural width (abcToSvg pins per-SVG max-width in px),
738
+ else a short last system balloons. */
739
+ .sw-staff-lines svg { display: block; width: 100%; height: auto; }
740
+ .sw-staff-missing { padding: 1em; border: 1px dashed currentColor; border-radius: 6px; opacity: .8; font-size: .9em; }
741
+ .sw-staff-title { font-size: 1.15em; font-weight: 700; }
742
+ .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
743
+ `;
744
+
745
+ // src/player/pitch.ts
746
+ var OCTAVE_OFFSET = {
747
+ "ati-mandra": -2,
748
+ mandra: -1,
749
+ madhyam: 0,
750
+ taar: 1,
751
+ "ati-taar": 2
752
+ };
753
+ var SHUDDHA = {
754
+ s: 0,
755
+ r: 2,
756
+ g: 4,
757
+ m: 5,
758
+ p: 7,
759
+ d: 9,
760
+ n: 11
761
+ };
762
+ var SHARP_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
763
+ var PITCH_CLASS = {
764
+ c: 0,
765
+ "c#": 1,
766
+ db: 1,
767
+ d: 2,
768
+ "d#": 3,
769
+ eb: 3,
770
+ e: 4,
771
+ "e#": 5,
772
+ fb: 4,
773
+ f: 5,
774
+ "f#": 6,
775
+ gb: 6,
776
+ g: 7,
777
+ "g#": 8,
778
+ ab: 8,
779
+ a: 9,
780
+ "a#": 10,
781
+ bb: 10,
782
+ b: 11,
783
+ "b#": 0,
784
+ cb: 11
785
+ };
786
+ function parseTonic(tonic) {
787
+ if (!tonic) return 0;
788
+ const key = tonic.trim().toLowerCase().replace(/s/g, "#");
789
+ return PITCH_CLASS[key] ?? 0;
790
+ }
791
+ function svaraSemitone(note) {
792
+ if (note.svara === void 0) return null;
793
+ const [, letter] = note.svara;
794
+ let semi = SHUDDHA[letter];
795
+ if (note.komal === true || note.microtone !== void 0) semi -= 1;
796
+ if (note.tivra === true) semi += 1;
797
+ return semi;
798
+ }
799
+ function svaraToMidi(note, tonicPc, baseOctave = 4) {
800
+ const semi = svaraSemitone(note);
801
+ if (semi === null || note.svara === void 0) return null;
802
+ const [octave] = note.svara;
803
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + semi;
804
+ }
805
+ function kanToMidi(svara, tonicPc, baseOctave = 4) {
806
+ if (svara === void 0) return null;
807
+ const [octave, letter] = svara;
808
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + SHUDDHA[letter];
809
+ }
810
+ function midiToSampleName(midi) {
811
+ const pc = (midi % 12 + 12) % 12;
812
+ const octave = Math.floor(midi / 12) - 1;
813
+ return `${SHARP_NAMES[pc]}${octave}`;
814
+ }
815
+ function sampleFileName(midi) {
816
+ return midiToSampleName(midi).toLowerCase().replace("#", "s");
817
+ }
818
+
819
+ // src/player/schedule.ts
820
+ var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true;
821
+ function buildSchedule(doc, opts = {}) {
822
+ const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
823
+ const baseOctave = opts.baseOctave ?? 4;
824
+ const events = [];
825
+ let cursor = 0;
826
+ let lastPitched = -1;
827
+ doc.parts.forEach((part, p) => {
828
+ part.rows.forEach((row, r) => {
829
+ row.notes.forEach((note, i) => {
830
+ if (note.bar !== void 0 || isSilent(note)) return;
831
+ const path = `${p}.${r}.${i}`;
832
+ const dur = note.durationMatra ?? 1;
833
+ if (note.svara !== void 0) {
834
+ events.push({
835
+ beat: cursor,
836
+ durationBeats: dur,
837
+ midi: svaraToMidi(note, tonicPc, baseOctave),
838
+ kan: kanToMidi(note.kan, tonicPc, baseOctave),
839
+ path,
840
+ kind: note.sustain === true ? "sustain" : "note"
841
+ });
842
+ lastPitched = events.length - 1;
843
+ } else if (note.sustain === true) {
844
+ if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
845
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "sustain" });
846
+ } else {
847
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "rest" });
848
+ }
849
+ cursor += dur;
850
+ });
851
+ });
852
+ });
853
+ return { events, totalBeats: cursor };
854
+ }
855
+
856
+ // src/viewer/staff.ts
857
+ init_taals();
858
+
859
+ // src/player/instruments.ts
860
+ init_taals();
861
+ function keyMap(keys) {
862
+ const m = {};
863
+ for (const k of keys) m[k] = `${k.replace("#", "s")}.mp3`;
864
+ return m;
865
+ }
866
+ var PIANO_KEYS = [
867
+ "A0",
868
+ "C1",
869
+ "D#1",
870
+ "F#1",
871
+ "A1",
872
+ "C2",
873
+ "D#2",
874
+ "F#2",
875
+ "A2",
876
+ "C3",
877
+ "D#3",
878
+ "F#3",
879
+ "A3",
880
+ "C4",
881
+ "D#4",
882
+ "F#4",
883
+ "A4",
884
+ "C5",
885
+ "D#5",
886
+ "F#5",
887
+ "A5",
888
+ "C6",
889
+ "D#6",
890
+ "F#6",
891
+ "A6",
892
+ "C7",
893
+ "D#7",
894
+ "F#7",
895
+ "A7",
896
+ "C8"
897
+ ];
898
+ var HARMONIUM_KEYS = [
899
+ "C2",
900
+ "C#2",
901
+ "D2",
902
+ "D#2",
903
+ "E2",
904
+ "F2",
905
+ "F#2",
906
+ "G2",
907
+ "G#2",
908
+ "A2",
909
+ "A#2",
910
+ "C3",
911
+ "C#3",
912
+ "D3",
913
+ "D#3",
914
+ "E3",
915
+ "F3",
916
+ "F#3",
917
+ "G3",
918
+ "G#3",
919
+ "A3",
920
+ "A#3",
921
+ "C4",
922
+ "C#4",
923
+ "D4",
924
+ "D#4",
925
+ "E4",
926
+ "F4",
927
+ "F#4",
928
+ "G4",
929
+ "G#4",
930
+ "A4",
931
+ "A#4",
932
+ "C5",
933
+ "C#5",
934
+ "D5"
935
+ ];
936
+ var VIOLIN_KEYS = ["A3", "C4", "E4", "G4", "A4", "C5", "E5", "G5", "A5", "C6", "E6", "G6", "A6", "C7"];
937
+ var FLUTE_KEYS = ["C4", "E4", "A4", "C5", "E5", "A5", "C6", "E6", "A6", "C7"];
938
+ ({
939
+ piano: { urls: keyMap(PIANO_KEYS)},
940
+ santoor: { urls: santoorUrls()},
941
+ harmonium: { urls: keyMap(HARMONIUM_KEYS)},
942
+ violin: { urls: keyMap(VIOLIN_KEYS)},
943
+ flute: { urls: keyMap(FLUTE_KEYS)}
944
+ });
945
+ new Set(
946
+ Object.keys(TAALS)
947
+ );
948
+ Array.from({ length: 17 }, (_, i) => 60 + i * 15);
949
+ function santoorUrls() {
950
+ const urls = {};
951
+ const add = (midi) => {
952
+ const stem = sampleFileName(midi);
953
+ const name = stem.replace("s", "#").replace(/^(.)/, (c) => c.toUpperCase());
954
+ urls[name] = `${stem}.mp3`;
955
+ };
956
+ for (let midi = 60; midi <= 95; midi++) add(midi);
957
+ add(96);
958
+ return urls;
959
+ }
960
+ function drumPattern(taal) {
961
+ const n = taal.numBeats;
962
+ const sam = taal.sam ?? 0;
963
+ const khaali = new Set(taal.khaali ?? []);
964
+ const bhaags = taal.bhaags ?? [n];
965
+ const pattern = new Array(n).fill("hihat");
966
+ let pos = 0;
967
+ for (const len of bhaags) {
968
+ pattern[pos] = khaali.has(pos) ? "hihat" : "snare";
969
+ pos += len;
970
+ }
971
+ pattern[sam] = "kick";
972
+ return pattern;
973
+ }
974
+
975
+ // src/viewer/staff.ts
976
+ var ABC_SHARP = ["C", "^C", "D", "^D", "E", "F", "^F", "G", "^G", "A", "^A", "B"];
977
+ function midiToAbc(midi) {
978
+ const pc = (midi % 12 + 12) % 12;
979
+ const octave = Math.floor(midi / 12) - 1;
980
+ const token = ABC_SHARP[pc];
981
+ const acc = token[0] === "^" ? "^" : "";
982
+ const letter = acc ? token[1] : token[0];
983
+ return octave >= 5 ? acc + letter.toLowerCase() + "'".repeat(octave - 5) : acc + letter + ",".repeat(4 - octave);
984
+ }
985
+ function abcDuration(beats) {
986
+ if (Math.abs(beats - 1) < 1e-6) return "";
987
+ if (Math.abs(beats - Math.round(beats)) < 1e-6) return String(Math.round(beats));
988
+ const inv = 1 / beats;
989
+ if (Math.abs(inv - Math.round(inv)) < 1e-6) return `/${Math.round(inv)}`;
990
+ const sixteenths = Math.max(1, Math.round(beats * 4));
991
+ return `${sixteenths}/4`;
992
+ }
993
+ function abcNote(ev) {
994
+ const grace = ev.kan !== null ? `{${midiToAbc(ev.kan)}}` : "";
995
+ return `${grace}${midiToAbc(ev.midi)}${abcDuration(ev.durationBeats)}`;
996
+ }
997
+ function eventPaths(schedule) {
998
+ const paths = [];
999
+ for (const ev of schedule.events) {
1000
+ if (ev.midi !== null || ev.kind === "rest") paths.push(ev.path);
1001
+ }
1002
+ return paths;
1003
+ }
1004
+ function abcText(s) {
1005
+ return s.replace(/[\r\n]+/g, " ").trim();
1006
+ }
1007
+ function buildAbc(schedule, lyricOf, opts) {
1008
+ const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
1009
+ const tokens = [];
1010
+ const words = [];
1011
+ let barPos = 0;
1012
+ const EPS2 = 1e-6;
1013
+ const advance = (dur) => {
1014
+ barPos += dur;
1015
+ while (barPos >= beatsPerBar - EPS2) {
1016
+ tokens.push("|");
1017
+ words.push("|");
1018
+ barPos -= beatsPerBar;
1019
+ }
1020
+ };
1021
+ for (const ev of schedule.events) {
1022
+ if (ev.midi === null) {
1023
+ if (ev.kind === "rest") {
1024
+ tokens.push(`z${abcDuration(ev.durationBeats)}`);
1025
+ words.push("*");
1026
+ advance(ev.durationBeats);
1027
+ }
1028
+ continue;
1029
+ }
1030
+ tokens.push(abcNote(ev));
1031
+ const lyr = lyricOf(ev);
1032
+ words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
1033
+ advance(ev.durationBeats);
1034
+ }
1035
+ if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1036
+ else if (tokens.length) tokens[tokens.length - 1] = "|]";
1037
+ const meter = ratioMeter(beatsPerBar);
1038
+ const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
1039
+ if (opts.drums) head.push(...percMapLines(opts.drums));
1040
+ head.push("K:C treble");
1041
+ const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
1042
+ const lines = [...head];
1043
+ if (opts.drums) {
1044
+ lines.push("V:1", melodyBody);
1045
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1046
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, beatsPerBar, opts.drums));
1047
+ } else {
1048
+ lines.push(melodyBody);
1049
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1050
+ }
1051
+ return lines.join("\n");
1052
+ }
1053
+ function ratioMeter(beats) {
1054
+ return `${Math.max(1, Math.round(beats))}/4`;
1055
+ }
1056
+ var DRUM_ABC = {
1057
+ kick: { note: "F,", gm: "bass-drum-1" },
1058
+ snare: { note: "C", gm: "acoustic-snare" },
1059
+ hihat: { note: "g", gm: "closed-hi-hat" },
1060
+ tom1: { note: "E", gm: "high-tom" },
1061
+ tom2: { note: "A,", gm: "low-mid-tom" },
1062
+ tom3: { note: "D,", gm: "low-floor-tom" }
1063
+ };
1064
+ function percMapLines(voices) {
1065
+ const seen = /* @__PURE__ */ new Set();
1066
+ const lines = [];
1067
+ for (const v of voices) {
1068
+ if (seen.has(v)) continue;
1069
+ seen.add(v);
1070
+ const { note, gm } = DRUM_ABC[v];
1071
+ lines.push(`%%percmap ${note} ${gm}`);
1072
+ }
1073
+ return lines;
1074
+ }
1075
+ function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
1076
+ const n = Math.max(1, Math.ceil(totalBeats));
1077
+ const tokens = [];
1078
+ for (let beat = 0; beat < n; beat++) {
1079
+ tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
1080
+ if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
1081
+ }
1082
+ if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
1083
+ else tokens[tokens.length - 1] = "|]";
1084
+ return tokens.join(" ");
1085
+ }
1086
+ function lyricLookup(doc) {
1087
+ return (ev) => {
1088
+ const [p, r, i] = ev.path.split(".").map(Number);
1089
+ return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
1090
+ };
1091
+ }
1092
+ function resolveDrumTaal(doc) {
1093
+ const id = doc.meta.taal?.id;
1094
+ return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
1095
+ }
1096
+ function docToAbc(doc, opts = {}) {
1097
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
1098
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1099
+ const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
1100
+ const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
1101
+ return buildAbc(schedule, lyricLookup(doc), { bpm, lyrics: opts.lyrics !== false, beatsPerBar, drums });
1102
+ }
1103
+ async function abcToSvg(abc, opts = {}, notePaths) {
1104
+ if (typeof document === "undefined") {
1105
+ throw new Error("abcToSvg requires a DOM; render staff notation in the browser or print iframe");
1106
+ }
1107
+ let ABCJS;
1108
+ try {
1109
+ const mod = await import('abcjs');
1110
+ ABCJS = mod.default ?? mod;
1111
+ } catch {
1112
+ return `<div class="sw-staff-missing">Staff notation needs the optional <code>abcjs</code> package.</div>`;
1113
+ }
1114
+ const host = document.createElement("div");
1115
+ host.style.cssText = "position:fixed; left:-10000px; top:0; width:720px; visibility:hidden;";
1116
+ const mount = document.body ?? document.documentElement;
1117
+ mount.appendChild(host);
1118
+ try {
1119
+ ABCJS.renderAbc(host, abc, {
1120
+ add_classes: true,
1121
+ staffwidth: opts.staffwidth ?? 680,
1122
+ scale: opts.scale ?? 1,
1123
+ // Reflow into short lines. staffwidth is the width budget;
1124
+ // preferredMeasuresPerLine keeps systems swaralipi-short.
1125
+ // (Not `responsive: resize` — its absolute-positioned aspect-ratio wrapper
1126
+ // overlaps the header and mis-sizes on the print page.)
1127
+ wrap: { minSpacing: 1.8, maxSpacing: 2.7, preferredMeasuresPerLine: opts.measuresPerLine ?? 4 },
1128
+ // CRUCIAL: put each wrapped system in its OWN <svg> (default is all systems
1129
+ // stacked in one tall SVG, which is atomic and can't page-break — that was
1130
+ // the near-blank-first-page / mid-staff-cutoff print bug). One SVG per line
1131
+ // lets `break-inside: avoid` keep each staff whole while pages fill.
1132
+ oneSvgPerLine: true
1133
+ });
1134
+ for (const svg of host.querySelectorAll("svg")) {
1135
+ const w = parseFloat(svg.getAttribute("width") ?? "");
1136
+ const h = parseFloat(svg.getAttribute("height") ?? "");
1137
+ if (w > 0 && h > 0) {
1138
+ if (!svg.getAttribute("viewBox")) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
1139
+ svg.setAttribute("preserveAspectRatio", "xMinYMin meet");
1140
+ svg.removeAttribute("width");
1141
+ svg.removeAttribute("height");
1142
+ svg.setAttribute("style", `max-width:${w}px`);
1143
+ }
1144
+ }
1145
+ if (notePaths) {
1146
+ const melody = host.querySelectorAll(".abcjs-v0.abcjs-note, .abcjs-v0.abcjs-rest");
1147
+ let i = 0;
1148
+ for (const el of melody) {
1149
+ const path = notePaths[i++];
1150
+ if (path !== void 0) el.setAttribute("data-note", path);
1151
+ }
1152
+ }
1153
+ return host.innerHTML;
1154
+ } finally {
1155
+ host.remove();
1156
+ }
1157
+ }
1158
+ async function renderStaffHtml(doc, opts = {}) {
1159
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1160
+ const abc = docToAbc(doc, { ...opts, bpm });
1161
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
1162
+ const engraved = await abcToSvg(abc, opts, eventPaths(schedule));
1163
+ const lines = engraved.includes("sw-staff-missing") ? engraved : `<div class="sw-staff-lines">${engraved}</div>`;
1164
+ const header = opts.showHeader === false ? "" : renderMetaHeader(doc.meta, opts.headerLang ?? "en", opts.transliterate);
1165
+ const metaParts = [];
1166
+ if (opts.instrument) metaParts.push(`<span class="sw-staff-instrument">${escapeAttr(opts.instrument)}</span>`);
1167
+ if (opts.drums) metaParts.push(`<span class="sw-staff-drums">+ drums</span>`);
1168
+ const meta = metaParts.length ? `<div class="sw-staff-meta">${metaParts.join("")}</div>` : "";
1169
+ return `<div class="sw-staff">${header}${meta}${lines}</div>`;
1170
+ }
1171
+ function escapeAttr(s) {
1172
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1173
+ }
715
1174
 
716
1175
  // src/viewer/print.ts
1176
+ var DEFAULT_WATERMARK = "www.gitabitan.io";
1177
+ var DEFAULT_NOTATOR = "gitabitan.io Team";
717
1178
  function escapeHtml2(s) {
718
1179
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
719
1180
  }
720
- function renderPrintDocument(doc, opts = {}) {
721
- const lang = opts.lang ?? "bn";
722
- const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
723
- const title = opts.title ?? tr(doc.meta.title);
724
- const body = renderToHtml(doc, {
725
- lang,
726
- showHeader: opts.showHeader,
727
- transliterate: opts.transliterate
728
- });
1181
+ function watermarkCss(text) {
1182
+ if (!text) return "";
1183
+ return `
1184
+ .sw-watermark {
1185
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
1186
+ display: flex; align-items: center; justify-content: center;
1187
+ overflow: hidden;
1188
+ -webkit-print-color-adjust: exact; print-color-adjust: exact;
1189
+ }
1190
+ .sw-watermark span {
1191
+ font-size: 11vw; font-weight: 700; letter-spacing: .08em;
1192
+ color: rgba(0,0,0,0.07); transform: rotate(-35deg); white-space: nowrap;
1193
+ }
1194
+ body > .sw-sheet, body > .sw-footer { position: relative; z-index: 1; }
1195
+ `;
1196
+ }
1197
+ function chrome(text, notator) {
1198
+ const top = text ? `<div class="sw-watermark" aria-hidden="true"><span>${escapeHtml2(text)}</span></div>` : "";
1199
+ const bottom = notator ? `<footer class="sw-footer">${escapeHtml2(notator)}</footer>` : "";
1200
+ return { top, bottom };
1201
+ }
1202
+ function resolveNotator(doc, opts) {
1203
+ return doc.meta.notationBy ?? opts.notator ?? DEFAULT_NOTATOR;
1204
+ }
1205
+ function pageHtml(lang, title, bodyHtml, opts, doc) {
1206
+ const watermark = opts.watermark ?? DEFAULT_WATERMARK;
1207
+ const { top, bottom } = chrome(watermark, resolveNotator(doc, opts));
729
1208
  return `<!doctype html>
730
1209
  <html lang="${escapeHtml2(lang)}">
731
1210
  <head>
@@ -734,20 +1213,42 @@ function renderPrintDocument(doc, opts = {}) {
734
1213
  <title>${escapeHtml2(title)}</title>
735
1214
  <style>
736
1215
  ${SWARALIPI_CSS}
1216
+ ${SWARALIPI_STAFF_CSS}
737
1217
  ${SWARALIPI_PRINT_CSS}
1218
+ ${watermarkCss(watermark)}
738
1219
  html, body { margin: 0; }
739
1220
  body { color: #111; background: #fff; padding: 16px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
740
1221
  .sw-sheet { max-width: 100%; }
1222
+ .sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
741
1223
  </style>
742
1224
  </head>
743
- <body>${body}</body>
1225
+ <body>${top}${bodyHtml}${bottom}</body>
744
1226
  </html>`;
745
1227
  }
1228
+ function renderPrintDocument(doc, opts = {}) {
1229
+ const lang = opts.lang === "staff" ? "bn" : opts.lang ?? "bn";
1230
+ const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
1231
+ const title = opts.title ?? tr(doc.meta.title);
1232
+ const body = renderToHtml(doc, { lang, showHeader: opts.showHeader, transliterate: opts.transliterate });
1233
+ return pageHtml(lang, title, body, opts, doc);
1234
+ }
1235
+ async function renderStaffPrintDocument(doc, opts = {}) {
1236
+ if (opts.lang !== "staff") return renderPrintDocument(doc, opts);
1237
+ const title = opts.title ?? doc.meta.title;
1238
+ const staff = await renderStaffHtml(doc, opts);
1239
+ return pageHtml("en", title, staff, opts, doc);
1240
+ }
746
1241
  function printSwaralipi(doc, opts = {}) {
747
1242
  if (typeof document === "undefined") {
748
1243
  throw new Error("printSwaralipi requires a DOM; use renderPrintDocument() for server-side PDF");
749
1244
  }
750
- const html = renderPrintDocument(doc, opts);
1245
+ if (opts.lang === "staff") {
1246
+ void renderStaffPrintDocument(doc, opts).then(printHtmlInIframe);
1247
+ } else {
1248
+ printHtmlInIframe(renderPrintDocument(doc, opts));
1249
+ }
1250
+ }
1251
+ function printHtmlInIframe(html) {
751
1252
  const iframe = document.createElement("iframe");
752
1253
  iframe.setAttribute("aria-hidden", "true");
753
1254
  iframe.style.cssText = "position:fixed; left:-10000px; top:0; width:800px; height:600px; border:0;";
@@ -789,7 +1290,7 @@ var TARGET = {
789
1290
  en: "iast"
790
1291
  };
791
1292
  async function loadLyricTransliterator(lang) {
792
- if (lang === "bn") return void 0;
1293
+ if (lang === "bn" || lang === "staff") return void 0;
793
1294
  const mod = await import('@indic-transliteration/sanscript');
794
1295
  const sanscript = mod.default ?? mod;
795
1296
  const target = TARGET[lang];
@@ -797,7 +1298,7 @@ async function loadLyricTransliterator(lang) {
797
1298
  }
798
1299
 
799
1300
  // src/viewer/element.ts
800
- var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en"]);
1301
+ var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en", "staff"]);
801
1302
  function ensureNotationFonts() {
802
1303
  if (typeof FontFace === "undefined" || typeof document === "undefined") return;
803
1304
  const present = /* @__PURE__ */ new Set();
@@ -817,6 +1318,8 @@ function createElementClass() {
817
1318
  #abort;
818
1319
  #translit;
819
1320
  #translitLang;
1321
+ #renderSeq = 0;
1322
+ #staffOptions = {};
820
1323
  validate = false;
821
1324
  constructor() {
822
1325
  super();
@@ -824,7 +1327,7 @@ function createElementClass() {
824
1327
  if (typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && "adoptedStyleSheets" in this.#root) {
825
1328
  if (!sharedSheet) {
826
1329
  sharedSheet = new CSSStyleSheet();
827
- sharedSheet.replaceSync(SWARALIPI_CSS);
1330
+ sharedSheet.replaceSync(SWARALIPI_CSS + SWARALIPI_STAFF_CSS);
828
1331
  }
829
1332
  this.#root.adoptedStyleSheets = [...this.#root.adoptedStyleSheets, sharedSheet];
830
1333
  this.#adopted = true;
@@ -843,6 +1346,13 @@ function createElementClass() {
843
1346
  }
844
1347
  this.#render();
845
1348
  }
1349
+ get staffOptions() {
1350
+ return this.#staffOptions;
1351
+ }
1352
+ set staffOptions(value) {
1353
+ this.#staffOptions = value;
1354
+ if (this.#lang === "staff") this.#render();
1355
+ }
846
1356
  get #lang() {
847
1357
  const attr = this.getAttribute("lang");
848
1358
  return attr !== null && LANGS.has(attr) ? attr : "bn";
@@ -889,14 +1399,22 @@ function createElementClass() {
889
1399
  }).catch(() => void 0);
890
1400
  }
891
1401
  #styleFallback() {
892
- return this.#adopted ? "" : `<style>${SWARALIPI_CSS}</style>`;
1402
+ return this.#adopted ? "" : `<style>${SWARALIPI_CSS}${SWARALIPI_STAFF_CSS}</style>`;
893
1403
  }
894
1404
  #render() {
1405
+ const seq = ++this.#renderSeq;
895
1406
  if (this.#doc === void 0) {
896
1407
  this.#root.innerHTML = this.#styleFallback();
897
1408
  return;
898
1409
  }
899
1410
  const lang = this.#lang;
1411
+ if (lang === "staff") {
1412
+ const doc = this.#doc;
1413
+ void renderStaffHtml(doc, this.staffOptions).then((html2) => {
1414
+ if (seq === this.#renderSeq) this.#root.innerHTML = this.#styleFallback() + html2;
1415
+ }).catch(() => void 0);
1416
+ return;
1417
+ }
900
1418
  const html = renderToHtml(this.#doc, {
901
1419
  lang,
902
1420
  showHeader: !this.hasAttribute("hide-header"),
@@ -927,6 +1445,7 @@ function createElementClass() {
927
1445
  lang,
928
1446
  showHeader: !this.hasAttribute("hide-header"),
929
1447
  transliterate: this.#translitLang === lang ? this.#translit : void 0,
1448
+ ...lang === "staff" ? this.staffOptions : {},
930
1449
  ...options
931
1450
  });
932
1451
  }
@@ -946,19 +1465,30 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
946
1465
  return ctor;
947
1466
  }
948
1467
 
1468
+ exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
1469
+ exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
949
1470
  exports.DIGITS = DIGITS;
950
1471
  exports.LABELS = LABELS;
951
1472
  exports.MARKS = MARKS;
952
1473
  exports.NOTATION_FONT_URLS = NOTATION_FONT_URLS;
953
1474
  exports.SWARALIPI_CSS = SWARALIPI_CSS;
954
1475
  exports.SWARALIPI_PRINT_CSS = SWARALIPI_PRINT_CSS;
1476
+ exports.SWARALIPI_STAFF_CSS = SWARALIPI_STAFF_CSS;
1477
+ exports.abcDuration = abcDuration;
1478
+ exports.abcToSvg = abcToSvg;
1479
+ exports.buildAbc = buildAbc;
955
1480
  exports.composeCluster = composeCluster;
956
1481
  exports.defineSwaralipiSheet = defineSwaralipiSheet;
1482
+ exports.docToAbc = docToAbc;
957
1483
  exports.groupRowIntoCells = groupRowIntoCells;
958
1484
  exports.loadLyricTransliterator = loadLyricTransliterator;
959
1485
  exports.localizeDigits = localizeDigits;
1486
+ exports.midiToAbc = midiToAbc;
960
1487
  exports.printSwaralipi = printSwaralipi;
1488
+ exports.renderMetaHeader = renderMetaHeader;
961
1489
  exports.renderPrintDocument = renderPrintDocument;
1490
+ exports.renderStaffHtml = renderStaffHtml;
1491
+ exports.renderStaffPrintDocument = renderStaffPrintDocument;
962
1492
  exports.renderToHtml = renderToHtml;
963
1493
  exports.resolveArcs = resolveArcs;
964
1494
  //# sourceMappingURL=index.cjs.map