@ugfoundation/swaralipi-js 0.8.1 → 0.9.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.
- package/dist/{batch-NDL6CW7Q.js → batch-PYBXQ4OM.js} +5 -5
- package/dist/{batch-NDL6CW7Q.js.map → batch-PYBXQ4OM.js.map} +1 -1
- package/dist/{chunk-LPCKL3AM.js → chunk-3ZQPM2CE.js} +28 -19
- package/dist/chunk-3ZQPM2CE.js.map +1 -0
- package/dist/{chunk-4IVUPMRK.js → chunk-7SMRG4IS.js} +14 -9
- package/dist/chunk-7SMRG4IS.js.map +1 -0
- package/dist/{chunk-4HNT6OAT.js → chunk-MYVIQU4O.js} +12 -3
- package/dist/chunk-MYVIQU4O.js.map +1 -0
- package/dist/{chunk-Z42M5JAA.js → chunk-RT4SVJMK.js} +5 -4
- package/dist/chunk-RT4SVJMK.js.map +1 -0
- package/dist/cli.cjs +33 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/convert/index.cjs +31 -17
- package/dist/convert/index.cjs.map +1 -1
- package/dist/convert/index.d.cts +1 -1
- package/dist/convert/index.d.ts +1 -1
- package/dist/convert/index.js +3 -3
- package/dist/index.cjs +38 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{lay-Csa4CD46.d.cts → lay-CZIzR9cM.d.ts} +16 -2
- package/dist/{lay-Ct9ky_EE.d.ts → lay-CfDlPhH2.d.cts} +16 -2
- package/dist/player/index.cjs +80 -48
- package/dist/player/index.cjs.map +1 -1
- package/dist/player/index.d.cts +11 -9
- package/dist/player/index.d.ts +11 -9
- package/dist/player/index.js +68 -46
- package/dist/player/index.js.map +1 -1
- package/dist/{schedule-B_UaluBi.d.cts → schedule-9uvjnCG_.d.ts} +15 -10
- package/dist/{schedule-CdILTNsk.d.ts → schedule-TxSfHL-T.d.cts} +15 -10
- package/dist/{schema-DGA_bNVZ.d.ts → schema-Cf_epUIC.d.cts} +333 -63
- package/dist/{schema-DGA_bNVZ.d.cts → schema-Cf_epUIC.d.ts} +333 -63
- package/dist/spec/index.cjs +38 -17
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +26 -8
- package/dist/spec/index.d.ts +26 -8
- package/dist/spec/index.js +2 -2
- package/dist/viewer/index.cjs +306 -178
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +36 -12
- package/dist/viewer/index.d.ts +36 -12
- package/dist/viewer/index.js +234 -135
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4HNT6OAT.js.map +0 -1
- package/dist/chunk-4IVUPMRK.js.map +0 -1
- package/dist/chunk-LPCKL3AM.js.map +0 -1
- package/dist/chunk-Z42M5JAA.js.map +0 -1
package/dist/viewer/index.cjs
CHANGED
|
@@ -38,30 +38,16 @@ var init_lay = __esm({
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// src/spec/
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
|
|
41
|
+
// src/spec/taals.ts
|
|
42
|
+
function isTaalFerta(name) {
|
|
43
|
+
return name !== void 0 && name.normalize("NFC").replace(/[\s-]/g, "") === TAAL_FERTA;
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// src/spec/svara.ts
|
|
52
|
-
function hasKomal(letter) {
|
|
53
|
-
return letter === "r" || letter === "g" || letter === "d" || letter === "n";
|
|
45
|
+
function sameTaal(a, b) {
|
|
46
|
+
if (a === b) return true;
|
|
47
|
+
if (!a || !b) return false;
|
|
48
|
+
return a.id === b.id && a.numBeats === b.numBeats && a.variation === b.variation && String(a.bhaags) === String(b.bhaags);
|
|
54
49
|
}
|
|
55
|
-
|
|
56
|
-
return letter === "m";
|
|
57
|
-
}
|
|
58
|
-
var init_svara = __esm({
|
|
59
|
-
"src/spec/svara.ts"() {
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// src/spec/taals.ts
|
|
64
|
-
var TAALS;
|
|
50
|
+
var TAALS, TAAL_FERTA;
|
|
65
51
|
var init_taals = __esm({
|
|
66
52
|
"src/spec/taals.ts"() {
|
|
67
53
|
TAALS = {
|
|
@@ -85,9 +71,32 @@ var init_taals = __esm({
|
|
|
85
71
|
dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] }
|
|
86
72
|
// ধামার
|
|
87
73
|
};
|
|
74
|
+
TAAL_FERTA = "\u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE";
|
|
88
75
|
}
|
|
89
76
|
});
|
|
90
|
-
|
|
77
|
+
|
|
78
|
+
// src/spec/grace.ts
|
|
79
|
+
function graceNotes(grace) {
|
|
80
|
+
if (grace === void 0) return [];
|
|
81
|
+
return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
|
|
82
|
+
}
|
|
83
|
+
var init_grace = __esm({
|
|
84
|
+
"src/spec/grace.ts"() {
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/spec/svara.ts
|
|
89
|
+
function hasKomal(letter) {
|
|
90
|
+
return letter === "r" || letter === "g" || letter === "d" || letter === "n";
|
|
91
|
+
}
|
|
92
|
+
function hasTivra(letter) {
|
|
93
|
+
return letter === "m";
|
|
94
|
+
}
|
|
95
|
+
var init_svara = __esm({
|
|
96
|
+
"src/spec/svara.ts"() {
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, graceSchema, noteSchema, laySchema, taalSchema, rowSchema, partSchema, metaSchema, swaralipiSchema;
|
|
91
100
|
var init_schema = __esm({
|
|
92
101
|
"src/spec/schema.ts"() {
|
|
93
102
|
octaveSchema = zod.z.enum([
|
|
@@ -140,22 +149,6 @@ var init_schema = __esm({
|
|
|
140
149
|
)
|
|
141
150
|
);
|
|
142
151
|
laySchema = zod.z.enum(["drut", "madhya", "vilambit"]);
|
|
143
|
-
rowSchema = zod.z.object({
|
|
144
|
-
/** Optional line label (e.g. a tuk/section marker). */
|
|
145
|
-
label: zod.z.string().optional(),
|
|
146
|
-
/**
|
|
147
|
-
* লয়: the tempo tier this row switches to, held until another row changes
|
|
148
|
-
* it (absent on every row = the whole song at `meta.bpm`, i.e. দ্রুত).
|
|
149
|
-
* Read it with `rowLay()`, which also picks up the mark NLTR *prints*
|
|
150
|
-
* ("বিলম্বিত লয়:") in already-converted documents.
|
|
151
|
-
*/
|
|
152
|
-
lay: laySchema.optional(),
|
|
153
|
-
notes: zod.z.array(noteSchema)
|
|
154
|
-
});
|
|
155
|
-
partSchema = zod.z.object({
|
|
156
|
-
name: zod.z.string().optional(),
|
|
157
|
-
rows: zod.z.array(rowSchema)
|
|
158
|
-
});
|
|
159
152
|
taalSchema = zod.z.object({
|
|
160
153
|
/** Player sample key, e.g. `dadra`, `teentaal`. */
|
|
161
154
|
id: zod.z.string(),
|
|
@@ -176,6 +169,31 @@ var init_schema = __esm({
|
|
|
176
169
|
}).refine((t) => t.bhaags === void 0 || t.bhaags.reduce((a, b) => a + b, 0) === t.numBeats, {
|
|
177
170
|
message: "bhaags must sum to numBeats"
|
|
178
171
|
});
|
|
172
|
+
rowSchema = zod.z.object({
|
|
173
|
+
/** Optional line label (e.g. a tuk/section marker). */
|
|
174
|
+
label: zod.z.string().optional(),
|
|
175
|
+
/**
|
|
176
|
+
* লয়: the tempo tier this row switches to, held until another row changes
|
|
177
|
+
* it (absent on every row = the whole song at `meta.bpm`, i.e. দ্রুত).
|
|
178
|
+
* Read it with `rowLay()`, which also picks up the mark NLTR *prints*
|
|
179
|
+
* ("বিলম্বিত লয়:") in already-converted documents.
|
|
180
|
+
*/
|
|
181
|
+
lay: laySchema.optional(),
|
|
182
|
+
/**
|
|
183
|
+
* তালফেরতা: the তাল this row switches to, held until another row changes it
|
|
184
|
+
* (absent on every row = the whole song in `meta.taal`). `null` = মুক্তছন্দ
|
|
185
|
+
* from here on: the accompaniment rests until a later row names a taal.
|
|
186
|
+
* Independent of `lay` — either, both or neither may change on one row.
|
|
187
|
+
*/
|
|
188
|
+
taal: taalSchema.nullable().optional(),
|
|
189
|
+
/** That taal's name as printed (e.g. `ষষ্ঠী(২/৪)`) — `meta.taalName`'s twin. */
|
|
190
|
+
taalName: zod.z.string().optional(),
|
|
191
|
+
notes: zod.z.array(noteSchema)
|
|
192
|
+
});
|
|
193
|
+
partSchema = zod.z.object({
|
|
194
|
+
name: zod.z.string().optional(),
|
|
195
|
+
rows: zod.z.array(rowSchema)
|
|
196
|
+
});
|
|
179
197
|
metaSchema = zod.z.object({
|
|
180
198
|
title: zod.z.string(),
|
|
181
199
|
/** Cross-reference to a gitabitan.io song content id (filename, immutable). */
|
|
@@ -222,12 +240,14 @@ __export(spec_exports, {
|
|
|
222
240
|
LAY_SPEEDS: () => LAY_SPEEDS,
|
|
223
241
|
SPEC_VERSION: () => SPEC_VERSION,
|
|
224
242
|
TAALS: () => TAALS,
|
|
243
|
+
TAAL_FERTA: () => TAAL_FERTA,
|
|
225
244
|
barSchema: () => barSchema,
|
|
226
245
|
bracketSchema: () => bracketSchema,
|
|
227
246
|
graceNotes: () => graceNotes,
|
|
228
247
|
graceSchema: () => graceSchema,
|
|
229
248
|
hasKomal: () => hasKomal,
|
|
230
249
|
hasTivra: () => hasTivra,
|
|
250
|
+
isTaalFerta: () => isTaalFerta,
|
|
231
251
|
laySchema: () => laySchema,
|
|
232
252
|
metaSchema: () => metaSchema,
|
|
233
253
|
microtoneSchema: () => microtoneSchema,
|
|
@@ -240,6 +260,7 @@ __export(spec_exports, {
|
|
|
240
260
|
rowLay: () => rowLay,
|
|
241
261
|
rowSchema: () => rowSchema,
|
|
242
262
|
safeParseSwaralipi: () => safeParseSwaralipi,
|
|
263
|
+
sameTaal: () => sameTaal,
|
|
243
264
|
svaraLetterSchema: () => svaraLetterSchema,
|
|
244
265
|
svaraSchema: () => svaraSchema,
|
|
245
266
|
swaralipiSchema: () => swaralipiSchema,
|
|
@@ -260,12 +281,13 @@ var init_spec = __esm({
|
|
|
260
281
|
init_grace();
|
|
261
282
|
init_lay();
|
|
262
283
|
init_svara();
|
|
263
|
-
SPEC_VERSION = "0.
|
|
284
|
+
SPEC_VERSION = "0.9.0";
|
|
264
285
|
}
|
|
265
286
|
});
|
|
266
287
|
|
|
267
288
|
// src/player/schedule.ts
|
|
268
289
|
init_lay();
|
|
290
|
+
init_taals();
|
|
269
291
|
|
|
270
292
|
// src/player/pitch.ts
|
|
271
293
|
init_grace();
|
|
@@ -389,18 +411,23 @@ function buildSchedule(doc, opts = {}) {
|
|
|
389
411
|
let matra = 0;
|
|
390
412
|
const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
|
|
391
413
|
const withLay = opts.lay !== false;
|
|
392
|
-
const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut" }];
|
|
414
|
+
const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
|
|
393
415
|
let scale = 1;
|
|
416
|
+
let taal = doc.meta.taal;
|
|
394
417
|
doc.parts.forEach((part, p) => {
|
|
395
418
|
part.rows.forEach((row, r) => {
|
|
396
419
|
const lay = withLay ? rowLay(row) : void 0;
|
|
397
|
-
|
|
398
|
-
|
|
420
|
+
const layChanged = lay !== void 0 && speeds[lay] !== scale;
|
|
421
|
+
const nextTaal = row.taal !== void 0 ? row.taal : taal;
|
|
422
|
+
if (layChanged || !sameTaal(nextTaal, taal)) {
|
|
423
|
+
if (layChanged) scale = speeds[lay];
|
|
424
|
+
taal = nextTaal;
|
|
399
425
|
const last = tempo[tempo.length - 1];
|
|
400
|
-
|
|
426
|
+
const seg = { beat: cursor, endBeat: cursor, scale, lay: layChanged ? lay : last.lay, taal };
|
|
427
|
+
if (last.beat === cursor) tempo[tempo.length - 1] = seg;
|
|
401
428
|
else {
|
|
402
429
|
last.endBeat = cursor;
|
|
403
|
-
tempo.push(
|
|
430
|
+
tempo.push(seg);
|
|
404
431
|
}
|
|
405
432
|
}
|
|
406
433
|
const glides = meendContinuations(row.notes);
|
|
@@ -439,7 +466,7 @@ function buildSchedule(doc, opts = {}) {
|
|
|
439
466
|
});
|
|
440
467
|
});
|
|
441
468
|
});
|
|
442
|
-
const numBeats =
|
|
469
|
+
const numBeats = tempo[0].taal?.numBeats;
|
|
443
470
|
const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
|
|
444
471
|
tempo[tempo.length - 1].endBeat = cursor;
|
|
445
472
|
return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
|
|
@@ -447,6 +474,7 @@ function buildSchedule(doc, opts = {}) {
|
|
|
447
474
|
|
|
448
475
|
// src/viewer/render.ts
|
|
449
476
|
init_grace();
|
|
477
|
+
init_taals();
|
|
450
478
|
|
|
451
479
|
// src/viewer/glyphs.ts
|
|
452
480
|
init_svara();
|
|
@@ -497,6 +525,11 @@ var LAY_LABELS = {
|
|
|
497
525
|
hi: { drut: "\u0926\u094D\u0930\u0941\u0924 \u0932\u092F", madhya: "\u092E\u0927\u094D\u092F \u0932\u092F", vilambit: "\u0935\u093F\u0932\u0902\u092C\u093F\u0924 \u0932\u092F" },
|
|
498
526
|
en: { drut: "Fast (drut)", madhya: "Medium (madhya)", vilambit: "Slow (vilambit)" }
|
|
499
527
|
};
|
|
528
|
+
var FREE_TAAL_LABELS = {
|
|
529
|
+
bn: "\u09AE\u09C1\u0995\u09CD\u09A4\u099B\u09A8\u09CD\u09A6",
|
|
530
|
+
hi: "\u092E\u0941\u0915\u094D\u0924\u091B\u0902\u0926",
|
|
531
|
+
en: "Free rhythm"
|
|
532
|
+
};
|
|
500
533
|
function composeCluster(letter, opts) {
|
|
501
534
|
const classes = [];
|
|
502
535
|
if (opts.lang === "bn") {
|
|
@@ -642,42 +675,64 @@ function alignRows(rows) {
|
|
|
642
675
|
}
|
|
643
676
|
function resolveArcs(cells) {
|
|
644
677
|
const arcs = [];
|
|
645
|
-
let
|
|
678
|
+
let open;
|
|
646
679
|
let sawEnds = false;
|
|
647
680
|
const fillCells = [];
|
|
648
|
-
const openAt = (
|
|
681
|
+
const openAt = (cell, note) => {
|
|
649
682
|
sawEnds = true;
|
|
650
|
-
|
|
683
|
+
open ??= { cell, note };
|
|
651
684
|
};
|
|
652
|
-
const closeAt = (
|
|
685
|
+
const closeAt = (cell, note) => {
|
|
653
686
|
sawEnds = true;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
687
|
+
const from = open ?? { cell: 0, note: 0 };
|
|
688
|
+
arcs.push({
|
|
689
|
+
start: from.cell,
|
|
690
|
+
startNote: from.note,
|
|
691
|
+
end: cell,
|
|
692
|
+
endNote: note,
|
|
693
|
+
openLeft: open === void 0,
|
|
694
|
+
openRight: false
|
|
695
|
+
});
|
|
696
|
+
open = void 0;
|
|
660
697
|
};
|
|
661
698
|
cells.forEach((cell, i) => {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
699
|
+
const pitched = cell.notes.flatMap((n, j) => isPitched(n) ? [j] : []);
|
|
700
|
+
const first = pitched[0] ?? 0;
|
|
701
|
+
const last = pitched[pitched.length - 1] ?? cell.notes.length - 1;
|
|
702
|
+
let marks = "";
|
|
703
|
+
cell.notes.forEach((note, j) => {
|
|
704
|
+
if (note.meend === "start") openAt(i, j);
|
|
705
|
+
else if (note.meend === "end") closeAt(i, j);
|
|
706
|
+
for (const b of note.below ?? []) if (isArcRaw(b.raw)) marks += b.raw;
|
|
707
|
+
});
|
|
708
|
+
const reopens = /[xX].*[wW]/.test(marks);
|
|
709
|
+
for (const ch of marks) {
|
|
710
|
+
if (ch === "w" || ch === "W") openAt(i, reopens ? last : first);
|
|
711
|
+
else if (ch === "x" || ch === "X") closeAt(i, reopens ? first : last);
|
|
712
|
+
else if (ch === "$") {
|
|
713
|
+
openAt(i, first);
|
|
714
|
+
closeAt(i, last);
|
|
715
|
+
} else fillCells.push(i);
|
|
673
716
|
}
|
|
674
717
|
});
|
|
675
|
-
|
|
676
|
-
|
|
718
|
+
const lastCell = Math.max(cells.length - 1, 0);
|
|
719
|
+
const lastNote = (c) => Math.max((cells[c]?.notes.length ?? 1) - 1, 0);
|
|
720
|
+
if (open !== void 0) {
|
|
721
|
+
arcs.push({
|
|
722
|
+
start: open.cell,
|
|
723
|
+
startNote: open.note,
|
|
724
|
+
end: lastCell,
|
|
725
|
+
endNote: lastNote(lastCell),
|
|
726
|
+
openLeft: false,
|
|
727
|
+
openRight: true
|
|
728
|
+
});
|
|
677
729
|
} else if (!sawEnds && fillCells.length > 0) {
|
|
730
|
+
const end = Math.max(...fillCells);
|
|
678
731
|
arcs.push({
|
|
679
732
|
start: Math.min(...fillCells),
|
|
680
|
-
|
|
733
|
+
startNote: 0,
|
|
734
|
+
end,
|
|
735
|
+
endNote: lastNote(end),
|
|
681
736
|
openLeft: true,
|
|
682
737
|
openRight: true,
|
|
683
738
|
line: true
|
|
@@ -685,6 +740,35 @@ function resolveArcs(cells) {
|
|
|
685
740
|
}
|
|
686
741
|
return arcs;
|
|
687
742
|
}
|
|
743
|
+
function arcInk(cells, arcs) {
|
|
744
|
+
const ink = { cells: cells.map(() => ""), notes: cells.map((c) => c.notes.map(() => "")) };
|
|
745
|
+
const mark = (c, j, cls, clip = false) => {
|
|
746
|
+
ink.notes[c][j] += ` ${cls}`;
|
|
747
|
+
if (clip && !ink.cells[c].includes(" sw-mk")) ink.cells[c] += " sw-mk";
|
|
748
|
+
};
|
|
749
|
+
for (const arc of arcs) {
|
|
750
|
+
const { start: s, end: e, startNote: j, endNote: k } = arc;
|
|
751
|
+
const hookL = arc.line !== true && !arc.openLeft;
|
|
752
|
+
const hookR = arc.line !== true && !arc.openRight;
|
|
753
|
+
if (s === e && hookL && hookR) {
|
|
754
|
+
if (j === k) {
|
|
755
|
+
mark(s, j, "sw-mos");
|
|
756
|
+
mark(s, j, "sw-mcs");
|
|
757
|
+
} else {
|
|
758
|
+
mark(s, j, "sw-mob");
|
|
759
|
+
for (let n = j + 1; n < k; n++) mark(s, n, "sw-mf");
|
|
760
|
+
mark(s, k, "sw-mcb");
|
|
761
|
+
}
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
for (let c = s; c <= e; c++) {
|
|
765
|
+
if (c === s && hookL) mark(c, j, "sw-mo", true);
|
|
766
|
+
else if (c === e && hookR) mark(c, k, "sw-mc", true);
|
|
767
|
+
else mark(c, 0, "sw-mm", true);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return ink;
|
|
771
|
+
}
|
|
688
772
|
var SVARA_CHAR = /[srgmpqndtkuv]/;
|
|
689
773
|
function rawMarks(raw, count) {
|
|
690
774
|
if (raw === void 0) return void 0;
|
|
@@ -860,25 +944,9 @@ function renderNote(note, kind, opts, ctx) {
|
|
|
860
944
|
const raw = rawDisplay(note);
|
|
861
945
|
return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
|
|
862
946
|
}
|
|
863
|
-
function renderArc(arc, offset) {
|
|
864
|
-
const open = arc.line === true;
|
|
865
|
-
const openL = arc.openLeft || open;
|
|
866
|
-
const openR = arc.openRight || open;
|
|
867
|
-
const fill = '<i class="sw-arc-m"></i>';
|
|
868
|
-
const hookL = openL ? "" : '<i class="sw-arc-l">w</i>';
|
|
869
|
-
const hookR = openR ? "" : '<i class="sw-arc-r">x</i>';
|
|
870
|
-
const piece = (col, cls, inner) => `<span class="sw-arc${cls}" style="grid-column:${col}">${inner}</span>`;
|
|
871
|
-
const f = offset + arc.start + COL_BASE;
|
|
872
|
-
const t = offset + arc.end + COL_BASE;
|
|
873
|
-
const openCls = (openL ? " sw-arc--openl" : "") + (openR ? " sw-arc--openr" : "");
|
|
874
|
-
if (t === f) return piece(`${f}`, openCls, hookL + fill + hookR);
|
|
875
|
-
const head = piece(`${f}`, openL ? " sw-arc--openl" : " sw-arc--head", hookL + fill);
|
|
876
|
-
const tail = piece(`${t}`, openR ? " sw-arc--openr" : " sw-arc--tail", fill + hookR);
|
|
877
|
-
const mid = t > f + 1 ? piece(`${f + 1} / ${t}`, " sw-arc--mid", fill) : "";
|
|
878
|
-
return head + mid + tail;
|
|
879
|
-
}
|
|
880
947
|
function renderRow(row, partIdx, rowIdx, offset, ctx) {
|
|
881
948
|
const cells = groupRowIntoCells(row.notes);
|
|
949
|
+
const ink = arcInk(cells, resolveArcs(cells));
|
|
882
950
|
const out = [];
|
|
883
951
|
if (row.label !== void 0)
|
|
884
952
|
out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
|
|
@@ -888,11 +956,10 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
|
|
|
888
956
|
const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
|
|
889
957
|
const notes = cell.notes.map((note, j) => {
|
|
890
958
|
const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
|
|
891
|
-
const cls = isPitched(note) ? " sw-p" : "";
|
|
892
959
|
const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
|
|
893
|
-
return `<span class="sw-n${
|
|
960
|
+
return `<span class="sw-n${bar}${ink.notes[ci][j]}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
|
|
894
961
|
}).join("");
|
|
895
|
-
out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
|
|
962
|
+
out.push(`<span class="sw-cell${barCls}${ink.cells[ci]}" style="grid-column:${col}">${notes}</span>`);
|
|
896
963
|
const slices = cellLyricSlices(cell);
|
|
897
964
|
if (slices !== void 0) {
|
|
898
965
|
const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
|
|
@@ -905,7 +972,6 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
|
|
|
905
972
|
if (below.length > 0)
|
|
906
973
|
out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
|
|
907
974
|
});
|
|
908
|
-
for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
|
|
909
975
|
return `<div class="sw-row">${out.join("")}</div>`;
|
|
910
976
|
}
|
|
911
977
|
function renderHeader(meta, ctx) {
|
|
@@ -936,15 +1002,25 @@ function renderToHtml(doc, opts = {}) {
|
|
|
936
1002
|
out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
|
|
937
1003
|
let n = 0;
|
|
938
1004
|
let lay;
|
|
1005
|
+
let taal = doc.meta.taal;
|
|
1006
|
+
let named = !isTaalFerta(doc.meta.taalName);
|
|
939
1007
|
doc.parts.forEach((part, p) => {
|
|
940
1008
|
out.push('<section class="sw-part">');
|
|
941
1009
|
if (part.name !== void 0)
|
|
942
1010
|
out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
|
|
943
1011
|
part.rows.forEach((row, r) => {
|
|
1012
|
+
const marks = [];
|
|
1013
|
+
if (row.taal !== void 0 && (!named || !sameTaal(row.taal, taal))) {
|
|
1014
|
+
taal = row.taal;
|
|
1015
|
+
named = true;
|
|
1016
|
+
const name = row.taalName ?? (taal === null ? FREE_TAAL_LABELS[ctx.lang] : taal.id);
|
|
1017
|
+
marks.push(`<span class="sw-taal" data-taal="${escapeHtml(taal?.id ?? "free")}">${escapeHtml(ctx.tr(name))}</span>`);
|
|
1018
|
+
}
|
|
944
1019
|
if (row.lay !== void 0 && row.lay !== lay) {
|
|
945
1020
|
lay = row.lay;
|
|
946
|
-
|
|
1021
|
+
marks.push(`<span class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</span>`);
|
|
947
1022
|
}
|
|
1023
|
+
if (marks.length > 0) out.push(`<div class="sw-change">${marks.join("")}</div>`);
|
|
948
1024
|
out.push(renderRow(row, p, r, layout.offsets[n++], ctx));
|
|
949
1025
|
});
|
|
950
1026
|
out.push("</section>");
|
|
@@ -963,7 +1039,7 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
|
|
|
963
1039
|
sheet.style.removeProperty("--sw-size");
|
|
964
1040
|
sheet.style.removeProperty("--sw-tight");
|
|
965
1041
|
const available = scroll.clientWidth;
|
|
966
|
-
let natural = block.
|
|
1042
|
+
let natural = block.offsetWidth;
|
|
967
1043
|
if (available === 0 || natural === 0) continue;
|
|
968
1044
|
if (natural > available && minSpacing < 1) {
|
|
969
1045
|
let loose = minSpacing;
|
|
@@ -971,11 +1047,11 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
|
|
|
971
1047
|
for (let i = 0; i < 6; i++) {
|
|
972
1048
|
const mid = (loose + tight) / 2;
|
|
973
1049
|
sheet.style.setProperty("--sw-tight", String(mid));
|
|
974
|
-
if (block.
|
|
1050
|
+
if (block.offsetWidth <= available) loose = mid;
|
|
975
1051
|
else tight = mid;
|
|
976
1052
|
}
|
|
977
1053
|
sheet.style.setProperty("--sw-tight", String(loose));
|
|
978
|
-
natural = block.
|
|
1054
|
+
natural = block.offsetWidth;
|
|
979
1055
|
}
|
|
980
1056
|
const base = parseFloat(getComputedStyle(sheet).fontSize);
|
|
981
1057
|
if (!(base > 0)) continue;
|
|
@@ -1038,10 +1114,12 @@ ${FONT_FACES}
|
|
|
1038
1114
|
|
|
1039
1115
|
.sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
|
|
1040
1116
|
|
|
1041
|
-
/* \u09B2\u09AF\u09BC
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1117
|
+
/* \u09A4\u09BE\u09B2 / \u09B2\u09AF\u09BC change: the line where the \u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE or the tempo tier turns over,
|
|
1118
|
+
centred between two systems the way \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 prints "\u099D\u09BE\u0981\u09AA\u09A4\u09BE\u09B2" or
|
|
1119
|
+
"\u09A6\u09CD\u09B0\u09C1\u09A4 \u09B2\u09AF\u09BC". A full-width band rather than a row label \u2014 the label track is
|
|
1120
|
+
what the notation columns are measured against, and a six-em word in it
|
|
1121
|
+
would shove the whole sheet right. */
|
|
1122
|
+
.sw-change {
|
|
1045
1123
|
grid-column: 1 / -1;
|
|
1046
1124
|
display: flex;
|
|
1047
1125
|
align-items: center;
|
|
@@ -1055,12 +1133,14 @@ ${FONT_FACES}
|
|
|
1055
1133
|
opacity: 0.75;
|
|
1056
1134
|
break-after: avoid;
|
|
1057
1135
|
}
|
|
1058
|
-
.sw-
|
|
1136
|
+
.sw-change::before,
|
|
1137
|
+
.sw-change::after {
|
|
1059
1138
|
content: '';
|
|
1060
1139
|
flex: 1;
|
|
1061
1140
|
height: 1px;
|
|
1062
1141
|
background: var(--sw-rule, rgba(128, 128, 128, 0.35));
|
|
1063
1142
|
}
|
|
1143
|
+
.sw-taal + .sw-lay::before { content: '\xB7'; margin-right: 0.6em; }
|
|
1064
1144
|
|
|
1065
1145
|
/* ---- block grid: ONE column set for the whole sheet ----
|
|
1066
1146
|
* NLTR emits a song as a single html table, which is why its lines align
|
|
@@ -1073,31 +1153,45 @@ ${FONT_FACES}
|
|
|
1073
1153
|
column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
|
|
1074
1154
|
align-items: end;
|
|
1075
1155
|
width: max-content;
|
|
1156
|
+
/* The last column's cell spacers overhang half a gap past the grid; clip
|
|
1157
|
+
that sideways so it neither counts as width to fit nor raises a scrollbar. */
|
|
1158
|
+
overflow-x: clip;
|
|
1076
1159
|
}
|
|
1077
1160
|
.sw-part { display: contents; }
|
|
1161
|
+
/* Four FIXED bands per row \u2014 above / notation / arc / lyric \u2014 so a beat label,
|
|
1162
|
+
* a \u0995\u09A3 or a \u09AE\u09C0\u09A1\u09BC never changes how far a line sits from its words: the
|
|
1163
|
+
* notation hugs its lyric and the next system starts a little further off,
|
|
1164
|
+
* as the printed \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 sets it. Content taller than its band overflows
|
|
1165
|
+
* rather than pushing the rhythm apart. */
|
|
1078
1166
|
.sw-row {
|
|
1079
1167
|
grid-column: 1 / -1;
|
|
1080
1168
|
display: grid;
|
|
1081
1169
|
grid-template-columns: subgrid;
|
|
1082
|
-
grid-template-rows:
|
|
1170
|
+
grid-template-rows: 0.95em 1.35em 0.4em 1.35em;
|
|
1083
1171
|
align-items: end;
|
|
1084
|
-
padding: 0.
|
|
1172
|
+
padding-bottom: 0.35em;
|
|
1173
|
+
line-height: 1.35;
|
|
1085
1174
|
}
|
|
1086
1175
|
.sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
|
|
1087
1176
|
|
|
1088
|
-
/* One column per cell
|
|
1089
|
-
* flex
|
|
1090
|
-
*
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
.sw-cell
|
|
1094
|
-
.sw-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
.sw-
|
|
1100
|
-
.sw-
|
|
1177
|
+
/* One column per cell, its notes set tight as one word (\u09B0\u09B8\u09BE) and centred by
|
|
1178
|
+
* two flex spacers. Each spacer overhangs half the column gap (a min-width
|
|
1179
|
+
* paid back by a negative margin, so the column is sized by the notes alone),
|
|
1180
|
+
* which lets neighbouring cells' spacers meet mid-gap: a \u09AE\u09C0\u09A1\u09BC crossing cells is
|
|
1181
|
+
* inked by these boxes with no hole between them. */
|
|
1182
|
+
.sw-cell, .sw-lyr { display: flex; justify-content: center; white-space: nowrap; }
|
|
1183
|
+
.sw-cell {
|
|
1184
|
+
--sw-half-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1) / 2);
|
|
1185
|
+
grid-row: 2;
|
|
1186
|
+
align-self: stretch;
|
|
1187
|
+
}
|
|
1188
|
+
.sw-cell::before, .sw-cell::after { content: ''; flex: 1 0 0; min-width: var(--sw-half-gap); }
|
|
1189
|
+
.sw-cell::before { margin-left: calc(var(--sw-half-gap) * -1); }
|
|
1190
|
+
.sw-cell::after { margin-right: calc(var(--sw-half-gap) * -1); }
|
|
1191
|
+
.sw-cell > .sw-n { position: relative; }
|
|
1192
|
+
.sw-above { grid-row: 1; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
|
|
1193
|
+
.sw-below { grid-row: 3; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
|
|
1194
|
+
.sw-lyr { grid-row: 4; }
|
|
1101
1195
|
|
|
1102
1196
|
/* ---- notation clusters & marks ---- */
|
|
1103
1197
|
.sw-cl { position: relative; display: inline-block; }
|
|
@@ -1118,12 +1212,16 @@ ${FONT_FACES}
|
|
|
1118
1212
|
.sw-ati-taar::before { top: -0.3em; }
|
|
1119
1213
|
.sw-ati-mandra::before { bottom: -0.35em; }
|
|
1120
1214
|
|
|
1121
|
-
/* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it
|
|
1215
|
+
/* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it.
|
|
1216
|
+
* line-height 0 (here and on .sw-micro): a raised inline box taller than the
|
|
1217
|
+
* strut grows its line box and drops that one note's baseline below its
|
|
1218
|
+
* neighbours' (css2 \xA710.8). */
|
|
1122
1219
|
.sw-kan,
|
|
1123
1220
|
.sw-resh {
|
|
1124
1221
|
position: relative;
|
|
1125
1222
|
display: inline-block;
|
|
1126
1223
|
font-size: 0.58em;
|
|
1224
|
+
line-height: 0;
|
|
1127
1225
|
vertical-align: 0.75em;
|
|
1128
1226
|
}
|
|
1129
1227
|
.sw-kan { margin-right: 0.08em; }
|
|
@@ -1155,7 +1253,7 @@ ${FONT_FACES}
|
|
|
1155
1253
|
.sw-cut::before { top: 0.2em; }
|
|
1156
1254
|
.sw-cut::after { top: 0.55em; }
|
|
1157
1255
|
.sw-br { opacity: 0.8; padding: 0 0.06em; }
|
|
1158
|
-
.sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
|
|
1256
|
+
.sw-micro { font-size: 0.6em; line-height: 0; vertical-align: super; opacity: 0.8; }
|
|
1159
1257
|
|
|
1160
1258
|
/* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
|
|
1161
1259
|
* raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
|
|
@@ -1175,49 +1273,46 @@ ${FONT_FACES}
|
|
|
1175
1273
|
/* a bar owns a whole column but needs no matra of width */
|
|
1176
1274
|
.sw-bar-cell { min-width: 0; }
|
|
1177
1275
|
|
|
1178
|
-
/* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
|
|
1179
|
-
* Swarabitan
|
|
1180
|
-
* flat
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
.sw-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
.sw-
|
|
1193
|
-
.sw-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
.sw-
|
|
1207
|
-
.sw-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
margin-bottom: -0.01em;
|
|
1219
|
-
background: currentColor;
|
|
1220
|
-
}
|
|
1276
|
+
/* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x/$ segments and spec meend) ----
|
|
1277
|
+
* Swarabitan's own arc, all langs: w is the opening hook, curling down from
|
|
1278
|
+
* its tip into a flat, x its mirror, y that flat alone \u2014 and they join
|
|
1279
|
+
* hole-free, so one run of text is a whole end (tools/align-check/glyph.html).
|
|
1280
|
+
* Each end sets its hook tip on its note's centre and runs the flat toward the
|
|
1281
|
+
* other end (render.ts arcInk): across cells it is cut at the mid-gap, where
|
|
1282
|
+
* the next cell's run carries on (.sw-mk clips a cell half a gap past each
|
|
1283
|
+
* side, as far as its spacers reach); inside one cell, at its note's edge. So
|
|
1284
|
+
* a long arc extends the flat and a short one trims it off. line-height 0 sets
|
|
1285
|
+
* every run's baseline the same distance below its note. The clip reaches
|
|
1286
|
+
* 0.5px further so two cells' anti-aliased edges overlap instead of leaving a
|
|
1287
|
+
* hairline seam between their identical flats. */
|
|
1288
|
+
.sw-mk { clip-path: inset(-3em calc(var(--sw-half-gap) * -1 - 0.5px)); }
|
|
1289
|
+
.sw-mo::after, .sw-mob::after, .sw-mos::after,
|
|
1290
|
+
.sw-mc::before, .sw-mcb::before, .sw-mcs::before,
|
|
1291
|
+
.sw-mf::before, .sw-mm::before {
|
|
1292
|
+
content: 'yyyyyyyyyyyy';
|
|
1293
|
+
position: absolute;
|
|
1294
|
+
top: calc(100% + 0.05em);
|
|
1295
|
+
font-family: Swarabitan;
|
|
1296
|
+
font-size: 0.9em;
|
|
1297
|
+
line-height: 0;
|
|
1298
|
+
white-space: pre;
|
|
1299
|
+
overflow-x: clip;
|
|
1300
|
+
}
|
|
1301
|
+
.sw-mo::after, .sw-mob::after, .sw-mos::after { content: 'wyyyyyyyyyyyy'; }
|
|
1302
|
+
/* rtl: a closing run cut short keeps its hook on its right edge */
|
|
1303
|
+
.sw-mc::before, .sw-mcb::before, .sw-mcs::before { content: 'yyyyyyyyyyyyx'; direction: rtl; }
|
|
1304
|
+
.sw-mo::after, .sw-mob::after { left: 50%; }
|
|
1305
|
+
.sw-mc::before, .sw-mcb::before { right: 50%; }
|
|
1306
|
+
.sw-mos::after { left: 0; }
|
|
1307
|
+
.sw-mcs::before { right: 0; }
|
|
1308
|
+
.sw-mf::before { left: 0; right: 0; }
|
|
1309
|
+
.sw-mm::before { left: -12em; }
|
|
1310
|
+
/* Inside one cell a hook gets half a note to curl in, about a third of its
|
|
1311
|
+
* curl: squeezed to half width (same stroke) it lands flat at the cut, so the
|
|
1312
|
+
* two flats meet level whatever the notes' widths \u2014 a small arc, not a V. */
|
|
1313
|
+
.sw-mob::after, .sw-mos::after, .sw-mcb::before, .sw-mcs::before { width: 100%; transform: scaleX(0.5); }
|
|
1314
|
+
.sw-mob::after, .sw-mos::after { transform-origin: left; }
|
|
1315
|
+
.sw-mcb::before, .sw-mcs::before { transform-origin: right; }
|
|
1221
1316
|
|
|
1222
1317
|
/* ---- annotation content ---- */
|
|
1223
1318
|
.sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
|
|
@@ -1263,7 +1358,7 @@ var SWARALIPI_PRINT_CSS = `
|
|
|
1263
1358
|
.sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
|
|
1264
1359
|
.sw-scroll { overflow: visible !important; }
|
|
1265
1360
|
.sw-row { break-inside: avoid; page-break-inside: avoid; }
|
|
1266
|
-
.sw-part-name, .sw-header, .sw-
|
|
1361
|
+
.sw-part-name, .sw-header, .sw-change { break-after: avoid-page; page-break-after: avoid; }
|
|
1267
1362
|
.sw-hl { background: none !important; }
|
|
1268
1363
|
/* Keep each engraved staff system whole across a page break (the abcjs line
|
|
1269
1364
|
is a <div> wrapping the <svg>, so the break rule must sit on the div). */
|
|
@@ -1439,11 +1534,15 @@ function eventPaths(schedule) {
|
|
|
1439
1534
|
}
|
|
1440
1535
|
return paths;
|
|
1441
1536
|
}
|
|
1537
|
+
var barLen = (m) => m.beatsPerBar > 0 ? m.beatsPerBar : 4;
|
|
1442
1538
|
function abcText(s) {
|
|
1443
1539
|
return s.replace(/[\r\n]+/g, " ").trim();
|
|
1444
1540
|
}
|
|
1445
1541
|
function buildAbc(schedule, lyricOf, opts) {
|
|
1446
|
-
const
|
|
1542
|
+
const meters = opts.meters?.length ? opts.meters : [{ beat: 0, beatsPerBar: opts.beatsPerBar, drums: opts.drums }];
|
|
1543
|
+
const withDrums = opts.drums !== void 0 || meters.some((m2) => m2.drums !== void 0);
|
|
1544
|
+
let m = 0;
|
|
1545
|
+
let beatsPerBar = barLen(meters[0]);
|
|
1447
1546
|
const tokens = [];
|
|
1448
1547
|
const words = [];
|
|
1449
1548
|
let barPos = 0;
|
|
@@ -1457,6 +1556,17 @@ function buildAbc(schedule, lyricOf, opts) {
|
|
|
1457
1556
|
}
|
|
1458
1557
|
};
|
|
1459
1558
|
schedule.events.forEach((ev, i) => {
|
|
1559
|
+
while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS2) {
|
|
1560
|
+
m++;
|
|
1561
|
+
if (barPos > EPS2) {
|
|
1562
|
+
tokens.push("|");
|
|
1563
|
+
words.push("|");
|
|
1564
|
+
}
|
|
1565
|
+
barPos = 0;
|
|
1566
|
+
const next = barLen(meters[m]);
|
|
1567
|
+
if (next !== beatsPerBar) tokens.push(`[M:${ratioMeter(next)}]`);
|
|
1568
|
+
beatsPerBar = next;
|
|
1569
|
+
}
|
|
1460
1570
|
if (ev.midi === null) {
|
|
1461
1571
|
if (ev.kind === "rest") {
|
|
1462
1572
|
tokens.push(`z${abcDuration(ev.durationBeats)}`);
|
|
@@ -1475,16 +1585,16 @@ function buildAbc(schedule, lyricOf, opts) {
|
|
|
1475
1585
|
});
|
|
1476
1586
|
if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
|
|
1477
1587
|
else if (tokens.length) tokens[tokens.length - 1] = "|]";
|
|
1478
|
-
const meter = ratioMeter(
|
|
1588
|
+
const meter = ratioMeter(barLen(meters[0]));
|
|
1479
1589
|
const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
|
|
1480
|
-
if (
|
|
1590
|
+
if (withDrums) head.push(...percMapLines(meters.flatMap((x) => x.drums ?? [])));
|
|
1481
1591
|
head.push("K:C treble");
|
|
1482
1592
|
const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
|
|
1483
1593
|
const lines = [...head];
|
|
1484
|
-
if (
|
|
1594
|
+
if (withDrums) {
|
|
1485
1595
|
lines.push("V:1", melodyBody);
|
|
1486
1596
|
if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
|
|
1487
|
-
lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats,
|
|
1597
|
+
lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, meters));
|
|
1488
1598
|
} else {
|
|
1489
1599
|
lines.push(melodyBody);
|
|
1490
1600
|
if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
|
|
@@ -1513,13 +1623,21 @@ function percMapLines(voices) {
|
|
|
1513
1623
|
}
|
|
1514
1624
|
return lines;
|
|
1515
1625
|
}
|
|
1516
|
-
function buildDrumVoice(totalBeats,
|
|
1517
|
-
const n = Math.max(1, Math.ceil(totalBeats));
|
|
1626
|
+
function buildDrumVoice(totalBeats, meters) {
|
|
1518
1627
|
const tokens = [];
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1628
|
+
meters.forEach((m, i) => {
|
|
1629
|
+
const bpb = barLen(m);
|
|
1630
|
+
if (i > 0) {
|
|
1631
|
+
if (tokens.length > 0 && tokens[tokens.length - 1] !== "|") tokens.push("|");
|
|
1632
|
+
if (bpb !== barLen(meters[i - 1])) tokens.push(`[M:${ratioMeter(bpb)}]`);
|
|
1633
|
+
}
|
|
1634
|
+
const end = meters[i + 1]?.beat ?? Math.max(1, Math.ceil(totalBeats));
|
|
1635
|
+
const n = Math.ceil(end - m.beat - 1e-6);
|
|
1636
|
+
for (let beat = 0; beat < n; beat++) {
|
|
1637
|
+
tokens.push(m.drums?.length ? DRUM_ABC[m.drums[beat % m.drums.length]].note : "z");
|
|
1638
|
+
if ((beat + 1) % bpb === 0) tokens.push("|");
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1523
1641
|
if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
|
|
1524
1642
|
else tokens[tokens.length - 1] = "|]";
|
|
1525
1643
|
return tokens.join(" ");
|
|
@@ -1530,16 +1648,25 @@ function lyricLookup(doc) {
|
|
|
1530
1648
|
return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
|
|
1531
1649
|
};
|
|
1532
1650
|
}
|
|
1533
|
-
function resolveDrumTaal(
|
|
1534
|
-
const id =
|
|
1651
|
+
function resolveDrumTaal(taal) {
|
|
1652
|
+
const id = taal?.id;
|
|
1535
1653
|
return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
|
|
1536
1654
|
}
|
|
1537
1655
|
function docToAbc(doc, opts = {}) {
|
|
1538
1656
|
const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave, lay: false });
|
|
1539
1657
|
const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1658
|
+
const meters = schedule.tempo.map((seg) => ({
|
|
1659
|
+
beat: seg.beat,
|
|
1660
|
+
beatsPerBar: opts.beatsPerBar ?? seg.taal?.numBeats ?? 4,
|
|
1661
|
+
drums: opts.drums && seg.taal !== null ? drumPattern(resolveDrumTaal(seg.taal)) : void 0
|
|
1662
|
+
}));
|
|
1663
|
+
return buildAbc(schedule, lyricLookup(doc), {
|
|
1664
|
+
bpm,
|
|
1665
|
+
lyrics: opts.lyrics !== false,
|
|
1666
|
+
beatsPerBar: meters[0].beatsPerBar,
|
|
1667
|
+
drums: opts.drums ? meters[0].drums ?? [] : void 0,
|
|
1668
|
+
meters
|
|
1669
|
+
});
|
|
1543
1670
|
}
|
|
1544
1671
|
async function abcToSvg(abc, opts = {}, notePaths) {
|
|
1545
1672
|
if (typeof document === "undefined") {
|
|
@@ -1618,7 +1745,7 @@ function buildTaalAbc(taal, opts = {}) {
|
|
|
1618
1745
|
const voices = Array.from({ length: n }, (_, i) => src[i % src.length]);
|
|
1619
1746
|
const bpm = Math.round(opts.bpm ?? 90);
|
|
1620
1747
|
const head = ["X:1", `M:${ratioMeter(n)}`, "L:1/4", `Q:1/4=${bpm}`, ...percMapLines(voices), "K:C"];
|
|
1621
|
-
const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, n, voices)];
|
|
1748
|
+
const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, [{ beat: 0, beatsPerBar: n, drums: voices }])];
|
|
1622
1749
|
if (opts.bols?.length) {
|
|
1623
1750
|
const w = opts.bols.slice(0, n).map((b) => b.trim() ? abcText(b).replace(/\s+/g, "~") : "*");
|
|
1624
1751
|
while (w.length < n) w.push("*");
|
|
@@ -2009,6 +2136,7 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
|
|
|
2009
2136
|
exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
|
|
2010
2137
|
exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
|
|
2011
2138
|
exports.DIGITS = DIGITS;
|
|
2139
|
+
exports.FREE_TAAL_LABELS = FREE_TAAL_LABELS;
|
|
2012
2140
|
exports.LABELS = LABELS;
|
|
2013
2141
|
exports.LAY_LABELS = LAY_LABELS;
|
|
2014
2142
|
exports.MARKS = MARKS;
|