@k-l-lambda/lilylet 0.1.65 → 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 +52 -10
- 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 +50 -12
- 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 = {
|
|
@@ -128,7 +162,9 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals)
|
|
|
128
162
|
// Lilylet octave: 0 = middle C octave (C4), positive = higher, negative = lower
|
|
129
163
|
// When ottava is active, the source pitch is the sounding pitch, but we need to output the written pitch
|
|
130
164
|
// For 8va up (ottavaShift=1), written pitch is one octave lower than sounding
|
|
131
|
-
const
|
|
165
|
+
const soundingOct = 4 + pitch.octave;
|
|
166
|
+
const oct = soundingOct - ottavaShift;
|
|
167
|
+
const octGes = ottavaShift !== 0 ? soundingOct : undefined;
|
|
132
168
|
// Get the accidental implied by the key signature for this note
|
|
133
169
|
const keyAccidentals = getKeyAccidentals(keyFifths);
|
|
134
170
|
const keyAccid = keyAccidentals[pitch.phonet];
|
|
@@ -189,7 +225,7 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals)
|
|
|
189
225
|
measureAccidentals.set(pitchKey, 'n');
|
|
190
226
|
}
|
|
191
227
|
}
|
|
192
|
-
return { pname: pitch.phonet, oct, accid, accidGes };
|
|
228
|
+
return { pname: pitch.phonet, oct, octGes, accid, accidGes };
|
|
193
229
|
};
|
|
194
230
|
// Convert tremolo division to stem.mod value
|
|
195
231
|
const tremoloToStemMod = (division) => {
|
|
@@ -209,6 +245,8 @@ const buildNoteElement = (pitch, dur, dots, indent, inChord, options = {}, noteI
|
|
|
209
245
|
}
|
|
210
246
|
if (pitch.accid)
|
|
211
247
|
attrs += ` accid="${pitch.accid}"`;
|
|
248
|
+
if (pitch.octGes !== undefined)
|
|
249
|
+
attrs += ` oct.ges="${pitch.octGes}"`;
|
|
212
250
|
if (pitch.accidGes)
|
|
213
251
|
attrs += ` accid.ges="${pitch.accidGes}"`;
|
|
214
252
|
if (!inChord && dots > 0)
|
|
@@ -616,8 +654,7 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
616
654
|
const effectiveStaffNum = effectiveStaff ?? layerStaffNum;
|
|
617
655
|
if (effectiveStaffNum === layerStaffNum) {
|
|
618
656
|
const clefIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
619
|
-
|
|
620
|
-
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`;
|
|
621
658
|
}
|
|
622
659
|
activeClef = ctx.clef;
|
|
623
660
|
endingClef = ctx.clef;
|
|
@@ -655,6 +692,8 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measur
|
|
|
655
692
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}" dur="${noteDur}"`;
|
|
656
693
|
if (pitch.accid)
|
|
657
694
|
attrs += ` accid="${pitch.accid}"`;
|
|
695
|
+
if (pitch.octGes !== undefined)
|
|
696
|
+
attrs += ` oct.ges="${pitch.octGes}"`;
|
|
658
697
|
if (pitch.accidGes)
|
|
659
698
|
attrs += ` accid.ges="${pitch.accidGes}"`;
|
|
660
699
|
result += `${indent} <note ${attrs} />\n`;
|
|
@@ -666,6 +705,8 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measur
|
|
|
666
705
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}"`;
|
|
667
706
|
if (pitch.accid)
|
|
668
707
|
attrs += ` accid="${pitch.accid}"`;
|
|
708
|
+
if (pitch.octGes !== undefined)
|
|
709
|
+
attrs += ` oct.ges="${pitch.octGes}"`;
|
|
669
710
|
if (pitch.accidGes)
|
|
670
711
|
attrs += ` accid.ges="${pitch.accidGes}"`;
|
|
671
712
|
result += `${indent} <note ${attrs} />\n`;
|
|
@@ -678,6 +719,8 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measur
|
|
|
678
719
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}" dur="${noteDur}"`;
|
|
679
720
|
if (pitch.accid)
|
|
680
721
|
attrs += ` accid="${pitch.accid}"`;
|
|
722
|
+
if (pitch.octGes !== undefined)
|
|
723
|
+
attrs += ` oct.ges="${pitch.octGes}"`;
|
|
681
724
|
if (pitch.accidGes)
|
|
682
725
|
attrs += ` accid.ges="${pitch.accidGes}"`;
|
|
683
726
|
result += `${indent} <note ${attrs} />\n`;
|
|
@@ -689,6 +732,8 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measur
|
|
|
689
732
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}"`;
|
|
690
733
|
if (pitch.accid)
|
|
691
734
|
attrs += ` accid="${pitch.accid}"`;
|
|
735
|
+
if (pitch.octGes !== undefined)
|
|
736
|
+
attrs += ` oct.ges="${pitch.octGes}"`;
|
|
692
737
|
if (pitch.accidGes)
|
|
693
738
|
attrs += ` accid.ges="${pitch.accidGes}"`;
|
|
694
739
|
result += `${indent} <note ${attrs} />\n`;
|
|
@@ -1002,8 +1047,7 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
1002
1047
|
if (ctx.clef && ctx.clef !== currentClef) {
|
|
1003
1048
|
const layerStaff = voice.staff || 1;
|
|
1004
1049
|
if (currentStaff === layerStaff) {
|
|
1005
|
-
|
|
1006
|
-
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`;
|
|
1007
1051
|
}
|
|
1008
1052
|
currentClef = ctx.clef;
|
|
1009
1053
|
}
|
|
@@ -1568,8 +1612,7 @@ const encodeScoreDef = (keySig, timeNum, timeDen, partInfos, indent, meterSymbol
|
|
|
1568
1612
|
for (let ls = 1; ls <= info.maxStaff; ls++) {
|
|
1569
1613
|
const globalStaff = info.staffOffset + ls;
|
|
1570
1614
|
const clef = info.clefs[ls] || Clef.treble;
|
|
1571
|
-
|
|
1572
|
-
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`;
|
|
1573
1616
|
}
|
|
1574
1617
|
xml += `${indent} </staffGrp>\n`;
|
|
1575
1618
|
}
|
|
@@ -1577,8 +1620,7 @@ const encodeScoreDef = (keySig, timeNum, timeDen, partInfos, indent, meterSymbol
|
|
|
1577
1620
|
// Single staff part
|
|
1578
1621
|
const globalStaff = info.staffOffset + 1;
|
|
1579
1622
|
const clef = info.clefs[1] || Clef.treble;
|
|
1580
|
-
|
|
1581
|
-
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`;
|
|
1582
1624
|
}
|
|
1583
1625
|
}
|
|
1584
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
|
;
|