@k-l-lambda/lilylet 0.1.71 → 0.1.72
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/highlight.d.ts +1 -0
- package/lib/highlight.js +1 -0
- package/lib/lilylet/highlight.d.ts +29 -0
- package/lib/lilylet/highlight.js +145 -0
- package/package.json +8 -2
- package/source/lilylet/highlight.ts +192 -0
- package/lib/source/abc/abc.d.ts +0 -102
- package/lib/source/abc/abc.js +0 -25
- package/lib/source/abc/parser.d.ts +0 -3
- package/lib/source/abc/parser.js +0 -6
- package/lib/source/lilylet/abcDecoder.d.ts +0 -25
- package/lib/source/lilylet/abcDecoder.js +0 -1035
- package/lib/source/lilylet/index.d.ts +0 -10
- package/lib/source/lilylet/index.js +0 -10
- package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
- package/lib/source/lilylet/lilypondDecoder.js +0 -1223
- package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
- package/lib/source/lilylet/lilypondEncoder.js +0 -893
- package/lib/source/lilylet/meiEncoder.d.ts +0 -8
- package/lib/source/lilylet/meiEncoder.js +0 -1985
- package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
- package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
- package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
- package/lib/source/lilylet/musicXmlEncoder.js +0 -701
- package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
- package/lib/source/lilylet/musicXmlTypes.js +0 -7
- package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
- package/lib/source/lilylet/musicXmlUtils.js +0 -469
- package/lib/source/lilylet/parser.d.ts +0 -14
- package/lib/source/lilylet/parser.js +0 -161
- package/lib/source/lilylet/serializer.d.ts +0 -11
- package/lib/source/lilylet/serializer.js +0 -791
- package/lib/source/lilylet/types.d.ts +0 -253
- package/lib/source/lilylet/types.js +0 -100
- package/lib/tests/abc-abcjs-parse.d.ts +0 -8
- package/lib/tests/abc-abcjs-parse.js +0 -90
- package/lib/tests/abc-abcjs-svg.d.ts +0 -1
- package/lib/tests/abc-abcjs-svg.js +0 -143
- package/lib/tests/abc-decoder.d.ts +0 -1
- package/lib/tests/abc-decoder.js +0 -67
- package/lib/tests/abc-mei-compare.d.ts +0 -1
- package/lib/tests/abc-mei-compare.js +0 -525
- package/lib/tests/auto-beam.d.ts +0 -9
- package/lib/tests/auto-beam.js +0 -151
- package/lib/tests/computeMeiHashes.d.ts +0 -1
- package/lib/tests/computeMeiHashes.js +0 -87
- package/lib/tests/encoder-mutation.d.ts +0 -9
- package/lib/tests/encoder-mutation.js +0 -110
- package/lib/tests/gpt-review-issues.d.ts +0 -5
- package/lib/tests/gpt-review-issues.js +0 -255
- package/lib/tests/json-to-lyl.d.ts +0 -1
- package/lib/tests/json-to-lyl.js +0 -18
- package/lib/tests/lilypond-roundtrip.d.ts +0 -7
- package/lib/tests/lilypond-roundtrip.js +0 -558
- package/lib/tests/lilypondDecoder.d.ts +0 -6
- package/lib/tests/lilypondDecoder.js +0 -95
- package/lib/tests/ly-to-lyl.d.ts +0 -1
- package/lib/tests/ly-to-lyl.js +0 -12
- package/lib/tests/mei.d.ts +0 -1
- package/lib/tests/mei.js +0 -278
- package/lib/tests/musicxml-decoder.d.ts +0 -4
- package/lib/tests/musicxml-decoder.js +0 -61
- package/lib/tests/musicxml-detail.d.ts +0 -4
- package/lib/tests/musicxml-detail.js +0 -85
- package/lib/tests/musicxml-fprod.d.ts +0 -9
- package/lib/tests/musicxml-fprod.js +0 -153
- package/lib/tests/musicxml-roundtrip.d.ts +0 -7
- package/lib/tests/musicxml-roundtrip.js +0 -296
- package/lib/tests/musicxml-to-mei.d.ts +0 -6
- package/lib/tests/musicxml-to-mei.js +0 -115
- package/lib/tests/parser.d.ts +0 -1
- package/lib/tests/parser.js +0 -17
- package/lib/tests/render-k283.d.ts +0 -1
- package/lib/tests/render-k283.js +0 -33
- package/lib/tests/render-lyl.d.ts +0 -1
- package/lib/tests/render-lyl.js +0 -35
- package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
- package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
- package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
- package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
- package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
- package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
- package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
- package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
- package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
- package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
- package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
- package/lib/tests/unit/gptReviewIssues.test.js +0 -240
- package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
- package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
- package/lib/tests/unit/partialWarning.test.d.ts +0 -4
- package/lib/tests/unit/partialWarning.test.js +0 -65
- package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
- package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
- package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
- package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
- package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
- package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
- package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
- package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
- package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
- package/lib/tests/unit/voiceStaffParsing.test.js +0 -118
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import createVerovioModule from "verovio/wasm";
|
|
6
|
-
// @ts-ignore
|
|
7
|
-
import { VerovioToolkit } from "verovio/esm";
|
|
8
|
-
import { DOMParser } from "@xmldom/xmldom";
|
|
9
|
-
import { abcDecoder, meiEncoder } from "../source/lilylet/index.js";
|
|
10
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const ABC_DIR = path.join(__dirname, "assets/abc");
|
|
12
|
-
const OUTPUT_DIR = path.join(__dirname, "output/abc-mei-compare");
|
|
13
|
-
const MEI_NS = "http://www.music-encoding.org/ns/mei";
|
|
14
|
-
/** Count V: declarations in ABC source to determine voice count. */
|
|
15
|
-
const countAbcVoices = (abc) => {
|
|
16
|
-
const voiceIds = new Set();
|
|
17
|
-
for (const line of abc.split("\n")) {
|
|
18
|
-
const m = line.match(/^V:\s*(\S+)/);
|
|
19
|
-
if (m)
|
|
20
|
-
voiceIds.add(m[1]);
|
|
21
|
-
// Also count inline [V:x] directives
|
|
22
|
-
const inlines = line.matchAll(/\[V:(\S+?)\]/g);
|
|
23
|
-
for (const im of inlines)
|
|
24
|
-
voiceIds.add(im[1]);
|
|
25
|
-
}
|
|
26
|
-
return Math.max(1, voiceIds.size);
|
|
27
|
-
};
|
|
28
|
-
/** Extract only voice-1 measures from Verovio's flat measure list.
|
|
29
|
-
* Verovio creates one <measure> per [V:x]..| segment, cycling through voices.
|
|
30
|
-
* Voice 1 measures are at indices 0, numVoices, 2*numVoices, etc.
|
|
31
|
-
*/
|
|
32
|
-
const extractVoice1Measures = (allMeasures, numVoices) => {
|
|
33
|
-
if (numVoices <= 1)
|
|
34
|
-
return allMeasures;
|
|
35
|
-
const result = [];
|
|
36
|
-
for (let i = 0; i < allMeasures.length; i += numVoices) {
|
|
37
|
-
result.push({ ...allMeasures[i], number: result.length + 1 });
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
};
|
|
41
|
-
// ─── MEI XML Extraction ─────────────────────────────────────────────
|
|
42
|
-
const extractMeasures = (meiXml) => {
|
|
43
|
-
const doc = new DOMParser().parseFromString(meiXml, "text/xml");
|
|
44
|
-
const measures = [];
|
|
45
|
-
const body = doc.getElementsByTagNameNS(MEI_NS, "body")[0]
|
|
46
|
-
|| doc.getElementsByTagName("body")[0];
|
|
47
|
-
if (!body)
|
|
48
|
-
return measures;
|
|
49
|
-
const sections = body.getElementsByTagNameNS(MEI_NS, "section");
|
|
50
|
-
const section = sections.length > 0 ? sections[0] : body;
|
|
51
|
-
// Collect all <measure> elements
|
|
52
|
-
const measureEls = section.getElementsByTagNameNS(MEI_NS, "measure");
|
|
53
|
-
for (let mi = 0; mi < measureEls.length; mi++) {
|
|
54
|
-
const measureEl = measureEls[mi];
|
|
55
|
-
const mNum = parseInt(measureEl.getAttribute("n") || String(mi + 1));
|
|
56
|
-
const info = { number: mNum, notes: [] };
|
|
57
|
-
// Find staff 1
|
|
58
|
-
const staves = measureEl.getElementsByTagNameNS(MEI_NS, "staff");
|
|
59
|
-
let staff1 = null;
|
|
60
|
-
for (let si = 0; si < staves.length; si++) {
|
|
61
|
-
if (staves[si].getAttribute("n") === "1") {
|
|
62
|
-
staff1 = staves[si];
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (!staff1 && staves.length > 0)
|
|
67
|
-
staff1 = staves[0];
|
|
68
|
-
if (!staff1) {
|
|
69
|
-
measures.push(info);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
// Find layer 1
|
|
73
|
-
const layers = staff1.getElementsByTagNameNS(MEI_NS, "layer");
|
|
74
|
-
let layer1 = null;
|
|
75
|
-
for (let li = 0; li < layers.length; li++) {
|
|
76
|
-
if (layers[li].getAttribute("n") === "1") {
|
|
77
|
-
layer1 = layers[li];
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (!layer1 && layers.length > 0)
|
|
82
|
-
layer1 = layers[0];
|
|
83
|
-
if (!layer1) {
|
|
84
|
-
measures.push(info);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
// Extract key/time from scoreDef or staffDef preceding this measure
|
|
88
|
-
const scoreDefs = measureEl.getElementsByTagNameNS(MEI_NS, "scoreDef");
|
|
89
|
-
if (scoreDefs.length > 0) {
|
|
90
|
-
const sd = scoreDefs[0];
|
|
91
|
-
const keySig = sd.getAttribute("key.sig");
|
|
92
|
-
const timeSig = sd.getAttribute("meter.count") && sd.getAttribute("meter.unit")
|
|
93
|
-
? `${sd.getAttribute("meter.count")}/${sd.getAttribute("meter.unit")}`
|
|
94
|
-
: undefined;
|
|
95
|
-
if (keySig)
|
|
96
|
-
info.keySig = keySig;
|
|
97
|
-
if (timeSig)
|
|
98
|
-
info.timeSig = timeSig;
|
|
99
|
-
}
|
|
100
|
-
// Also check staffDef inside the measure for key/time
|
|
101
|
-
const staffDefs = measureEl.getElementsByTagNameNS(MEI_NS, "staffDef");
|
|
102
|
-
for (let sdi = 0; sdi < staffDefs.length; sdi++) {
|
|
103
|
-
const sd = staffDefs[sdi];
|
|
104
|
-
if (sd.getAttribute("n") === "1" || !sd.getAttribute("n")) {
|
|
105
|
-
const keySig = sd.getAttribute("key.sig");
|
|
106
|
-
const meterCount = sd.getAttribute("meter.count");
|
|
107
|
-
const meterUnit = sd.getAttribute("meter.unit");
|
|
108
|
-
if (keySig)
|
|
109
|
-
info.keySig = keySig;
|
|
110
|
-
if (meterCount && meterUnit)
|
|
111
|
-
info.timeSig = `${meterCount}/${meterUnit}`;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// Extract notes and rests from layer 1
|
|
115
|
-
extractNotesFromElement(layer1, info.notes);
|
|
116
|
-
measures.push(info);
|
|
117
|
-
}
|
|
118
|
-
// Also extract initial key/time from the scoreDef in the header
|
|
119
|
-
const initialScoreDef = doc.getElementsByTagNameNS(MEI_NS, "scoreDef")[0];
|
|
120
|
-
if (initialScoreDef && measures.length > 0) {
|
|
121
|
-
const keySig = initialScoreDef.getAttribute("key.sig");
|
|
122
|
-
const meterCount = initialScoreDef.getAttribute("meter.count");
|
|
123
|
-
const meterUnit = initialScoreDef.getAttribute("meter.unit");
|
|
124
|
-
if (keySig && !measures[0].keySig)
|
|
125
|
-
measures[0].keySig = keySig;
|
|
126
|
-
if (meterCount && meterUnit && !measures[0].timeSig) {
|
|
127
|
-
measures[0].timeSig = `${meterCount}/${meterUnit}`;
|
|
128
|
-
}
|
|
129
|
-
// Check inside staffDef too
|
|
130
|
-
const staffDefs = initialScoreDef.getElementsByTagNameNS(MEI_NS, "staffDef");
|
|
131
|
-
for (let i = 0; i < staffDefs.length; i++) {
|
|
132
|
-
const sd = staffDefs[i];
|
|
133
|
-
if (sd.getAttribute("n") === "1" || !sd.getAttribute("n")) {
|
|
134
|
-
const ks = sd.getAttribute("key.sig");
|
|
135
|
-
const mc = sd.getAttribute("meter.count");
|
|
136
|
-
const mu = sd.getAttribute("meter.unit");
|
|
137
|
-
if (ks && !measures[0].keySig)
|
|
138
|
-
measures[0].keySig = ks;
|
|
139
|
-
if (mc && mu && !measures[0].timeSig)
|
|
140
|
-
measures[0].timeSig = `${mc}/${mu}`;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return measures;
|
|
145
|
-
};
|
|
146
|
-
const extractNotesFromElement = (el, notes) => {
|
|
147
|
-
for (let i = 0; i < el.childNodes.length; i++) {
|
|
148
|
-
const child = el.childNodes[i];
|
|
149
|
-
if (!child.tagName)
|
|
150
|
-
continue;
|
|
151
|
-
const localName = child.localName || child.tagName.replace(/^.*:/, "");
|
|
152
|
-
if (localName === "note") {
|
|
153
|
-
const pname = child.getAttribute("pname");
|
|
154
|
-
const oct = child.getAttribute("oct");
|
|
155
|
-
const dur = child.getAttribute("dur");
|
|
156
|
-
if (pname && oct && dur) {
|
|
157
|
-
const accid = child.getAttribute("accid") || child.getAttribute("accid.ges") || undefined;
|
|
158
|
-
const dots = parseInt(child.getAttribute("dots") || "0");
|
|
159
|
-
notes.push({
|
|
160
|
-
pname,
|
|
161
|
-
oct: parseInt(oct),
|
|
162
|
-
accid,
|
|
163
|
-
dur,
|
|
164
|
-
dots,
|
|
165
|
-
isRest: false,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
// Check for child accid element
|
|
169
|
-
if (pname && oct && dur) {
|
|
170
|
-
const accidEls = child.getElementsByTagNameNS(MEI_NS, "accid");
|
|
171
|
-
if (accidEls.length > 0 && !notes[notes.length - 1].accid) {
|
|
172
|
-
const a = accidEls[0].getAttribute("accid") || accidEls[0].getAttribute("accid.ges");
|
|
173
|
-
if (a)
|
|
174
|
-
notes[notes.length - 1].accid = a;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else if (localName === "rest" || localName === "mRest") {
|
|
179
|
-
const dur = child.getAttribute("dur") || (localName === "mRest" ? "1" : undefined);
|
|
180
|
-
if (dur) {
|
|
181
|
-
notes.push({
|
|
182
|
-
pname: "",
|
|
183
|
-
oct: 0,
|
|
184
|
-
dur,
|
|
185
|
-
dots: parseInt(child.getAttribute("dots") || "0"),
|
|
186
|
-
isRest: true,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
else if (localName === "chord") {
|
|
191
|
-
// For chords, extract all notes inside
|
|
192
|
-
extractNotesFromElement(child, notes);
|
|
193
|
-
}
|
|
194
|
-
else if (localName === "beam" || localName === "tuplet") {
|
|
195
|
-
// Recurse into containers
|
|
196
|
-
extractNotesFromElement(child, notes);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
// ─── Comparison Logic ────────────────────────────────────────────────
|
|
201
|
-
const compareFiles = (verovioMeasures, lilyletMeasures) => {
|
|
202
|
-
const diffs = [];
|
|
203
|
-
const measureCount = Math.min(verovioMeasures.length, lilyletMeasures.length);
|
|
204
|
-
let totalCompared = 0;
|
|
205
|
-
let totalMatched = 0;
|
|
206
|
-
for (let mi = 0; mi < measureCount; mi++) {
|
|
207
|
-
const vm = verovioMeasures[mi];
|
|
208
|
-
const lm = lilyletMeasures[mi];
|
|
209
|
-
// Compare key/time sigs
|
|
210
|
-
if (vm.keySig && lm.keySig && vm.keySig !== lm.keySig) {
|
|
211
|
-
diffs.push({ measure: mi + 1, index: -1, field: "keySig", verovio: vm.keySig, lilylet: lm.keySig });
|
|
212
|
-
}
|
|
213
|
-
if (vm.timeSig && lm.timeSig && vm.timeSig !== lm.timeSig) {
|
|
214
|
-
diffs.push({ measure: mi + 1, index: -1, field: "timeSig", verovio: vm.timeSig, lilylet: lm.timeSig });
|
|
215
|
-
}
|
|
216
|
-
// Compare notes
|
|
217
|
-
const noteCount = Math.min(vm.notes.length, lm.notes.length);
|
|
218
|
-
if (vm.notes.length !== lm.notes.length) {
|
|
219
|
-
diffs.push({
|
|
220
|
-
measure: mi + 1,
|
|
221
|
-
index: -1,
|
|
222
|
-
field: "noteCount",
|
|
223
|
-
verovio: String(vm.notes.length),
|
|
224
|
-
lilylet: String(lm.notes.length),
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
for (let ni = 0; ni < noteCount; ni++) {
|
|
228
|
-
const vn = vm.notes[ni];
|
|
229
|
-
const ln = lm.notes[ni];
|
|
230
|
-
totalCompared++;
|
|
231
|
-
let matched = true;
|
|
232
|
-
if (vn.isRest !== ln.isRest) {
|
|
233
|
-
diffs.push({ measure: mi + 1, index: ni, field: "rest/note", verovio: vn.isRest ? "rest" : "note", lilylet: ln.isRest ? "rest" : "note" });
|
|
234
|
-
matched = false;
|
|
235
|
-
}
|
|
236
|
-
else if (!vn.isRest) {
|
|
237
|
-
// Compare pitch
|
|
238
|
-
if (vn.pname !== ln.pname || vn.oct !== ln.oct) {
|
|
239
|
-
diffs.push({ measure: mi + 1, index: ni, field: "pitch", verovio: `${vn.pname}${vn.oct}`, lilylet: `${ln.pname}${ln.oct}` });
|
|
240
|
-
matched = false;
|
|
241
|
-
}
|
|
242
|
-
// Compare accidentals
|
|
243
|
-
if ((vn.accid || "") !== (ln.accid || "")) {
|
|
244
|
-
diffs.push({ measure: mi + 1, index: ni, field: "accid", verovio: vn.accid || "none", lilylet: ln.accid || "none" });
|
|
245
|
-
matched = false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Compare duration
|
|
249
|
-
if (vn.dur !== ln.dur) {
|
|
250
|
-
diffs.push({ measure: mi + 1, index: ni, field: "dur", verovio: vn.dur, lilylet: ln.dur });
|
|
251
|
-
matched = false;
|
|
252
|
-
}
|
|
253
|
-
if (vn.dots !== ln.dots) {
|
|
254
|
-
diffs.push({ measure: mi + 1, index: ni, field: "dots", verovio: String(vn.dots), lilylet: String(ln.dots) });
|
|
255
|
-
matched = false;
|
|
256
|
-
}
|
|
257
|
-
if (matched)
|
|
258
|
-
totalMatched++;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const matchPercent = totalCompared > 0 ? Math.round((totalMatched / totalCompared) * 100) : 0;
|
|
262
|
-
return { matchPercent, diffs };
|
|
263
|
-
};
|
|
264
|
-
// ─── Main ────────────────────────────────────────────────────────────
|
|
265
|
-
const main = async () => {
|
|
266
|
-
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
267
|
-
// Initialize Verovio
|
|
268
|
-
console.log("Initializing Verovio...");
|
|
269
|
-
const VerovioModule = await createVerovioModule();
|
|
270
|
-
const vrvToolkit = new VerovioToolkit(VerovioModule);
|
|
271
|
-
console.log("Verovio initialized.\n");
|
|
272
|
-
const files = fs.readdirSync(ABC_DIR).filter(f => f.endsWith(".abc")).sort();
|
|
273
|
-
console.log(`Found ${files.length} ABC files\n`);
|
|
274
|
-
const results = [];
|
|
275
|
-
for (const file of files) {
|
|
276
|
-
const baseName = path.basename(file, ".abc");
|
|
277
|
-
const abcContent = fs.readFileSync(path.join(ABC_DIR, file), "utf-8");
|
|
278
|
-
let verovioMei = "";
|
|
279
|
-
let lilyletMei = "";
|
|
280
|
-
let verovioLog = "";
|
|
281
|
-
let error;
|
|
282
|
-
// ── Direct path: ABC → Verovio → MEI ──
|
|
283
|
-
try {
|
|
284
|
-
vrvToolkit.setOptions({
|
|
285
|
-
scale: 40,
|
|
286
|
-
adjustPageHeight: true,
|
|
287
|
-
pageHeight: 6000,
|
|
288
|
-
pageWidth: 2100,
|
|
289
|
-
inputFrom: "abc",
|
|
290
|
-
});
|
|
291
|
-
const loaded = vrvToolkit.loadData(abcContent);
|
|
292
|
-
verovioLog = vrvToolkit.getLog();
|
|
293
|
-
if (loaded) {
|
|
294
|
-
verovioMei = vrvToolkit.getMEI();
|
|
295
|
-
// Render Verovio SVG
|
|
296
|
-
const verovioSvg = vrvToolkit.renderToSVG(1);
|
|
297
|
-
if (verovioSvg) {
|
|
298
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.verovio.svg`), verovioSvg);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
error = `Verovio failed to load: ${verovioLog}`;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
error = `Verovio error: ${err.message}`;
|
|
307
|
-
}
|
|
308
|
-
// ── Indirect path: ABC → lilylet abcDecoder → meiEncoder → MEI ──
|
|
309
|
-
try {
|
|
310
|
-
const doc = abcDecoder.decode(abcContent);
|
|
311
|
-
lilyletMei = meiEncoder.encode(doc);
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
error = (error ? error + "; " : "") + `Lilylet error: ${err.message}`;
|
|
315
|
-
}
|
|
316
|
-
// Render lilylet MEI via a fresh Verovio toolkit (ABC mode contaminates state)
|
|
317
|
-
if (lilyletMei) {
|
|
318
|
-
try {
|
|
319
|
-
const vrvMei = new VerovioToolkit(VerovioModule);
|
|
320
|
-
vrvMei.setOptions({
|
|
321
|
-
scale: 40,
|
|
322
|
-
adjustPageHeight: true,
|
|
323
|
-
pageHeight: 6000,
|
|
324
|
-
pageWidth: 2100,
|
|
325
|
-
});
|
|
326
|
-
const loaded = vrvMei.loadData(lilyletMei);
|
|
327
|
-
if (loaded) {
|
|
328
|
-
const lilyletSvg = vrvMei.renderToSVG(1);
|
|
329
|
-
if (lilyletSvg) {
|
|
330
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.lilylet.svg`), lilyletSvg);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// Verovio rendering of lilylet MEI failed — not a comparison error
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Save MEI files
|
|
339
|
-
if (verovioMei) {
|
|
340
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.verovio.mei`), verovioMei);
|
|
341
|
-
}
|
|
342
|
-
if (lilyletMei) {
|
|
343
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.lilylet.mei`), lilyletMei);
|
|
344
|
-
}
|
|
345
|
-
// ── Extract and compare ──
|
|
346
|
-
const numVoices = countAbcVoices(abcContent);
|
|
347
|
-
const verovioAllMeasures = verovioMei ? extractMeasures(verovioMei) : [];
|
|
348
|
-
const verovioMeasures = extractVoice1Measures(verovioAllMeasures, numVoices);
|
|
349
|
-
const lilyletMeasures = lilyletMei ? extractMeasures(lilyletMei) : [];
|
|
350
|
-
const { matchPercent, diffs } = compareFiles(verovioMeasures, lilyletMeasures);
|
|
351
|
-
const totalNotes = {
|
|
352
|
-
verovio: verovioMeasures.reduce((s, m) => s + m.notes.length, 0),
|
|
353
|
-
lilylet: lilyletMeasures.reduce((s, m) => s + m.notes.length, 0),
|
|
354
|
-
};
|
|
355
|
-
const result = {
|
|
356
|
-
name: baseName,
|
|
357
|
-
verovioMeasures,
|
|
358
|
-
lilyletMeasures,
|
|
359
|
-
verovioLog,
|
|
360
|
-
error,
|
|
361
|
-
matchPercent,
|
|
362
|
-
totalNotes,
|
|
363
|
-
diffs,
|
|
364
|
-
};
|
|
365
|
-
results.push(result);
|
|
366
|
-
// Console summary
|
|
367
|
-
const status = error ? "ERR" : matchPercent >= 90 ? "OK " : "DIF";
|
|
368
|
-
const noteInfo = `notes V:${totalNotes.verovio} L:${totalNotes.lilylet}`;
|
|
369
|
-
const measInfo = `meas V:${verovioMeasures.length} L:${lilyletMeasures.length}`;
|
|
370
|
-
console.log(`[${status}] ${baseName.padEnd(28)} ${measInfo.padEnd(16)} ${noteInfo.padEnd(20)} match:${matchPercent}%`);
|
|
371
|
-
if (error)
|
|
372
|
-
console.log(` ${error.substring(0, 120)}`);
|
|
373
|
-
}
|
|
374
|
-
// ── Generate report ──
|
|
375
|
-
generateReport(results);
|
|
376
|
-
// ── Generate comparison HTML gallery ──
|
|
377
|
-
generateComparisonHtml(results);
|
|
378
|
-
// Summary
|
|
379
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
380
|
-
const ok = results.filter(r => !r.error && r.matchPercent >= 90).length;
|
|
381
|
-
const diff = results.filter(r => !r.error && r.matchPercent < 90).length;
|
|
382
|
-
const err = results.filter(r => r.error).length;
|
|
383
|
-
console.log(`Results: ${ok} match(>=90%), ${diff} differ, ${err} error out of ${results.length}`);
|
|
384
|
-
console.log(`Output: ${OUTPUT_DIR}`);
|
|
385
|
-
console.log(`Report: ${OUTPUT_DIR}/report.md`);
|
|
386
|
-
};
|
|
387
|
-
// ─── Report ──────────────────────────────────────────────────────────
|
|
388
|
-
const generateReport = (results) => {
|
|
389
|
-
const lines = [];
|
|
390
|
-
lines.push("# ABC → MEI Comparison Report\n");
|
|
391
|
-
lines.push(`Generated: ${new Date().toISOString()}\n`);
|
|
392
|
-
// Summary table
|
|
393
|
-
lines.push("## Summary\n");
|
|
394
|
-
lines.push("| File | Measures (V/L) | Notes (V/L) | Match % | Status |");
|
|
395
|
-
lines.push("|------|---------------|-------------|---------|--------|");
|
|
396
|
-
for (const r of results) {
|
|
397
|
-
const measV = r.verovioMeasures.length;
|
|
398
|
-
const measL = r.lilyletMeasures.length;
|
|
399
|
-
const status = r.error ? "Error" : r.matchPercent >= 90 ? "OK" : "Diff";
|
|
400
|
-
lines.push(`| ${r.name} | ${measV}/${measL} | ${r.totalNotes.verovio}/${r.totalNotes.lilylet} | ${r.matchPercent}% | ${status} |`);
|
|
401
|
-
}
|
|
402
|
-
// Per-file details
|
|
403
|
-
lines.push("\n## Per-File Details\n");
|
|
404
|
-
for (const r of results) {
|
|
405
|
-
if (r.error || r.diffs.length === 0) {
|
|
406
|
-
if (r.error) {
|
|
407
|
-
lines.push(`### ${r.name}\n`);
|
|
408
|
-
lines.push(`**Error**: ${r.error}\n`);
|
|
409
|
-
}
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
lines.push(`### ${r.name}\n`);
|
|
413
|
-
lines.push(`- Measures: Verovio ${r.verovioMeasures.length}, Lilylet ${r.lilyletMeasures.length}`);
|
|
414
|
-
lines.push(`- Notes: Verovio ${r.totalNotes.verovio}, Lilylet ${r.totalNotes.lilylet}`);
|
|
415
|
-
lines.push(`- Match: ${r.matchPercent}%`);
|
|
416
|
-
if (r.verovioLog) {
|
|
417
|
-
lines.push(`- Verovio log: ${r.verovioLog.substring(0, 200).replace(/\n/g, " ")}`);
|
|
418
|
-
}
|
|
419
|
-
lines.push("");
|
|
420
|
-
// Categorize diffs
|
|
421
|
-
const diffCats = {};
|
|
422
|
-
for (const d of r.diffs) {
|
|
423
|
-
const cat = d.field;
|
|
424
|
-
if (!diffCats[cat])
|
|
425
|
-
diffCats[cat] = [];
|
|
426
|
-
diffCats[cat].push(d);
|
|
427
|
-
}
|
|
428
|
-
// Show first 20 diffs per category
|
|
429
|
-
for (const [cat, diffs] of Object.entries(diffCats)) {
|
|
430
|
-
lines.push(`**${cat}** (${diffs.length} differences):`);
|
|
431
|
-
const shown = diffs.slice(0, 20);
|
|
432
|
-
for (const d of shown) {
|
|
433
|
-
const loc = d.index >= 0 ? `m${d.measure}[${d.index}]` : `m${d.measure}`;
|
|
434
|
-
lines.push(`- ${loc}: V=\`${d.verovio}\` L=\`${d.lilylet}\``);
|
|
435
|
-
}
|
|
436
|
-
if (diffs.length > 20)
|
|
437
|
-
lines.push(`- ... and ${diffs.length - 20} more`);
|
|
438
|
-
lines.push("");
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// Analysis
|
|
442
|
-
lines.push("## Analysis\n");
|
|
443
|
-
const totalDiffs = results.reduce((s, r) => s + r.diffs.length, 0);
|
|
444
|
-
const pitchDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "pitch").length, 0);
|
|
445
|
-
const durDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "dur").length, 0);
|
|
446
|
-
const dotDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "dots").length, 0);
|
|
447
|
-
const accidDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "accid").length, 0);
|
|
448
|
-
const countDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "noteCount").length, 0);
|
|
449
|
-
const restNoteDiffs = results.reduce((s, r) => s + r.diffs.filter(d => d.field === "rest/note").length, 0);
|
|
450
|
-
lines.push(`Total differences: ${totalDiffs}`);
|
|
451
|
-
lines.push(`- Pitch: ${pitchDiffs}`);
|
|
452
|
-
lines.push(`- Duration: ${durDiffs}`);
|
|
453
|
-
lines.push(`- Dots: ${dotDiffs}`);
|
|
454
|
-
lines.push(`- Accidentals: ${accidDiffs}`);
|
|
455
|
-
lines.push(`- Note counts: ${countDiffs}`);
|
|
456
|
-
lines.push(`- Rest/note type: ${restNoteDiffs}`);
|
|
457
|
-
lines.push("");
|
|
458
|
-
lines.push("**Note**: Verovio's ABC import does NOT support multi-voice music. It only parses voice 1 content,");
|
|
459
|
-
lines.push("so differences may arise from Verovio merging/dropping multi-voice data. Comparison is limited to staff 1, layer 1.");
|
|
460
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, "report.md"), lines.join("\n"));
|
|
461
|
-
};
|
|
462
|
-
// ─── Comparison HTML Gallery ─────────────────────────────────────────
|
|
463
|
-
const generateComparisonHtml = (results) => {
|
|
464
|
-
const cards = results.map(r => {
|
|
465
|
-
const status = r.error ? "error" : r.matchPercent >= 90 ? "ok" : "diff";
|
|
466
|
-
const statusLabel = r.error ? "Error" : r.matchPercent >= 90 ? `${r.matchPercent}% Match` : `${r.matchPercent}% Match`;
|
|
467
|
-
return ` <div class="card ${status}">
|
|
468
|
-
<div class="card-header">
|
|
469
|
-
<span class="name">${r.name}</span>
|
|
470
|
-
<span class="status-badge ${status}">${statusLabel}</span>
|
|
471
|
-
</div>
|
|
472
|
-
<div class="comparison">
|
|
473
|
-
<div class="side">
|
|
474
|
-
<div class="side-label">Verovio (direct ABC→MEI)</div>
|
|
475
|
-
<img src="${r.name}.verovio.svg" alt="Verovio" onerror="this.parentElement.innerHTML='<p>No SVG</p>'">
|
|
476
|
-
</div>
|
|
477
|
-
<div class="side">
|
|
478
|
-
<div class="side-label">Lilylet (ABC→lilylet→MEI)</div>
|
|
479
|
-
<img src="${r.name}.lilylet.svg" alt="Lilylet" onerror="this.parentElement.innerHTML='<p>No SVG</p>'">
|
|
480
|
-
</div>
|
|
481
|
-
</div>
|
|
482
|
-
<div class="card-footer">
|
|
483
|
-
<span>Measures: V:${r.verovioMeasures.length} L:${r.lilyletMeasures.length} | Notes: V:${r.totalNotes.verovio} L:${r.totalNotes.lilylet}</span>
|
|
484
|
-
<span><a href="${r.name}.verovio.mei">V-MEI</a> | <a href="${r.name}.lilylet.mei">L-MEI</a></span>
|
|
485
|
-
</div>
|
|
486
|
-
</div>`;
|
|
487
|
-
}).join("\n");
|
|
488
|
-
const html = `<!DOCTYPE html>
|
|
489
|
-
<html lang="en">
|
|
490
|
-
<head>
|
|
491
|
-
<meta charset="UTF-8">
|
|
492
|
-
<title>ABC→MEI Comparison</title>
|
|
493
|
-
<style>
|
|
494
|
-
* { box-sizing: border-box; }
|
|
495
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
496
|
-
h1 { text-align: center; color: #333; }
|
|
497
|
-
.gallery { display: flex; flex-direction: column; gap: 24px; max-width: 1400px; margin: 0 auto; }
|
|
498
|
-
.card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }
|
|
499
|
-
.card.error { border-left: 4px solid #e74c3c; }
|
|
500
|
-
.card.diff { border-left: 4px solid #f39c12; }
|
|
501
|
-
.card.ok { border-left: 4px solid #2ecc71; }
|
|
502
|
-
.card-header { padding: 12px 16px; background: #f8f9fa; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
503
|
-
.card-header .name { font-weight: 600; font-size: 15px; }
|
|
504
|
-
.status-badge { padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
|
|
505
|
-
.status-badge.ok { background: #d4edda; color: #155724; }
|
|
506
|
-
.status-badge.diff { background: #fff3cd; color: #856404; }
|
|
507
|
-
.status-badge.error { background: #f8d7da; color: #721c24; }
|
|
508
|
-
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #eee; }
|
|
509
|
-
.side { background: white; padding: 12px; text-align: center; }
|
|
510
|
-
.side-label { font-size: 12px; color: #888; margin-bottom: 8px; font-weight: 500; }
|
|
511
|
-
.side img { max-width: 100%; height: auto; }
|
|
512
|
-
.card-footer { padding: 8px 16px; background: #f8f9fa; border-top: 1px solid #eee; font-size: 12px; color: #666; display: flex; justify-content: space-between; }
|
|
513
|
-
.card-footer a { color: #4a90d9; text-decoration: none; }
|
|
514
|
-
</style>
|
|
515
|
-
</head>
|
|
516
|
-
<body>
|
|
517
|
-
<h1>ABC → MEI Comparison: Verovio vs Lilylet</h1>
|
|
518
|
-
<div class="gallery">
|
|
519
|
-
${cards}
|
|
520
|
-
</div>
|
|
521
|
-
</body>
|
|
522
|
-
</html>`;
|
|
523
|
-
fs.writeFileSync(path.join(OUTPUT_DIR, "index.html"), html);
|
|
524
|
-
};
|
|
525
|
-
main();
|
package/lib/tests/auto-beam.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test: Auto-beam on/off/auto modes
|
|
3
|
-
*
|
|
4
|
-
* Verifies that the autoBeam metadata option controls beam generation:
|
|
5
|
-
* - 'off': never auto-beam, even without manual beams
|
|
6
|
-
* - 'on': always auto-beam, even when manual beams exist
|
|
7
|
-
* - 'auto' / undefined: auto-beam only if no manual beam marks in source
|
|
8
|
-
*/
|
|
9
|
-
export {};
|