@k-l-lambda/lilylet 0.1.70 → 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/gmInstruments.d.ts +1 -0
- package/lib/gmInstruments.js +1 -0
- package/lib/highlight.d.ts +1 -0
- package/lib/highlight.js +1 -0
- package/lib/lilylet/abcDecoder.js +16 -7
- package/lib/lilylet/gmInstruments.d.ts +1 -0
- package/lib/lilylet/gmInstruments.js +295 -0
- package/lib/lilylet/highlight.d.ts +29 -0
- package/lib/lilylet/highlight.js +145 -0
- package/lib/lilylet/meiEncoder.js +126 -14
- package/lib/lilylet/staffLayout.d.ts +5 -0
- package/lib/lilylet/staffLayout.js +62 -0
- package/package.json +8 -2
- package/source/lilylet/abcDecoder.ts +14 -7
- package/source/lilylet/gmInstruments.ts +305 -0
- package/source/lilylet/highlight.ts +192 -0
- package/source/lilylet/meiEncoder.ts +135 -11
- package/source/lilylet/staffLayout.ts +76 -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
package/lib/tests/mei.js
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
// @ts-ignore
|
|
3
|
-
import createVerovioModule from "verovio/wasm";
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import { VerovioToolkit } from "verovio/esm";
|
|
6
|
-
import * as lilylet from "../source/lilylet.js";
|
|
7
|
-
const initVerovio = async () => {
|
|
8
|
-
const VerovioModule = await createVerovioModule();
|
|
9
|
-
return new VerovioToolkit(VerovioModule);
|
|
10
|
-
};
|
|
11
|
-
// Count notes/rests in the source AST, recursing through tuplet/times wrappers.
|
|
12
|
-
const countSourceNotes = (doc) => {
|
|
13
|
-
let count = 0;
|
|
14
|
-
const walk = (events) => {
|
|
15
|
-
for (const e of events) {
|
|
16
|
-
if (!e)
|
|
17
|
-
continue;
|
|
18
|
-
if (e.type === "note" || e.type === "rest")
|
|
19
|
-
count++;
|
|
20
|
-
else if (e.type === "tuplet" || e.type === "times")
|
|
21
|
-
walk(e.events || []);
|
|
22
|
-
else if (e.type === "tremolo")
|
|
23
|
-
count += 2;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
for (const m of doc.measures || []) {
|
|
27
|
-
for (const part of m.parts || []) {
|
|
28
|
-
for (const voice of part.voices || [])
|
|
29
|
-
walk(voice.events || []);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return count;
|
|
33
|
-
};
|
|
34
|
-
// Count <note>/<rest>/<chord>/<mRest>/<mSpace> elements in MEI output (rough but
|
|
35
|
-
// sufficient for detecting silently-dropped event types that produce empty sections).
|
|
36
|
-
const countMeiNotes = (mei) => {
|
|
37
|
-
const matches = mei.match(/<(note|rest|chord|mRest|mSpace)\b/g);
|
|
38
|
-
return matches ? matches.length : 0;
|
|
39
|
-
};
|
|
40
|
-
const testFile = async (vrvToolkit, filePath) => {
|
|
41
|
-
const file = filePath.split("/").pop();
|
|
42
|
-
try {
|
|
43
|
-
// Step 1: Parse .lyl file
|
|
44
|
-
const code = fs.readFileSync(filePath, { encoding: "utf-8" });
|
|
45
|
-
const doc = lilylet.parseCode(code);
|
|
46
|
-
// Step 2: Encode to MEI
|
|
47
|
-
const mei = lilylet.meiEncoder.encode(doc);
|
|
48
|
-
// Step 2.5: Sanity check — count source notes/rests vs MEI output.
|
|
49
|
-
// Catches silent drops where an event type is unhandled and MEI ends up empty.
|
|
50
|
-
const expected = countSourceNotes(doc);
|
|
51
|
-
const actual = countMeiNotes(mei);
|
|
52
|
-
if (expected > 0 && actual === 0) {
|
|
53
|
-
return {
|
|
54
|
-
file,
|
|
55
|
-
success: false,
|
|
56
|
-
error: `MEI output has 0 notes/rests but source has ${expected} (likely an event type is silently dropped)`,
|
|
57
|
-
mei,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// Step 3: Calculate pageHeight based on measure count and staff count
|
|
61
|
-
const measureCount = doc.measures?.length || 1;
|
|
62
|
-
// Calculate total staff count
|
|
63
|
-
let staffCount = 1;
|
|
64
|
-
if (doc.measures.length > 0) {
|
|
65
|
-
const firstMeasure = doc.measures[0];
|
|
66
|
-
staffCount = firstMeasure.parts.reduce((total, part) => {
|
|
67
|
-
const maxStaff = part.voices.reduce((max, voice) => Math.max(max, voice.staff || 1), 1);
|
|
68
|
-
return total + maxStaff;
|
|
69
|
-
}, 0) || 1;
|
|
70
|
-
}
|
|
71
|
-
const basePageHeight = 2000;
|
|
72
|
-
const measuresPerPage = 20;
|
|
73
|
-
const pageHeight = Math.max(basePageHeight, Math.ceil(measureCount / measuresPerPage) * basePageHeight) * 2 * staffCount;
|
|
74
|
-
// Step 4: Set Verovio options for single-page rendering
|
|
75
|
-
vrvToolkit.setOptions({
|
|
76
|
-
scale: 40,
|
|
77
|
-
adjustPageHeight: true,
|
|
78
|
-
pageHeight,
|
|
79
|
-
pageWidth: 2100,
|
|
80
|
-
});
|
|
81
|
-
// Step 5: Validate MEI with verovio
|
|
82
|
-
const success = vrvToolkit.loadData(mei);
|
|
83
|
-
if (!success) {
|
|
84
|
-
return {
|
|
85
|
-
file,
|
|
86
|
-
success: false,
|
|
87
|
-
error: "Verovio failed to load MEI data",
|
|
88
|
-
mei,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
// Step 6: Try to render SVG (further validation)
|
|
92
|
-
const svg = vrvToolkit.renderToSVG(1);
|
|
93
|
-
if (!svg || svg.length === 0) {
|
|
94
|
-
return {
|
|
95
|
-
file,
|
|
96
|
-
success: false,
|
|
97
|
-
error: "Verovio failed to render SVG",
|
|
98
|
-
mei,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
file,
|
|
103
|
-
success: true,
|
|
104
|
-
mei,
|
|
105
|
-
svg,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
return {
|
|
110
|
-
file,
|
|
111
|
-
success: false,
|
|
112
|
-
error: err instanceof Error ? err.message : String(err),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
// Generate index.html gallery
|
|
117
|
-
const generateIndexHtml = (files, outputDir, lylDir) => {
|
|
118
|
-
// Group files by category (prefix before first dash or hyphen pattern)
|
|
119
|
-
const getCategory = (file) => {
|
|
120
|
-
const name = file.replace('.lyl', '');
|
|
121
|
-
const parts = name.split('-');
|
|
122
|
-
// Find category: usually first 1-2 parts
|
|
123
|
-
if (parts.length >= 2) {
|
|
124
|
-
// Check common patterns
|
|
125
|
-
if (parts[0] === 'time' && parts[1] === 'signatures')
|
|
126
|
-
return 'time-signatures';
|
|
127
|
-
if (parts[0] === 'key' && parts[1] === 'signatures')
|
|
128
|
-
return 'key-signatures';
|
|
129
|
-
if (parts[0] === 'basic' && parts[1] === 'notes')
|
|
130
|
-
return 'basic-notes';
|
|
131
|
-
if (parts[0] === 'ties' && parts[1] === 'and')
|
|
132
|
-
return 'ties-and-slurs';
|
|
133
|
-
if (parts[0] === 'grace' && parts[1] === 'notes')
|
|
134
|
-
return 'grace-notes';
|
|
135
|
-
if (parts[0] === 'stem' && parts[1] === 'direction')
|
|
136
|
-
return 'stem-direction';
|
|
137
|
-
if (parts[0] === 'multiple' && parts[1] === 'staves')
|
|
138
|
-
return 'multiple-staves';
|
|
139
|
-
if (parts[0] === 'multiple' && parts[1] === 'voices')
|
|
140
|
-
return 'multiple-voices';
|
|
141
|
-
if (parts[0] === 'multiple' && parts[1] === 'measures')
|
|
142
|
-
return 'multiple-measures';
|
|
143
|
-
if (parts[0] === 'multiple' && parts[1] === 'parts')
|
|
144
|
-
return 'multiple-parts';
|
|
145
|
-
return parts[0];
|
|
146
|
-
}
|
|
147
|
-
return parts[0];
|
|
148
|
-
};
|
|
149
|
-
// Escape HTML entities
|
|
150
|
-
const escHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
151
|
-
const categories = [...new Set(files.map(getCategory))].sort();
|
|
152
|
-
const cards = files.map(file => {
|
|
153
|
-
const name = file.replace('.lyl', '');
|
|
154
|
-
const category = getCategory(file);
|
|
155
|
-
const source = fs.readFileSync(`${lylDir}/${file}`, 'utf-8').trim();
|
|
156
|
-
return ` <div class="card" data-category="${category}">
|
|
157
|
-
<div class="card-header">${name} <span class="category">${category}</span></div>
|
|
158
|
-
<div class="card-source"><code>${escHtml(source)}</code></div>
|
|
159
|
-
<div class="card-body"><img src="${name}.svg" alt="${name}"></div>
|
|
160
|
-
<div class="card-footer"><a href="${name}.mei">MEI</a><a href="${name}.svg" target="_blank">SVG</a></div>
|
|
161
|
-
</div>`;
|
|
162
|
-
}).join('\n');
|
|
163
|
-
const filterButtons = categories.map(cat => `<a href="#${cat}" class="filter-btn" data-filter="${cat}">${cat.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}</a>`).join('\n ');
|
|
164
|
-
const html = `<!DOCTYPE html>
|
|
165
|
-
<html lang="en">
|
|
166
|
-
<head>
|
|
167
|
-
<meta charset="UTF-8">
|
|
168
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
169
|
-
<title>Lilylet MEI Test Results</title>
|
|
170
|
-
<style>
|
|
171
|
-
* { box-sizing: border-box; }
|
|
172
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
173
|
-
h1 { text-align: center; color: #333; margin-bottom: 10px; }
|
|
174
|
-
.stats { text-align: center; color: #666; margin-bottom: 30px; }
|
|
175
|
-
.filter-bar { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
176
|
-
.filter-btn { padding: 8px 16px; border: 1px solid #ddd; background: white; border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.2s; text-decoration: none; color: #333; }
|
|
177
|
-
.filter-btn:hover { background: #e0e0e0; }
|
|
178
|
-
.filter-btn.active { background: #4a90d9; color: white; border-color: #4a90d9; }
|
|
179
|
-
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); gap: 20px; }
|
|
180
|
-
.card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }
|
|
181
|
-
.card.hidden { display: none; }
|
|
182
|
-
.card-header { padding: 12px 16px; background: #f8f9fa; border-bottom: 1px solid #eee; font-weight: 500; font-size: 14px; }
|
|
183
|
-
.card-header .category { float: right; font-size: 12px; color: #888; font-weight: normal; }
|
|
184
|
-
.card-source { padding: 10px 16px; background: #1e1e1e; border-bottom: 1px solid #eee; }
|
|
185
|
-
.card-source code { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace; font-size: 13px; color: #d4d4d4; white-space: pre-wrap; word-break: break-all; }
|
|
186
|
-
.card-body { padding: 16px; text-align: center; min-height: 100px; display: flex; align-items: center; justify-content: center; }
|
|
187
|
-
.card-body img { max-width: 100%; height: auto; }
|
|
188
|
-
.card-footer { padding: 12px 16px; background: #f8f9fa; border-top: 1px solid #eee; display: flex; gap: 10px; }
|
|
189
|
-
.card-footer a { color: #4a90d9; text-decoration: none; font-size: 14px; }
|
|
190
|
-
.card-footer a:hover { text-decoration: underline; }
|
|
191
|
-
</style>
|
|
192
|
-
</head>
|
|
193
|
-
<body>
|
|
194
|
-
<h1>Lilylet MEI Test Results</h1>
|
|
195
|
-
<div class="stats">${files.length} test cases</div>
|
|
196
|
-
<div class="filter-bar">
|
|
197
|
-
<a href="#" class="filter-btn active" data-filter="all">All</a>
|
|
198
|
-
${filterButtons}
|
|
199
|
-
</div>
|
|
200
|
-
<div class="gallery">
|
|
201
|
-
${cards}
|
|
202
|
-
</div>
|
|
203
|
-
<script>
|
|
204
|
-
function applyFilter(filter) {
|
|
205
|
-
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
206
|
-
const activeBtn = document.querySelector('.filter-btn[data-filter="' + filter + '"]');
|
|
207
|
-
if (activeBtn) activeBtn.classList.add('active');
|
|
208
|
-
document.querySelectorAll('.card').forEach(card => {
|
|
209
|
-
if (filter === 'all' || card.dataset.category === filter) {
|
|
210
|
-
card.classList.remove('hidden');
|
|
211
|
-
} else {
|
|
212
|
-
card.classList.add('hidden');
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
function handleHash() {
|
|
217
|
-
const hash = window.location.hash.slice(1);
|
|
218
|
-
applyFilter(hash || 'all');
|
|
219
|
-
}
|
|
220
|
-
window.addEventListener('hashchange', handleHash);
|
|
221
|
-
handleHash();
|
|
222
|
-
</script>
|
|
223
|
-
</body>
|
|
224
|
-
</html>`;
|
|
225
|
-
fs.writeFileSync(`${outputDir}/index.html`, html);
|
|
226
|
-
};
|
|
227
|
-
const main = async () => {
|
|
228
|
-
const lylDir = "./tests/assets/unit-cases";
|
|
229
|
-
const outputDir = "./tests/output/unit-cases";
|
|
230
|
-
// Create output directory if not exists
|
|
231
|
-
if (!fs.existsSync(outputDir)) {
|
|
232
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
233
|
-
}
|
|
234
|
-
// Initialize verovio
|
|
235
|
-
console.log("Initializing Verovio...");
|
|
236
|
-
const vrvToolkit = await initVerovio();
|
|
237
|
-
console.log("Verovio initialized.\n");
|
|
238
|
-
// Get all .lyl files
|
|
239
|
-
const files = fs.readdirSync(lylDir).filter(f => f.endsWith(".lyl"));
|
|
240
|
-
let passed = 0;
|
|
241
|
-
let failed = 0;
|
|
242
|
-
const failures = [];
|
|
243
|
-
for (const file of files) {
|
|
244
|
-
const result = await testFile(vrvToolkit, `${lylDir}/${file}`);
|
|
245
|
-
if (result.success) {
|
|
246
|
-
console.log(`✓ ${file}`);
|
|
247
|
-
passed++;
|
|
248
|
-
// Optionally save MEI and SVG output
|
|
249
|
-
if (result.mei) {
|
|
250
|
-
fs.writeFileSync(`${outputDir}/${file.replace(".lyl", ".mei")}`, result.mei);
|
|
251
|
-
}
|
|
252
|
-
if (result.svg) {
|
|
253
|
-
fs.writeFileSync(`${outputDir}/${file.replace(".lyl", ".svg")}`, result.svg);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
console.log(`✗ ${file}: ${result.error}`);
|
|
258
|
-
failed++;
|
|
259
|
-
failures.push(result);
|
|
260
|
-
// Save failed MEI for debugging
|
|
261
|
-
if (result.mei) {
|
|
262
|
-
fs.writeFileSync(`${outputDir}/${file.replace(".lyl", ".mei.failed")}`, result.mei);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
console.log(`\n========================================`);
|
|
267
|
-
console.log(`Total: ${files.length}, Passed: ${passed}, Failed: ${failed}`);
|
|
268
|
-
if (failures.length > 0) {
|
|
269
|
-
console.log(`\nFailed tests:`);
|
|
270
|
-
for (const f of failures) {
|
|
271
|
-
console.log(` - ${f.file}: ${f.error}`);
|
|
272
|
-
}
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
// Generate index.html gallery
|
|
276
|
-
generateIndexHtml(files, outputDir, lylDir);
|
|
277
|
-
};
|
|
278
|
-
main();
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test MusicXML Decoder
|
|
3
|
-
*/
|
|
4
|
-
import { musicXmlDecoder, meiEncoder } from '../source/lilylet/index.js';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
const simpleFile = path.join(import.meta.dirname, 'assets/unit-cases/simple.musicxml');
|
|
8
|
-
const complexFile = path.join(import.meta.dirname, 'assets/unit-cases/complex.musicxml');
|
|
9
|
-
async function testFile(filePath, name) {
|
|
10
|
-
console.log(`\n${'='.repeat(60)}`);
|
|
11
|
-
console.log(`Testing: ${name}`);
|
|
12
|
-
console.log('='.repeat(60));
|
|
13
|
-
const xml = fs.readFileSync(filePath, 'utf-8');
|
|
14
|
-
const doc = musicXmlDecoder.decode(xml);
|
|
15
|
-
console.log('\n=== Metadata ===');
|
|
16
|
-
console.log(doc.metadata);
|
|
17
|
-
console.log('\n=== Number of measures ===');
|
|
18
|
-
console.log(doc.measures.length);
|
|
19
|
-
console.log('\n=== First measure ===');
|
|
20
|
-
const firstMeasure = doc.measures[0];
|
|
21
|
-
console.log('Key:', firstMeasure.key);
|
|
22
|
-
console.log('Time:', firstMeasure.timeSig);
|
|
23
|
-
const firstVoice = firstMeasure.parts[0]?.voices[0];
|
|
24
|
-
if (firstVoice) {
|
|
25
|
-
console.log('Events:', firstVoice.events.length);
|
|
26
|
-
for (const event of firstVoice.events) {
|
|
27
|
-
if (event.type === 'note') {
|
|
28
|
-
const pitches = event.pitches.map(p => `${p.phonet}${p.accidental ? '#' : ''}${p.octave}`).join(',');
|
|
29
|
-
const marks = event.marks?.map(m => m.markType).join(', ') || 'none';
|
|
30
|
-
console.log(` Note: ${pitches}, dur=${event.duration.division}, marks=[${marks}]`);
|
|
31
|
-
}
|
|
32
|
-
else if (event.type === 'rest') {
|
|
33
|
-
console.log(` Rest: dur=${event.duration.division}`);
|
|
34
|
-
}
|
|
35
|
-
else if (event.type === 'context') {
|
|
36
|
-
console.log(` Context: clef=${event.clef}`);
|
|
37
|
-
}
|
|
38
|
-
else if (event.type === 'barline') {
|
|
39
|
-
console.log(` Barline: ${event.style}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Encode to MEI
|
|
44
|
-
const mei = meiEncoder.encode(doc);
|
|
45
|
-
console.log('\n=== MEI Output (truncated) ===');
|
|
46
|
-
console.log(mei.split('\n').slice(0, 30).join('\n') + '\n...');
|
|
47
|
-
return doc;
|
|
48
|
-
}
|
|
49
|
-
async function main() {
|
|
50
|
-
console.log('Testing MusicXML Decoder...');
|
|
51
|
-
try {
|
|
52
|
-
await testFile(simpleFile, 'Simple Test');
|
|
53
|
-
await testFile(complexFile, 'Complex Test (slurs, dynamics, articulations)');
|
|
54
|
-
console.log('\n✅ All MusicXML Decoder tests passed!');
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
console.error('❌ Test failed:', error);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
main();
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detailed examination of MusicXML decoder output
|
|
3
|
-
*/
|
|
4
|
-
import { musicXmlDecoder, meiEncoder } from '../source/lilylet/index.js';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
const MUSICXML_DIR = path.join(import.meta.dirname, 'assets/musicxml');
|
|
8
|
-
// Test file argument
|
|
9
|
-
const testFile = process.argv[2] || 'complex-02-chopin-etude.xml';
|
|
10
|
-
const filepath = path.join(MUSICXML_DIR, testFile);
|
|
11
|
-
console.log(`Examining: ${testFile}\n`);
|
|
12
|
-
const xml = fs.readFileSync(filepath, 'utf-8');
|
|
13
|
-
const doc = musicXmlDecoder.decode(xml);
|
|
14
|
-
console.log('=== Document Structure ===');
|
|
15
|
-
console.log('Metadata:', doc.metadata);
|
|
16
|
-
console.log('Total measures:', doc.measures.length);
|
|
17
|
-
// First measure
|
|
18
|
-
const m1 = doc.measures[0];
|
|
19
|
-
console.log('\n=== First Measure ===');
|
|
20
|
-
console.log('Key:', m1.key);
|
|
21
|
-
console.log('Time:', m1.timeSig);
|
|
22
|
-
console.log('Parts:', m1.parts.length);
|
|
23
|
-
console.log('Voices:', m1.parts[0].voices.length);
|
|
24
|
-
const v1 = m1.parts[0].voices[0];
|
|
25
|
-
console.log('\nFirst voice events (first 10):');
|
|
26
|
-
for (let i = 0; i < Math.min(10, v1.events.length); i++) {
|
|
27
|
-
const e = v1.events[i];
|
|
28
|
-
if (e.type === 'note') {
|
|
29
|
-
const pitches = e.pitches.map(p => `${p.phonet}${p.accidental || ''}${p.octave}`).join('+');
|
|
30
|
-
const marks = e.marks?.map(m => m.markType).join(',') || '-';
|
|
31
|
-
console.log(` [${i}] Note: ${pitches.padEnd(15)} dur=${e.duration.division.toString().padStart(2)} marks=${marks}`);
|
|
32
|
-
}
|
|
33
|
-
else if (e.type === 'rest') {
|
|
34
|
-
console.log(` [${i}] Rest: dur=${e.duration.division}`);
|
|
35
|
-
}
|
|
36
|
-
else if (e.type === 'context') {
|
|
37
|
-
const ctx = [];
|
|
38
|
-
if (e.clef)
|
|
39
|
-
ctx.push(`clef=${e.clef}`);
|
|
40
|
-
if (e.stemDirection)
|
|
41
|
-
ctx.push(`stem=${e.stemDirection}`);
|
|
42
|
-
if (e.ottava !== undefined)
|
|
43
|
-
ctx.push(`ottava=${e.ottava}`);
|
|
44
|
-
console.log(` [${i}] Context: ${ctx.join(', ')}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Count statistics
|
|
48
|
-
let totalNotes = 0;
|
|
49
|
-
let totalRests = 0;
|
|
50
|
-
let totalChords = 0;
|
|
51
|
-
const markCounts = {};
|
|
52
|
-
for (const measure of doc.measures) {
|
|
53
|
-
for (const part of measure.parts) {
|
|
54
|
-
for (const voice of part.voices) {
|
|
55
|
-
for (const event of voice.events) {
|
|
56
|
-
if (event.type === 'note') {
|
|
57
|
-
totalNotes++;
|
|
58
|
-
if (event.pitches.length > 1)
|
|
59
|
-
totalChords++;
|
|
60
|
-
if (event.marks) {
|
|
61
|
-
for (const mark of event.marks) {
|
|
62
|
-
markCounts[mark.markType] = (markCounts[mark.markType] || 0) + 1;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (event.type === 'rest') {
|
|
67
|
-
totalRests++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
console.log('\n=== Statistics ===');
|
|
74
|
-
console.log(`Notes: ${totalNotes}`);
|
|
75
|
-
console.log(`Chords: ${totalChords}`);
|
|
76
|
-
console.log(`Rests: ${totalRests}`);
|
|
77
|
-
console.log('\nMark counts:');
|
|
78
|
-
for (const [mark, count] of Object.entries(markCounts).sort((a, b) => b[1] - a[1])) {
|
|
79
|
-
console.log(` ${mark}: ${count}`);
|
|
80
|
-
}
|
|
81
|
-
// Encode to MEI and show snippet
|
|
82
|
-
console.log('\n=== MEI Output (first 40 lines) ===');
|
|
83
|
-
const mei = meiEncoder.encode(doc);
|
|
84
|
-
console.log(mei.split('\n').slice(0, 40).join('\n'));
|
|
85
|
-
console.log('...');
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MusicXML Decoder Unit Tests - Real-world fprod files
|
|
3
|
-
*
|
|
4
|
-
* Tests the decoder against 10 files from fprod:
|
|
5
|
-
* - 5 simple (Thompson beginner pieces)
|
|
6
|
-
* - 3 medium (Thompson intermediate)
|
|
7
|
-
* - 2 complex (Bach Invention, Chopin Etude)
|
|
8
|
-
*/
|
|
9
|
-
import { musicXmlDecoder, meiEncoder } from '../source/lilylet/index.js';
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
const MUSICXML_DIR = path.join(import.meta.dirname, 'assets/musicxml');
|
|
13
|
-
function countNotes(doc) {
|
|
14
|
-
let count = 0;
|
|
15
|
-
for (const measure of doc.measures) {
|
|
16
|
-
for (const part of measure.parts) {
|
|
17
|
-
for (const voice of part.voices) {
|
|
18
|
-
for (const event of voice.events) {
|
|
19
|
-
if (event.type === 'note') {
|
|
20
|
-
count += event.pitches.length;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return count;
|
|
27
|
-
}
|
|
28
|
-
function getCategory(filename) {
|
|
29
|
-
if (filename.startsWith('simple-'))
|
|
30
|
-
return 'simple';
|
|
31
|
-
if (filename.startsWith('medium-'))
|
|
32
|
-
return 'medium';
|
|
33
|
-
return 'complex';
|
|
34
|
-
}
|
|
35
|
-
async function testFile(filename) {
|
|
36
|
-
const filepath = path.join(MUSICXML_DIR, filename);
|
|
37
|
-
const category = getCategory(filename);
|
|
38
|
-
const warnings = [];
|
|
39
|
-
// Capture console warnings
|
|
40
|
-
const originalWarn = console.warn;
|
|
41
|
-
console.warn = (...args) => {
|
|
42
|
-
warnings.push(args.join(' '));
|
|
43
|
-
};
|
|
44
|
-
try {
|
|
45
|
-
const xml = fs.readFileSync(filepath, 'utf-8');
|
|
46
|
-
const doc = musicXmlDecoder.decode(xml);
|
|
47
|
-
// Validate basic structure
|
|
48
|
-
if (!doc.measures || doc.measures.length === 0) {
|
|
49
|
-
throw new Error('No measures found in decoded document');
|
|
50
|
-
}
|
|
51
|
-
const notes = countNotes(doc);
|
|
52
|
-
if (notes === 0) {
|
|
53
|
-
throw new Error('No notes found in decoded document');
|
|
54
|
-
}
|
|
55
|
-
// Try encoding to MEI
|
|
56
|
-
const mei = meiEncoder.encode(doc);
|
|
57
|
-
if (!mei.includes('<mei')) {
|
|
58
|
-
throw new Error('MEI encoding failed - no mei tag found');
|
|
59
|
-
}
|
|
60
|
-
console.warn = originalWarn;
|
|
61
|
-
return {
|
|
62
|
-
name: filename,
|
|
63
|
-
category,
|
|
64
|
-
success: true,
|
|
65
|
-
measures: doc.measures.length,
|
|
66
|
-
notes,
|
|
67
|
-
warnings,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
console.warn = originalWarn;
|
|
72
|
-
return {
|
|
73
|
-
name: filename,
|
|
74
|
-
category,
|
|
75
|
-
success: false,
|
|
76
|
-
measures: 0,
|
|
77
|
-
notes: 0,
|
|
78
|
-
error: error.message,
|
|
79
|
-
warnings,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async function main() {
|
|
84
|
-
console.log('MusicXML Decoder Unit Tests - fprod files\n');
|
|
85
|
-
console.log('='.repeat(80));
|
|
86
|
-
const files = fs.readdirSync(MUSICXML_DIR).filter(f => f.endsWith('.xml')).sort();
|
|
87
|
-
const results = [];
|
|
88
|
-
let passed = 0;
|
|
89
|
-
let failed = 0;
|
|
90
|
-
for (const file of files) {
|
|
91
|
-
const result = await testFile(file);
|
|
92
|
-
results.push(result);
|
|
93
|
-
const status = result.success ? '✅' : '❌';
|
|
94
|
-
const stats = result.success
|
|
95
|
-
? `measures=${result.measures}, notes=${result.notes}`
|
|
96
|
-
: `error: ${result.error}`;
|
|
97
|
-
console.log(`${status} [${result.category.padEnd(7)}] ${result.name}`);
|
|
98
|
-
console.log(` ${stats}`);
|
|
99
|
-
if (result.warnings.length > 0) {
|
|
100
|
-
console.log(` ⚠️ ${result.warnings.length} warnings`);
|
|
101
|
-
}
|
|
102
|
-
if (result.success) {
|
|
103
|
-
passed++;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
failed++;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
console.log('\n' + '='.repeat(80));
|
|
110
|
-
console.log('Summary:');
|
|
111
|
-
console.log(` Total: ${files.length}`);
|
|
112
|
-
console.log(` Passed: ${passed} ✅`);
|
|
113
|
-
console.log(` Failed: ${failed} ❌`);
|
|
114
|
-
// Group by category
|
|
115
|
-
const byCategory = {
|
|
116
|
-
simple: results.filter(r => r.category === 'simple'),
|
|
117
|
-
medium: results.filter(r => r.category === 'medium'),
|
|
118
|
-
complex: results.filter(r => r.category === 'complex'),
|
|
119
|
-
};
|
|
120
|
-
console.log('\nBy Category:');
|
|
121
|
-
for (const [cat, catResults] of Object.entries(byCategory)) {
|
|
122
|
-
const catPassed = catResults.filter(r => r.success).length;
|
|
123
|
-
console.log(` ${cat}: ${catPassed}/${catResults.length}`);
|
|
124
|
-
}
|
|
125
|
-
// Show failed tests
|
|
126
|
-
const failedTests = results.filter(r => !r.success);
|
|
127
|
-
if (failedTests.length > 0) {
|
|
128
|
-
console.log('\nFailed Tests:');
|
|
129
|
-
for (const test of failedTests) {
|
|
130
|
-
console.log(` ❌ ${test.name}: ${test.error}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Statistics for successful tests
|
|
134
|
-
const successfulTests = results.filter(r => r.success);
|
|
135
|
-
if (successfulTests.length > 0) {
|
|
136
|
-
const totalMeasures = successfulTests.reduce((sum, r) => sum + r.measures, 0);
|
|
137
|
-
const totalNotes = successfulTests.reduce((sum, r) => sum + r.notes, 0);
|
|
138
|
-
console.log('\nStatistics (successful tests):');
|
|
139
|
-
console.log(` Total measures: ${totalMeasures}`);
|
|
140
|
-
console.log(` Total notes: ${totalNotes}`);
|
|
141
|
-
console.log(` Avg measures/file: ${(totalMeasures / successfulTests.length).toFixed(1)}`);
|
|
142
|
-
console.log(` Avg notes/file: ${(totalNotes / successfulTests.length).toFixed(1)}`);
|
|
143
|
-
}
|
|
144
|
-
console.log('\n' + '='.repeat(80));
|
|
145
|
-
if (failed > 0) {
|
|
146
|
-
console.log(`\n❌ ${failed} test(s) failed`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
console.log('\n✅ All tests passed!');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
main();
|