@k-l-lambda/lilylet 0.1.66 → 0.1.67
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/lib/abc/grammar.jison.js +200 -179
- package/lib/lilylet/abcDecoder.js +98 -88
- package/lib/lilylet/meiEncoder.js +38 -8
- package/lib/lilylet/serializer.js +32 -16
- package/package.json +4 -1
- package/source/abc/abc.jison +19 -4
- package/source/abc/grammar.jison.js +200 -179
- package/source/lilylet/abcDecoder.ts +107 -77
- package/source/lilylet/meiEncoder.ts +39 -8
- package/source/lilylet/serializer.ts +34 -17
|
@@ -4,8 +4,35 @@
|
|
|
4
4
|
* Converts ABC notation files to Lilylet's internal LilyletDoc format.
|
|
5
5
|
*/
|
|
6
6
|
import parse from "../abc/parser.js";
|
|
7
|
-
import { Phonet, Accidental,
|
|
7
|
+
import { Phonet, Accidental, ArticulationType, OrnamentType, DynamicType, HairpinType, PedalType, NavigationMarkType, } from "./types.js";
|
|
8
8
|
// ============ Constants ============
|
|
9
|
+
// NotaGen catalog tags appear as leading single-% comments in ABC files,
|
|
10
|
+
// in the order: period, composer, instrumentation. They are mapped to
|
|
11
|
+
// Lilylet metadata: period -> genre, instrumentation -> instrument.
|
|
12
|
+
const NOTAGEN_PERIOD_SET = new Set([
|
|
13
|
+
"Baroque", "Classical", "Romantic",
|
|
14
|
+
]);
|
|
15
|
+
const NOTAGEN_INSTRUMENTATION_SET = new Set([
|
|
16
|
+
"Art Song", "Chamber", "Choral", "Keyboard", "Orchestral", "Vocal-Orchestral",
|
|
17
|
+
]);
|
|
18
|
+
const NOTAGEN_COMPOSER_SET = new Set([
|
|
19
|
+
"Bach, Johann Sebastian", "Bartok, Bela", "Beethoven, Ludwig van", "Berlioz, Hector",
|
|
20
|
+
"Bizet, Georges", "Boulanger, Lili", "Boulton, Harold", "Brahms, Johannes",
|
|
21
|
+
"Burgmuller, Friedrich", "Butterworth, George", "Chaminade, Cecile", "Chausson, Ernest",
|
|
22
|
+
"Chopin, Frederic", "Corelli, Arcangelo", "Cornelius, Peter", "Debussy, Claude",
|
|
23
|
+
"Dvorak, Antonin", "Faisst, Clara", "Faure, Gabriel", "Franz, Robert",
|
|
24
|
+
"Gonzaga, Chiquinha", "Grandval, Clemence de", "Grieg, Edvard", "Handel, George Frideric",
|
|
25
|
+
"Haydn, Joseph", "Hensel, Fanny", "Holmes, Augusta Mary Anne", "Jaell, Marie",
|
|
26
|
+
"Kinkel, Johanna", "Kralik, Mathilde", "Lang, Josephine", "Lehmann, Liza",
|
|
27
|
+
"Liszt, Franz", "Mayer, Emilie", "Medtner, Nikolay", "Mendelssohn, Felix",
|
|
28
|
+
"Mozart, Wolfgang Amadeus", "Munktell, Helena", "Paradis, Maria Theresia von",
|
|
29
|
+
"Parratt, Walter", "Prokofiev, Sergey", "Rachmaninoff, Sergei", "Ravel, Maurice",
|
|
30
|
+
"Reichardt, Louise", "Saint-Georges, Joseph Bologne", "Saint-Saens, Camille",
|
|
31
|
+
"Satie, Erik", "Scarlatti, Domenico", "Schroter, Corona", "Schubert, Franz",
|
|
32
|
+
"Schumann, Clara", "Schumann, Robert", "Scriabin, Aleksandr", "Shostakovich, Dmitry",
|
|
33
|
+
"Sibelius, Jean", "Smetana, Bedrich", "Tchaikovsky, Pyotr", "Viardot, Pauline",
|
|
34
|
+
"Vivaldi, Antonio", "Warlock, Peter", "Wolf, Hugo", "Zumsteeg, Emilie",
|
|
35
|
+
]);
|
|
9
36
|
const ABC_PHONET_MAP = {
|
|
10
37
|
"C": Phonet.c, "D": Phonet.d, "E": Phonet.e, "F": Phonet.f, "G": Phonet.g, "A": Phonet.a, "B": Phonet.b,
|
|
11
38
|
"c": Phonet.c, "d": Phonet.d, "e": Phonet.e, "f": Phonet.f, "g": Phonet.g, "a": Phonet.a, "b": Phonet.b,
|
|
@@ -237,13 +264,28 @@ const convertKeySignature = (abcKey) => {
|
|
|
237
264
|
* Convert ABC clef string to Lilylet Clef
|
|
238
265
|
*/
|
|
239
266
|
const convertClef = (clefStr) => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
267
|
+
// Split off an optional ABC octave shift suffix: "treble-8", "bass+8", "treble-15".
|
|
268
|
+
// ABC: "-N" lowers the sounding pitch (small N drawn below), "+N" raises it.
|
|
269
|
+
// LilyPond/Lilylet: "_N" = below, "^N" = above. Translate the sign accordingly.
|
|
270
|
+
const shift = clefStr?.match(/^(.*?)([+-])(8|15)$/);
|
|
271
|
+
const base = shift ? shift[1] : clefStr;
|
|
272
|
+
let resolved;
|
|
273
|
+
switch (base?.toLowerCase()) {
|
|
274
|
+
case "treble":
|
|
275
|
+
resolved = "treble";
|
|
276
|
+
break;
|
|
277
|
+
case "bass":
|
|
278
|
+
resolved = "bass";
|
|
279
|
+
break;
|
|
243
280
|
case "alto":
|
|
244
|
-
case "tenor":
|
|
281
|
+
case "tenor":
|
|
282
|
+
resolved = "alto";
|
|
283
|
+
break;
|
|
245
284
|
default: return undefined;
|
|
246
285
|
}
|
|
286
|
+
if (shift)
|
|
287
|
+
resolved += (shift[2] === "-" ? "_" : "^") + shift[3];
|
|
288
|
+
return resolved;
|
|
247
289
|
};
|
|
248
290
|
/**
|
|
249
291
|
* Convert ABC barline to Lilylet barline style
|
|
@@ -267,11 +309,41 @@ const convertBarline = (bar) => {
|
|
|
267
309
|
return "|";
|
|
268
310
|
}
|
|
269
311
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
312
|
+
// All voice numbers under a node, flattened.
|
|
313
|
+
const collectScoreVoices = (node) => {
|
|
314
|
+
if (typeof node === "string") {
|
|
315
|
+
const v = parseInt(node, 10);
|
|
316
|
+
return isNaN(v) ? [] : [v];
|
|
317
|
+
}
|
|
318
|
+
const voices = [];
|
|
319
|
+
for (const item of node.items || []) {
|
|
320
|
+
voices.push(...collectScoreVoices(item));
|
|
321
|
+
}
|
|
322
|
+
return voices;
|
|
323
|
+
};
|
|
324
|
+
// Expand a node into a list of parts; each part is a list of staves; each staff is a list of voices.
|
|
325
|
+
const scoreNodeToParts = (node) => {
|
|
326
|
+
if (typeof node === "string") {
|
|
327
|
+
const v = parseInt(node, 10);
|
|
328
|
+
return isNaN(v) ? [] : [[[v]]];
|
|
329
|
+
}
|
|
330
|
+
if (node.bound === "square") {
|
|
331
|
+
// each child is its own part
|
|
332
|
+
const parts = [];
|
|
333
|
+
for (const child of node.items || []) {
|
|
334
|
+
parts.push(...scoreNodeToParts(child));
|
|
335
|
+
}
|
|
336
|
+
return parts;
|
|
337
|
+
}
|
|
338
|
+
if (node.bound === "curly") {
|
|
339
|
+
// one part, each child is a separate staff (grand staff)
|
|
340
|
+
const staves = (node.items || []).map(child => collectScoreVoices(child)).filter(s => s.length > 0);
|
|
341
|
+
return staves.length > 0 ? [staves] : [];
|
|
342
|
+
}
|
|
343
|
+
// arc or no bound: one part, all voices on a single staff
|
|
344
|
+
const voices = collectScoreVoices(node);
|
|
345
|
+
return voices.length > 0 ? [[voices]] : [];
|
|
346
|
+
};
|
|
275
347
|
const parseScoreLayout = (headers) => {
|
|
276
348
|
const layoutHeader = headers.find((h) => h.staffLayout);
|
|
277
349
|
if (!layoutHeader)
|
|
@@ -279,86 +351,15 @@ const parseScoreLayout = (headers) => {
|
|
|
279
351
|
const layout = layoutHeader.staffLayout;
|
|
280
352
|
const voiceMap = new Map();
|
|
281
353
|
let partIndex = 0;
|
|
282
|
-
for (const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (typeof item === "string") {
|
|
288
|
-
voiceMap.set(parseInt(item), { partIndex, staffInPart });
|
|
354
|
+
for (const top of layout) {
|
|
355
|
+
for (const part of scoreNodeToParts(top)) {
|
|
356
|
+
part.forEach((staff, staffIdx) => {
|
|
357
|
+
for (const voice of staff) {
|
|
358
|
+
voiceMap.set(voice, { partIndex, staffInPart: staffIdx + 1 });
|
|
289
359
|
}
|
|
290
|
-
|
|
291
|
-
const sg = item;
|
|
292
|
-
for (const subItem of sg.items) {
|
|
293
|
-
if (typeof subItem === "string") {
|
|
294
|
-
voiceMap.set(parseInt(subItem), { partIndex, staffInPart });
|
|
295
|
-
}
|
|
296
|
-
else if (subItem.items) {
|
|
297
|
-
for (const leaf of subItem.items) {
|
|
298
|
-
if (typeof leaf === "string") {
|
|
299
|
-
voiceMap.set(parseInt(leaf), { partIndex, staffInPart });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
staffInPart++;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
partIndex++;
|
|
308
|
-
}
|
|
309
|
-
else if (group.bound === "arc" || !group.bound) {
|
|
310
|
-
// Arc or plain = voices sharing a staff in same part
|
|
311
|
-
for (const item of group.items) {
|
|
312
|
-
if (typeof item === "string") {
|
|
313
|
-
voiceMap.set(parseInt(item), { partIndex, staffInPart: 1 });
|
|
314
|
-
}
|
|
315
|
-
else if (item.items) {
|
|
316
|
-
for (const subItem of item.items) {
|
|
317
|
-
if (typeof subItem === "string") {
|
|
318
|
-
voiceMap.set(parseInt(subItem), { partIndex, staffInPart: 1 });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
360
|
+
});
|
|
323
361
|
partIndex++;
|
|
324
362
|
}
|
|
325
|
-
else {
|
|
326
|
-
// Square bracket or unknown - treat each item as separate part
|
|
327
|
-
for (const item of group.items) {
|
|
328
|
-
if (typeof item === "string") {
|
|
329
|
-
voiceMap.set(parseInt(item), { partIndex, staffInPart: 1 });
|
|
330
|
-
partIndex++;
|
|
331
|
-
}
|
|
332
|
-
else if (item.items) {
|
|
333
|
-
const sg = item;
|
|
334
|
-
if (sg.bound === "curly") {
|
|
335
|
-
let staffInPart = 1;
|
|
336
|
-
for (const subItem of sg.items) {
|
|
337
|
-
if (typeof subItem === "string") {
|
|
338
|
-
voiceMap.set(parseInt(subItem), { partIndex, staffInPart });
|
|
339
|
-
}
|
|
340
|
-
else if (subItem.items) {
|
|
341
|
-
for (const leaf of subItem.items) {
|
|
342
|
-
if (typeof leaf === "string") {
|
|
343
|
-
voiceMap.set(parseInt(leaf), { partIndex, staffInPart });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
staffInPart++;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
partIndex++;
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
for (const subItem of sg.items) {
|
|
353
|
-
if (typeof subItem === "string") {
|
|
354
|
-
voiceMap.set(parseInt(subItem), { partIndex, staffInPart: 1 });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
partIndex++;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
363
|
}
|
|
363
364
|
return voiceMap.size > 0 ? voiceMap : null;
|
|
364
365
|
};
|
|
@@ -790,8 +791,17 @@ const decodeTune = (tune) => {
|
|
|
790
791
|
}
|
|
791
792
|
}
|
|
792
793
|
for (const h of headers) {
|
|
793
|
-
|
|
794
|
+
const comment = h.comment;
|
|
795
|
+
if (typeof comment === "string") {
|
|
796
|
+
const value = comment.trim();
|
|
797
|
+
if (!metadata.genre && NOTAGEN_PERIOD_SET.has(value))
|
|
798
|
+
metadata.genre = value;
|
|
799
|
+
else if (!metadata.instrument && NOTAGEN_INSTRUMENTATION_SET.has(value))
|
|
800
|
+
metadata.instrument = value;
|
|
801
|
+
else if (!metadata.composer && NOTAGEN_COMPOSER_SET.has(value))
|
|
802
|
+
metadata.composer = value;
|
|
794
803
|
continue;
|
|
804
|
+
}
|
|
795
805
|
if (h.staffLayout)
|
|
796
806
|
continue;
|
|
797
807
|
const header = h;
|
|
@@ -44,6 +44,40 @@ const CLEF_SHAPES = {
|
|
|
44
44
|
F: { shape: "F", line: 4 },
|
|
45
45
|
C: { shape: "C", line: 3 },
|
|
46
46
|
};
|
|
47
|
+
// Resolve a clef string into MEI shape/line plus optional octave displacement.
|
|
48
|
+
// Octave transposition follows the LilyPond convention: a "_8"/"_15" suffix lowers
|
|
49
|
+
// the sounding pitch by one/two octaves (the small 8/15 is drawn below the clef),
|
|
50
|
+
// and "^8"/"^15" raises it (drawn above). MEI encodes this as dis ("8" | "15")
|
|
51
|
+
// and dis.place ("below" | "above").
|
|
52
|
+
const resolveClef = (clefStr) => {
|
|
53
|
+
const match = clefStr.match(/^(.*?)([_^])(8|15)$/);
|
|
54
|
+
const base = match ? match[1] : clefStr;
|
|
55
|
+
const clefInfo = CLEF_SHAPES[base] || CLEF_SHAPES.treble;
|
|
56
|
+
if (!match)
|
|
57
|
+
return { shape: clefInfo.shape, line: clefInfo.line };
|
|
58
|
+
return {
|
|
59
|
+
shape: clefInfo.shape,
|
|
60
|
+
line: clefInfo.line,
|
|
61
|
+
dis: match[3],
|
|
62
|
+
disPlace: match[2] === "^" ? "above" : "below",
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
// Attributes for a standalone <clef> element (mid-measure clef change).
|
|
66
|
+
const clefElementAttrs = (clefStr) => {
|
|
67
|
+
const c = resolveClef(clefStr);
|
|
68
|
+
let attrs = `shape="${c.shape}" line="${c.line}"`;
|
|
69
|
+
if (c.dis)
|
|
70
|
+
attrs += ` dis="${c.dis}" dis.place="${c.disPlace}"`;
|
|
71
|
+
return attrs;
|
|
72
|
+
};
|
|
73
|
+
// Attributes for a <staffDef> clef (clef.* namespace).
|
|
74
|
+
const staffDefClefAttrs = (clefStr) => {
|
|
75
|
+
const c = resolveClef(clefStr);
|
|
76
|
+
let attrs = `clef.shape="${c.shape}" clef.line="${c.line}"`;
|
|
77
|
+
if (c.dis)
|
|
78
|
+
attrs += ` clef.dis="${c.dis}" clef.dis.place="${c.disPlace}"`;
|
|
79
|
+
return attrs;
|
|
80
|
+
};
|
|
47
81
|
// Lilylet duration division to MEI dur
|
|
48
82
|
// division: 1=whole, 2=half, 4=quarter, 8=eighth, etc.
|
|
49
83
|
const DURATIONS = {
|
|
@@ -620,8 +654,7 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
620
654
|
const effectiveStaffNum = effectiveStaff ?? layerStaffNum;
|
|
621
655
|
if (effectiveStaffNum === layerStaffNum) {
|
|
622
656
|
const clefIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
623
|
-
|
|
624
|
-
xml += `${clefIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
|
|
657
|
+
xml += `${clefIndent}<clef xml:id="${generateId('clef')}" ${clefElementAttrs(ctx.clef)} />\n`;
|
|
625
658
|
}
|
|
626
659
|
activeClef = ctx.clef;
|
|
627
660
|
endingClef = ctx.clef;
|
|
@@ -1014,8 +1047,7 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
1014
1047
|
if (ctx.clef && ctx.clef !== currentClef) {
|
|
1015
1048
|
const layerStaff = voice.staff || 1;
|
|
1016
1049
|
if (currentStaff === layerStaff) {
|
|
1017
|
-
|
|
1018
|
-
xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
|
|
1050
|
+
xml += `${currentIndent}<clef xml:id="${generateId('clef')}" ${clefElementAttrs(ctx.clef)} />\n`;
|
|
1019
1051
|
}
|
|
1020
1052
|
currentClef = ctx.clef;
|
|
1021
1053
|
}
|
|
@@ -1580,8 +1612,7 @@ const encodeScoreDef = (keySig, timeNum, timeDen, partInfos, indent, meterSymbol
|
|
|
1580
1612
|
for (let ls = 1; ls <= info.maxStaff; ls++) {
|
|
1581
1613
|
const globalStaff = info.staffOffset + ls;
|
|
1582
1614
|
const clef = info.clefs[ls] || Clef.treble;
|
|
1583
|
-
|
|
1584
|
-
xml += `${indent} <staffDef xml:id="${generateId('staffdef')}" n="${globalStaff}" lines="5" clef.shape="${clefInfo.shape}" clef.line="${clefInfo.line}" />\n`;
|
|
1615
|
+
xml += `${indent} <staffDef xml:id="${generateId('staffdef')}" n="${globalStaff}" lines="5" ${staffDefClefAttrs(clef)} />\n`;
|
|
1585
1616
|
}
|
|
1586
1617
|
xml += `${indent} </staffGrp>\n`;
|
|
1587
1618
|
}
|
|
@@ -1589,8 +1620,7 @@ const encodeScoreDef = (keySig, timeNum, timeDen, partInfos, indent, meterSymbol
|
|
|
1589
1620
|
// Single staff part
|
|
1590
1621
|
const globalStaff = info.staffOffset + 1;
|
|
1591
1622
|
const clef = info.clefs[1] || Clef.treble;
|
|
1592
|
-
|
|
1593
|
-
xml += `${indent} <staffDef xml:id="${generateId('staffdef')}" n="${globalStaff}" lines="5" clef.shape="${clefInfo.shape}" clef.line="${clefInfo.line}" />\n`;
|
|
1623
|
+
xml += `${indent} <staffDef xml:id="${generateId('staffdef')}" n="${globalStaff}" lines="5" ${staffDefClefAttrs(clef)} />\n`;
|
|
1594
1624
|
}
|
|
1595
1625
|
}
|
|
1596
1626
|
xml += `${indent} </staffGrp>\n`;
|
|
@@ -286,7 +286,7 @@ const serializeContextChange = (event) => {
|
|
|
286
286
|
const parts = [];
|
|
287
287
|
// Clef
|
|
288
288
|
if (event.clef) {
|
|
289
|
-
parts.push('\\clef "' + CLEF_MAP[event.clef] + '"');
|
|
289
|
+
parts.push('\\clef "' + (CLEF_MAP[event.clef] ?? event.clef) + '"');
|
|
290
290
|
}
|
|
291
291
|
// Key signature
|
|
292
292
|
if (event.key) {
|
|
@@ -549,7 +549,7 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
549
549
|
const voiceClef = allStaffClefs?.[voice.staff] || findVoiceClef(voice);
|
|
550
550
|
const clefAlreadyEmitted = voiceClef && emittedClefs?.[voice.staff] === voiceClef;
|
|
551
551
|
if (voiceClef && !clefAlreadyEmitted) {
|
|
552
|
-
parts.push('\\clef "' + CLEF_MAP[voiceClef] + '"');
|
|
552
|
+
parts.push('\\clef "' + (CLEF_MAP[voiceClef] ?? voiceClef) + '"');
|
|
553
553
|
if (emittedClefs)
|
|
554
554
|
emittedClefs[voice.staff] = voiceClef;
|
|
555
555
|
}
|
|
@@ -572,7 +572,7 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
572
572
|
// Emit target staff clef if the event carries one or allStaffClefs knows it
|
|
573
573
|
const ctxClef = ctx.clef || allStaffClefs?.[activeStaff];
|
|
574
574
|
if (ctxClef && emittedClefs?.[activeStaff] !== ctxClef) {
|
|
575
|
-
parts.push('\\clef "' + CLEF_MAP[ctxClef] + '"');
|
|
575
|
+
parts.push('\\clef "' + (CLEF_MAP[ctxClef] ?? ctxClef) + '"');
|
|
576
576
|
if (emittedClefs)
|
|
577
577
|
emittedClefs[activeStaff] = ctxClef;
|
|
578
578
|
}
|
|
@@ -596,7 +596,7 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
596
596
|
// Emit the target staff's clef if it differs from what was last emitted for this staff
|
|
597
597
|
const targetClef = allStaffClefs?.[activeStaff];
|
|
598
598
|
if (targetClef && emittedClefs?.[activeStaff] !== targetClef) {
|
|
599
|
-
parts.push('\\clef "' + CLEF_MAP[targetClef] + '"');
|
|
599
|
+
parts.push('\\clef "' + (CLEF_MAP[targetClef] ?? targetClef) + '"');
|
|
600
600
|
if (emittedClefs)
|
|
601
601
|
emittedClefs[activeStaff] = targetClef;
|
|
602
602
|
}
|
|
@@ -651,11 +651,15 @@ const serializePart = (part, currentStaff, isGrandStaff = false, measureContext,
|
|
|
651
651
|
}
|
|
652
652
|
const voiceStrs = [];
|
|
653
653
|
let staff = currentStaff;
|
|
654
|
+
// A part is a grand staff only if its voices span more than one staff.
|
|
655
|
+
// Only then do we force \staff on every voice; single-staff parts emit \staff
|
|
656
|
+
// solely when the staff actually changes (e.g. resetting after a prior grand staff).
|
|
657
|
+
const partIsGrandStaff = new Set(part.voices.map(v => v.staff)).size > 1;
|
|
654
658
|
for (let i = 0; i < part.voices.length; i++) {
|
|
655
659
|
const voice = part.voices[i];
|
|
656
660
|
// Pass measureContext to all voices, isFirstVoice for key/time
|
|
657
661
|
const isFirstVoice = isFirstPart && i === 0;
|
|
658
|
-
const { str, newStaff } = serializeVoice(voice, staff,
|
|
662
|
+
const { str, newStaff } = serializeVoice(voice, staff, partIsGrandStaff, measureContext, isFirstVoice, clefsByStaff, emittedClefs);
|
|
659
663
|
voiceStrs.push(str);
|
|
660
664
|
staff = newStaff;
|
|
661
665
|
}
|
|
@@ -664,7 +668,7 @@ const serializePart = (part, currentStaff, isGrandStaff = false, measureContext,
|
|
|
664
668
|
};
|
|
665
669
|
// Serialize a measure, tracking staff state across parts
|
|
666
670
|
// Always output key/time at start of each measure
|
|
667
|
-
const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false, currentKey, currentTime,
|
|
671
|
+
const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false, currentKey, currentTime, partStaffClefs, partEmittedClefs) => {
|
|
668
672
|
const parts = [];
|
|
669
673
|
// Build measure context for all voices (key/time)
|
|
670
674
|
// Key and time are written to first voice, clef to all voices based on staff
|
|
@@ -673,12 +677,14 @@ const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false,
|
|
|
673
677
|
key: currentKey,
|
|
674
678
|
time: currentTime,
|
|
675
679
|
};
|
|
676
|
-
//
|
|
677
|
-
|
|
680
|
+
// Per-part clef state: each part has its own staff→clef maps so that distinct
|
|
681
|
+
// parts sharing staff number 1 do not clobber each other's clefs.
|
|
682
|
+
const clefsFor = (pi) => partStaffClefs?.[pi] || {};
|
|
683
|
+
const emittedFor = (pi) => partEmittedClefs?.[pi] || (partEmittedClefs ? (partEmittedClefs[pi] = {}) : {});
|
|
678
684
|
// Parts
|
|
679
685
|
let staff = currentStaff;
|
|
680
686
|
if (measure.parts.length === 1) {
|
|
681
|
-
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true,
|
|
687
|
+
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true, clefsFor(0), emittedFor(0));
|
|
682
688
|
if (partStr) {
|
|
683
689
|
parts.push(partStr);
|
|
684
690
|
}
|
|
@@ -690,7 +696,7 @@ const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false,
|
|
|
690
696
|
for (let i = 0; i < measure.parts.length; i++) {
|
|
691
697
|
const part = measure.parts[i];
|
|
692
698
|
// Pass measureContext to all parts, isFirstPart to first part only
|
|
693
|
-
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0,
|
|
699
|
+
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0, clefsFor(i), emittedFor(i));
|
|
694
700
|
if (str) {
|
|
695
701
|
partStrs.push(str);
|
|
696
702
|
}
|
|
@@ -722,6 +728,12 @@ const serializeMetadata = (metadata) => {
|
|
|
722
728
|
if (metadata.lyricist) {
|
|
723
729
|
lines.push('[lyricist "' + escapeString(metadata.lyricist) + '"]');
|
|
724
730
|
}
|
|
731
|
+
if (metadata.genre) {
|
|
732
|
+
lines.push('[genre "' + escapeString(metadata.genre) + '"]');
|
|
733
|
+
}
|
|
734
|
+
if (metadata.instrument) {
|
|
735
|
+
lines.push('[instrument "' + escapeString(metadata.instrument) + '"]');
|
|
736
|
+
}
|
|
725
737
|
if (metadata.autoBeam) {
|
|
726
738
|
lines.push('[auto-beam "' + escapeString(metadata.autoBeam) + '"]');
|
|
727
739
|
}
|
|
@@ -749,8 +761,11 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
749
761
|
let currentStaff = 1; // Parser starts at staff 1
|
|
750
762
|
let currentKey;
|
|
751
763
|
let currentTime;
|
|
752
|
-
|
|
753
|
-
|
|
764
|
+
// Clefs are tracked per part (each part is an independent instrument). Voice `staff`
|
|
765
|
+
// numbers are staff-within-part, so distinct parts may both use staff 1 — keying clef
|
|
766
|
+
// state by staff alone would conflate them. Outer key = part index, inner key = staff.
|
|
767
|
+
const partStaffClefs = {};
|
|
768
|
+
const partEmittedClefs = {};
|
|
754
769
|
for (let i = 0; i < doc.measures.length; i++) {
|
|
755
770
|
const measure = doc.measures[i];
|
|
756
771
|
// Update current key/time if measure has them
|
|
@@ -760,8 +775,9 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
760
775
|
if (measure.timeSig) {
|
|
761
776
|
currentTime = measure.timeSig;
|
|
762
777
|
}
|
|
763
|
-
// Collect clefs from this measure's voices
|
|
764
|
-
|
|
778
|
+
// Collect clefs from this measure's voices, per part
|
|
779
|
+
measure.parts.forEach((part, pi) => {
|
|
780
|
+
const staffClefs = partStaffClefs[pi] || (partStaffClefs[pi] = {});
|
|
765
781
|
for (const voice of part.voices) {
|
|
766
782
|
let clefActiveStaff = voice.staff;
|
|
767
783
|
for (const event of voice.events) {
|
|
@@ -776,8 +792,8 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
776
792
|
}
|
|
777
793
|
}
|
|
778
794
|
}
|
|
779
|
-
}
|
|
780
|
-
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime,
|
|
795
|
+
});
|
|
796
|
+
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime, partStaffClefs, partEmittedClefs);
|
|
781
797
|
// Always include measure, even if empty (use space rest for empty measures)
|
|
782
798
|
measureStrs.push(measureStr || 's1');
|
|
783
799
|
currentStaff = newStaff;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k-l-lambda/lilylet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"test:abc": "tsx ./tests/abc-decoder.ts",
|
|
37
37
|
"test:roundtrip": "tsx ./tests/lilypond-roundtrip.ts",
|
|
38
38
|
"test:abc-svg": "tsx ./tests/abc-abcjs-svg.ts",
|
|
39
|
+
"train:bpe": "tsx ./tools/trainBpeTokenizer.ts",
|
|
40
|
+
"build:manual-tokenizer": "tsx ./tools/buildManualTokenizer.ts",
|
|
41
|
+
"test:bpe": "tsx ./tests/unit/bpeTokenizer.test.ts",
|
|
39
42
|
"build:tests": "tsc -p tsconfig.tests.json; cp source/lilylet/grammar.jison.js lib-tests/source/lilylet/ && cp source/abc/grammar.jison.js lib-tests/source/abc/ && node tools/fixEsmExtensions.cjs lib-tests && ln -sfn ../../tests/assets lib-tests/tests/assets",
|
|
40
43
|
"test:roundtrip:compiled": "node lib-tests/tests/lilypond-roundtrip.js",
|
|
41
44
|
"ts": "tsx"
|
package/source/abc/abc.jison
CHANGED
|
@@ -229,6 +229,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
229
229
|
<key_signature>"treble" return 'TREBLE';
|
|
230
230
|
<key_signature>"bass" return 'BASS';
|
|
231
231
|
<key_signature>"tenor" return 'TENOR';
|
|
232
|
+
<key_signature>"alto" return 'ALTO';
|
|
232
233
|
<key_signature>"none" return 'NAME';
|
|
233
234
|
<key_signature>"Dor" return 'NAME';
|
|
234
235
|
<key_signature>"Phr" return 'NAME';
|
|
@@ -256,14 +257,16 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
256
257
|
<comment>[^\n]+ { return 'COMMENT'; }
|
|
257
258
|
<comment>\n { this.popState(); }
|
|
258
259
|
<spec_comment_name>[ \t]+ {}
|
|
259
|
-
<spec_comment_name>"score" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
|
|
260
|
-
<spec_comment_name>"staves" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
|
|
260
|
+
<spec_comment_name>"score" { this.popState(); this.pushState('spec_comment'); this._scoreDepth = 0; return 'SCORE'; }
|
|
261
|
+
<spec_comment_name>"staves" { this.popState(); this.pushState('spec_comment'); this._scoreDepth = 0; return 'SCORE'; }
|
|
261
262
|
<spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
|
|
262
263
|
<spec_comment_name>\n { this.popState(); this.popState(); }
|
|
263
264
|
<spec_comment>[ \t]+ {}
|
|
264
|
-
<spec_comment>[(
|
|
265
|
+
<spec_comment>[([{] { this._scoreDepth = (this._scoreDepth || 0) + 1; return yytext; }
|
|
266
|
+
<spec_comment>[)\]}] { this._scoreDepth = (this._scoreDepth || 0) - 1; return yytext; }
|
|
267
|
+
<spec_comment>[|] return yytext
|
|
265
268
|
<spec_comment>[\w]+ return 'NN'
|
|
266
|
-
<spec_comment>\n { this.popState(); this.popState(); return 'LAYOUT_END'; }
|
|
269
|
+
<spec_comment>\n { if (this._scoreDepth > 0) { /* layout continues on next line */ } else { this.popState(); this.popState(); return 'LAYOUT_END'; } }
|
|
267
270
|
<spec_comment_skip>[^\n]+ {}
|
|
268
271
|
<spec_comment_skip>\n { this.popState(); this.popState(); }
|
|
269
272
|
|
|
@@ -399,6 +402,7 @@ staff_shift
|
|
|
399
402
|
key_signature
|
|
400
403
|
: key_root -> $1
|
|
401
404
|
| NAME -> key(null, $1)
|
|
405
|
+
| clef -> $1
|
|
402
406
|
;
|
|
403
407
|
|
|
404
408
|
key_root
|
|
@@ -412,6 +416,11 @@ clef
|
|
|
412
416
|
: TREBLE -> clef($1)
|
|
413
417
|
| BASS -> clef($1)
|
|
414
418
|
| TENOR -> clef($1)
|
|
419
|
+
| ALTO -> clef($1)
|
|
420
|
+
| TREBLE plus_minus_number -> clef($1 + ($2 < 0 ? '-' : '+') + Math.abs($2))
|
|
421
|
+
| BASS plus_minus_number -> clef($1 + ($2 < 0 ? '-' : '+') + Math.abs($2))
|
|
422
|
+
| TENOR plus_minus_number -> clef($1 + ($2 < 0 ? '-' : '+') + Math.abs($2))
|
|
423
|
+
| ALTO plus_minus_number -> clef($1 + ($2 < 0 ? '-' : '+') + Math.abs($2))
|
|
415
424
|
;
|
|
416
425
|
|
|
417
426
|
sharp_or_flat
|
|
@@ -460,8 +469,11 @@ voice_exp
|
|
|
460
469
|
: number -> voice($1)
|
|
461
470
|
| number NAME -> voice($1, $2)
|
|
462
471
|
| number NAME assigns -> voice($1, $2, $3)
|
|
472
|
+
| number NAME plus_minus_number -> voice($1, $2 + ($3 < 0 ? '-' : '+') + Math.abs($3))
|
|
473
|
+
| number NAME plus_minus_number assigns -> voice($1, $2 + ($3 < 0 ? '-' : '+') + Math.abs($3), $4)
|
|
463
474
|
| NAME -> voice(1, $1)
|
|
464
475
|
| NAME assigns -> voice(1, $1, $2)
|
|
476
|
+
| NAME plus_minus_number assigns -> voice(1, $1, $3)
|
|
465
477
|
| upper_phonet number -> voice(1, $1 + String($2))
|
|
466
478
|
| upper_phonet number assigns -> voice(1, $1 + String($2), $3)
|
|
467
479
|
| upper_phonet number NAME -> voice(1, $1 + String($2))
|
|
@@ -524,6 +536,7 @@ bar
|
|
|
524
536
|
| ':' '|' '|' ':' -> ':|:'
|
|
525
537
|
| '|' '|' -> '||'
|
|
526
538
|
| '|' ']' -> '|]'
|
|
539
|
+
| '|' ']' ':' -> '|]'
|
|
527
540
|
| ':' '|' ']' -> ':|]'
|
|
528
541
|
| '|' N -> '|' + $2
|
|
529
542
|
| ':' '|' N -> ':|' + $2
|
|
@@ -586,6 +599,8 @@ articulation_content
|
|
|
586
599
|
| a -> articulation($1)
|
|
587
600
|
| "^" -> articulation($1)
|
|
588
601
|
| fingering_numbers -> ({fingering: Number($1)})
|
|
602
|
+
| '(' fingering_numbers ')' -> ({fingering: Number($2)})
|
|
603
|
+
| '(' fingering_numbers -> ({fingering: Number($2)})
|
|
589
604
|
| tremolo -> ({tremolo: $1})
|
|
590
605
|
| tremolo '-' -> ({tremolo: $1}) // unknown meaning of '-'?
|
|
591
606
|
;
|