@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.
Files changed (116) hide show
  1. package/lib/gmInstruments.d.ts +1 -0
  2. package/lib/gmInstruments.js +1 -0
  3. package/lib/highlight.d.ts +1 -0
  4. package/lib/highlight.js +1 -0
  5. package/lib/lilylet/abcDecoder.js +16 -7
  6. package/lib/lilylet/gmInstruments.d.ts +1 -0
  7. package/lib/lilylet/gmInstruments.js +295 -0
  8. package/lib/lilylet/highlight.d.ts +29 -0
  9. package/lib/lilylet/highlight.js +145 -0
  10. package/lib/lilylet/meiEncoder.js +126 -14
  11. package/lib/lilylet/staffLayout.d.ts +5 -0
  12. package/lib/lilylet/staffLayout.js +62 -0
  13. package/package.json +8 -2
  14. package/source/lilylet/abcDecoder.ts +14 -7
  15. package/source/lilylet/gmInstruments.ts +305 -0
  16. package/source/lilylet/highlight.ts +192 -0
  17. package/source/lilylet/meiEncoder.ts +135 -11
  18. package/source/lilylet/staffLayout.ts +76 -0
  19. package/lib/source/abc/abc.d.ts +0 -102
  20. package/lib/source/abc/abc.js +0 -25
  21. package/lib/source/abc/parser.d.ts +0 -3
  22. package/lib/source/abc/parser.js +0 -6
  23. package/lib/source/lilylet/abcDecoder.d.ts +0 -25
  24. package/lib/source/lilylet/abcDecoder.js +0 -1035
  25. package/lib/source/lilylet/index.d.ts +0 -10
  26. package/lib/source/lilylet/index.js +0 -10
  27. package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
  28. package/lib/source/lilylet/lilypondDecoder.js +0 -1223
  29. package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
  30. package/lib/source/lilylet/lilypondEncoder.js +0 -893
  31. package/lib/source/lilylet/meiEncoder.d.ts +0 -8
  32. package/lib/source/lilylet/meiEncoder.js +0 -1985
  33. package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
  34. package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
  35. package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
  36. package/lib/source/lilylet/musicXmlEncoder.js +0 -701
  37. package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
  38. package/lib/source/lilylet/musicXmlTypes.js +0 -7
  39. package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
  40. package/lib/source/lilylet/musicXmlUtils.js +0 -469
  41. package/lib/source/lilylet/parser.d.ts +0 -14
  42. package/lib/source/lilylet/parser.js +0 -161
  43. package/lib/source/lilylet/serializer.d.ts +0 -11
  44. package/lib/source/lilylet/serializer.js +0 -791
  45. package/lib/source/lilylet/types.d.ts +0 -253
  46. package/lib/source/lilylet/types.js +0 -100
  47. package/lib/tests/abc-abcjs-parse.d.ts +0 -8
  48. package/lib/tests/abc-abcjs-parse.js +0 -90
  49. package/lib/tests/abc-abcjs-svg.d.ts +0 -1
  50. package/lib/tests/abc-abcjs-svg.js +0 -143
  51. package/lib/tests/abc-decoder.d.ts +0 -1
  52. package/lib/tests/abc-decoder.js +0 -67
  53. package/lib/tests/abc-mei-compare.d.ts +0 -1
  54. package/lib/tests/abc-mei-compare.js +0 -525
  55. package/lib/tests/auto-beam.d.ts +0 -9
  56. package/lib/tests/auto-beam.js +0 -151
  57. package/lib/tests/computeMeiHashes.d.ts +0 -1
  58. package/lib/tests/computeMeiHashes.js +0 -87
  59. package/lib/tests/encoder-mutation.d.ts +0 -9
  60. package/lib/tests/encoder-mutation.js +0 -110
  61. package/lib/tests/gpt-review-issues.d.ts +0 -5
  62. package/lib/tests/gpt-review-issues.js +0 -255
  63. package/lib/tests/json-to-lyl.d.ts +0 -1
  64. package/lib/tests/json-to-lyl.js +0 -18
  65. package/lib/tests/lilypond-roundtrip.d.ts +0 -7
  66. package/lib/tests/lilypond-roundtrip.js +0 -558
  67. package/lib/tests/lilypondDecoder.d.ts +0 -6
  68. package/lib/tests/lilypondDecoder.js +0 -95
  69. package/lib/tests/ly-to-lyl.d.ts +0 -1
  70. package/lib/tests/ly-to-lyl.js +0 -12
  71. package/lib/tests/mei.d.ts +0 -1
  72. package/lib/tests/mei.js +0 -278
  73. package/lib/tests/musicxml-decoder.d.ts +0 -4
  74. package/lib/tests/musicxml-decoder.js +0 -61
  75. package/lib/tests/musicxml-detail.d.ts +0 -4
  76. package/lib/tests/musicxml-detail.js +0 -85
  77. package/lib/tests/musicxml-fprod.d.ts +0 -9
  78. package/lib/tests/musicxml-fprod.js +0 -153
  79. package/lib/tests/musicxml-roundtrip.d.ts +0 -7
  80. package/lib/tests/musicxml-roundtrip.js +0 -296
  81. package/lib/tests/musicxml-to-mei.d.ts +0 -6
  82. package/lib/tests/musicxml-to-mei.js +0 -115
  83. package/lib/tests/parser.d.ts +0 -1
  84. package/lib/tests/parser.js +0 -17
  85. package/lib/tests/render-k283.d.ts +0 -1
  86. package/lib/tests/render-k283.js +0 -33
  87. package/lib/tests/render-lyl.d.ts +0 -1
  88. package/lib/tests/render-lyl.js +0 -35
  89. package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
  90. package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
  91. package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
  92. package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
  93. package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
  94. package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
  95. package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
  96. package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
  97. package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
  98. package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
  99. package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
  100. package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
  101. package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
  102. package/lib/tests/unit/gptReviewIssues.test.js +0 -240
  103. package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
  104. package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
  105. package/lib/tests/unit/partialWarning.test.d.ts +0 -4
  106. package/lib/tests/unit/partialWarning.test.js +0 -65
  107. package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
  108. package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
  109. package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
  110. package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
  111. package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
  112. package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
  113. package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
  114. package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
  115. package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
  116. package/lib/tests/unit/voiceStaffParsing.test.js +0 -118
@@ -1,296 +0,0 @@
1
- /**
2
- * MusicXML Roundtrip Test
3
- *
4
- * Tests the lilylet -> musicxml -> lilylet conversion cycle.
5
- * Compares the output of two conversions to verify consistency.
6
- */
7
- import * as fs from "fs";
8
- import * as path from "path";
9
- import { parseCode, serializeLilyletDoc, musicXmlEncoder, musicXmlDecoder } from "../source/lilylet/index.js";
10
- const UNIT_CASES_DIR = path.join(import.meta.dirname, "assets/unit-cases");
11
- const OUTPUT_DIR = path.join(import.meta.dirname, "output/musicxml-roundtrip");
12
- // Known limitations - skip these tests
13
- const SKIP_FILES = new Set([]);
14
- // Ensure output directory exists
15
- if (!fs.existsSync(OUTPUT_DIR)) {
16
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
17
- }
18
- /**
19
- * Flatten tuplet events - extract inner events from TupletEvent
20
- */
21
- const flattenEvents = (events) => {
22
- const result = [];
23
- for (const e of events) {
24
- if (e.type === 'tuplet') {
25
- result.push(...e.events);
26
- }
27
- else {
28
- result.push(e);
29
- }
30
- }
31
- return result;
32
- };
33
- /**
34
- * Filter to only note and rest events (core musical content).
35
- * Context events (key, time, clef, ottava, stemDirection, tempo) are handled
36
- * at the measure/attribute level in MusicXML and may not survive as voice events.
37
- * Other event types (tremolo, harmony, barline, markup) may also be lost or
38
- * repositioned during roundtrip.
39
- */
40
- const filterToNoteRest = (events) => events.filter(e => e.type === 'note' || e.type === 'rest');
41
- /**
42
- * Compare two LilyletDoc structures with robust comparison.
43
- *
44
- * Strategy:
45
- * - Flatten tuplets, filter artifacts, dedupe context events
46
- * - Compare by staff (not voice index) across all measures
47
- * - Verify pitch/duration content of note events
48
- */
49
- const compareDocuments = (doc1, doc2) => {
50
- // Collect all note/rest events from all measures for a specific staff within a part
51
- const collectEventsByStaff = (measures, partIndex, staff) => {
52
- const allEvents = [];
53
- for (const m of measures) {
54
- const part = m.parts[partIndex];
55
- if (part) {
56
- for (const voice of part.voices) {
57
- if ((voice.staff || 1) === staff) {
58
- allEvents.push(...filterToNoteRest(flattenEvents(voice.events)));
59
- }
60
- }
61
- }
62
- }
63
- return allEvents;
64
- };
65
- // Get all unique staves used in a part across all measures
66
- const getStaves = (measures, partIndex) => {
67
- const staves = new Set();
68
- for (const m of measures) {
69
- const part = m.parts[partIndex];
70
- if (part) {
71
- for (const voice of part.voices) {
72
- staves.add(voice.staff || 1);
73
- }
74
- }
75
- }
76
- return Array.from(staves).sort((a, b) => a - b);
77
- };
78
- // Get max parts count
79
- const maxParts1 = Math.max(...doc1.measures.map(m => m.parts.length), 0);
80
- const maxParts2 = Math.max(...doc2.measures.map(m => m.parts.length), 0);
81
- if (maxParts1 !== maxParts2) {
82
- return {
83
- equal: false,
84
- diff: `Part count differs: ${maxParts1} vs ${maxParts2}`
85
- };
86
- }
87
- // Compare each part by staff
88
- for (let pi = 0; pi < maxParts1; pi++) {
89
- const staves1 = getStaves(doc1.measures, pi);
90
- const staves2 = getStaves(doc2.measures, pi);
91
- if (staves1.length !== staves2.length) {
92
- return {
93
- equal: false,
94
- diff: `Part ${pi + 1}: Staff count differs: ${staves1.length} vs ${staves2.length}`
95
- };
96
- }
97
- // Compare events for each staff
98
- for (const staff of staves1) {
99
- const events1 = collectEventsByStaff(doc1.measures, pi, staff);
100
- const events2 = collectEventsByStaff(doc2.measures, pi, staff);
101
- if (events1.length !== events2.length) {
102
- return {
103
- equal: false,
104
- diff: `Part ${pi + 1}, Staff ${staff}: Total event count differs: ${events1.length} vs ${events2.length}`
105
- };
106
- }
107
- // Content verification: compare note/rest pitch and duration values
108
- for (let i = 0; i < events1.length; i++) {
109
- const e1 = events1[i];
110
- const e2 = events2[i];
111
- if (e1.type !== e2.type) {
112
- return {
113
- equal: false,
114
- diff: `Part ${pi + 1}, Staff ${staff}, Event ${i + 1}: Type differs: ${e1.type} vs ${e2.type}`
115
- };
116
- }
117
- if (e1.type === 'note' && e2.type === 'note') {
118
- // Compare pitch count
119
- if (e1.pitches.length !== e2.pitches.length) {
120
- return {
121
- equal: false,
122
- diff: `Part ${pi + 1}, Staff ${staff}, Event ${i + 1}: Pitch count differs: ${e1.pitches.length} vs ${e2.pitches.length}`
123
- };
124
- }
125
- // Compare each pitch (phonet + octave)
126
- for (let j = 0; j < e1.pitches.length; j++) {
127
- const p1 = e1.pitches[j];
128
- const p2 = e2.pitches[j];
129
- if (p1.phonet !== p2.phonet || p1.octave !== p2.octave) {
130
- return {
131
- equal: false,
132
- diff: `Part ${pi + 1}, Staff ${staff}, Event ${i + 1}: Pitch ${j + 1} differs: ${p1.phonet}${p1.octave} vs ${p2.phonet}${p2.octave}`
133
- };
134
- }
135
- }
136
- // Compare duration (division + dots)
137
- if (e1.duration.division !== e2.duration.division || e1.duration.dots !== e2.duration.dots) {
138
- return {
139
- equal: false,
140
- diff: `Part ${pi + 1}, Staff ${staff}, Event ${i + 1}: Duration differs: div=${e1.duration.division} dots=${e1.duration.dots} vs div=${e2.duration.division} dots=${e2.duration.dots}`
141
- };
142
- }
143
- }
144
- if (e1.type === 'rest' && e2.type === 'rest') {
145
- if (e1.duration.division !== e2.duration.division || e1.duration.dots !== e2.duration.dots) {
146
- return {
147
- equal: false,
148
- diff: `Part ${pi + 1}, Staff ${staff}, Event ${i + 1}: Rest duration differs: div=${e1.duration.division} dots=${e1.duration.dots} vs div=${e2.duration.division} dots=${e2.duration.dots}`
149
- };
150
- }
151
- }
152
- }
153
- }
154
- }
155
- return { equal: true };
156
- };
157
- /**
158
- * Run full roundtrip test
159
- */
160
- const testRoundtrip = (filename) => {
161
- const filepath = path.join(UNIT_CASES_DIR, filename);
162
- try {
163
- // Step 1: Read and parse original lilylet
164
- const originalLyl = fs.readFileSync(filepath, "utf-8");
165
- const doc1 = parseCode(originalLyl);
166
- if (!doc1 || doc1.measures.length === 0) {
167
- return {
168
- filename,
169
- status: "error",
170
- error: "Failed to parse original lilylet file"
171
- };
172
- }
173
- // Step 2: Encode to MusicXML
174
- const generatedXml = musicXmlEncoder.encode(doc1);
175
- // Save MusicXML for inspection
176
- const baseName = path.basename(filename, ".lyl");
177
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.musicxml`), generatedXml);
178
- // Step 3: Decode MusicXML back to LilyletDoc
179
- let doc2;
180
- try {
181
- doc2 = musicXmlDecoder.decode(generatedXml);
182
- }
183
- catch (e) {
184
- return {
185
- filename,
186
- status: "error",
187
- error: `MusicXML decode error: ${e instanceof Error ? e.message : String(e)}`,
188
- originalLyl,
189
- generatedXml
190
- };
191
- }
192
- if (!doc2 || doc2.measures.length === 0) {
193
- return {
194
- filename,
195
- status: "error",
196
- error: "Failed to decode MusicXML output",
197
- originalLyl,
198
- generatedXml
199
- };
200
- }
201
- // Step 4: Serialize back to lilylet
202
- const roundtripLyl = serializeLilyletDoc(doc2);
203
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.roundtrip.lyl`), roundtripLyl);
204
- // Step 5: Compare structures
205
- const comparison = compareDocuments(doc1, doc2);
206
- if (comparison.equal) {
207
- return {
208
- filename,
209
- status: "pass",
210
- originalLyl,
211
- generatedXml,
212
- roundtripLyl
213
- };
214
- }
215
- else {
216
- return {
217
- filename,
218
- status: "fail",
219
- error: comparison.diff,
220
- originalLyl,
221
- generatedXml,
222
- roundtripLyl
223
- };
224
- }
225
- }
226
- catch (e) {
227
- return {
228
- filename,
229
- status: "error",
230
- error: e instanceof Error ? e.message : String(e)
231
- };
232
- }
233
- };
234
- /**
235
- * Main test runner
236
- */
237
- const main = async () => {
238
- console.log("MusicXML Roundtrip Test\n");
239
- console.log("=".repeat(80));
240
- // Get all .lyl files in unit-cases
241
- const files = fs.readdirSync(UNIT_CASES_DIR)
242
- .filter(f => f.endsWith(".lyl"))
243
- .sort();
244
- console.log(`\nFound ${files.length} test files\n`);
245
- const results = [];
246
- let passed = 0;
247
- let failed = 0;
248
- let errors = 0;
249
- let skipped = 0;
250
- for (const filename of files) {
251
- // Check skip list
252
- if (SKIP_FILES.has(filename)) {
253
- results.push({ filename, status: "skip" });
254
- skipped++;
255
- console.log(`⏭️ ${filename} (skipped - known limitation)`);
256
- continue;
257
- }
258
- // Run full roundtrip test
259
- const result = testRoundtrip(filename);
260
- results.push(result);
261
- const statusIcon = result.status === "pass" ? "✅" :
262
- result.status === "fail" ? "❌" : "⚠️";
263
- console.log(`${statusIcon} ${filename}`);
264
- if (result.status === "pass") {
265
- passed++;
266
- }
267
- else if (result.status === "fail") {
268
- failed++;
269
- console.log(` Diff: ${result.error}`);
270
- }
271
- else {
272
- errors++;
273
- console.log(` Error: ${result.error}`);
274
- }
275
- }
276
- console.log("\n" + "=".repeat(80));
277
- console.log(`\nResults: ${passed} passed, ${failed} failed, ${errors} errors, ${skipped} skipped`);
278
- console.log(`Output files saved to: ${OUTPUT_DIR}\n`);
279
- // Save summary
280
- const summary = {
281
- total: files.length,
282
- passed,
283
- failed,
284
- errors,
285
- skipped,
286
- results: results.map(r => ({
287
- filename: r.filename,
288
- status: r.status,
289
- error: r.error
290
- }))
291
- };
292
- fs.writeFileSync(path.join(OUTPUT_DIR, "_summary.json"), JSON.stringify(summary, null, 2));
293
- // Exit with error code if any tests failed
294
- process.exit(failed + errors > 0 ? 1 : 0);
295
- };
296
- main().catch(console.error);
@@ -1,6 +0,0 @@
1
- /**
2
- * MusicXML to MEI Conversion - Output to tests/output/from-xml
3
- *
4
- * Converts all MusicXML test files to MEI and saves to output directory.
5
- */
6
- export {};
@@ -1,115 +0,0 @@
1
- /**
2
- * MusicXML to MEI Conversion - Output to tests/output/from-xml
3
- *
4
- * Converts all MusicXML test files to MEI and saves to output directory.
5
- */
6
- import { musicXmlDecoder, meiEncoder, serializeLilyletDoc } from '../source/lilylet/index.js';
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
- const MUSICXML_DIR = path.join(import.meta.dirname, 'assets/musicxml');
10
- const OUTPUT_DIR = path.join(import.meta.dirname, 'output/from-xml');
11
- // Ensure output directory exists
12
- if (!fs.existsSync(OUTPUT_DIR)) {
13
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
14
- }
15
- function countNotes(doc) {
16
- let count = 0;
17
- for (const measure of doc.measures) {
18
- for (const part of measure.parts) {
19
- for (const voice of part.voices) {
20
- for (const event of voice.events) {
21
- if (event.type === 'note') {
22
- count += event.pitches.length;
23
- }
24
- }
25
- }
26
- }
27
- }
28
- return count;
29
- }
30
- async function convertFile(filename) {
31
- const filepath = path.join(MUSICXML_DIR, filename);
32
- const baseName = filename.replace('.xml', '');
33
- try {
34
- // Read and decode MusicXML
35
- const xml = fs.readFileSync(filepath, 'utf-8');
36
- const doc = musicXmlDecoder.decode(xml);
37
- const measures = doc.measures.length;
38
- const notes = countNotes(doc);
39
- // Save JSON
40
- const jsonFile = path.join(OUTPUT_DIR, `${baseName}.json`);
41
- fs.writeFileSync(jsonFile, JSON.stringify(doc, null, 2));
42
- // Encode to MEI and save
43
- const mei = meiEncoder.encode(doc);
44
- const meiFile = path.join(OUTPUT_DIR, `${baseName}.mei`);
45
- fs.writeFileSync(meiFile, mei);
46
- // Serialize to Lilylet (.lyl) and save
47
- const lyl = serializeLilyletDoc(doc);
48
- const lylFile = path.join(OUTPUT_DIR, `${baseName}.lyl`);
49
- fs.writeFileSync(lylFile, lyl);
50
- return {
51
- name: filename,
52
- success: true,
53
- measures,
54
- notes,
55
- meiFile: `${baseName}.mei`,
56
- lylFile: `${baseName}.lyl`,
57
- jsonFile: `${baseName}.json`,
58
- };
59
- }
60
- catch (error) {
61
- return {
62
- name: filename,
63
- success: false,
64
- error: error.message,
65
- };
66
- }
67
- }
68
- async function main() {
69
- console.log('MusicXML to MEI Conversion\n');
70
- console.log(`Input: ${MUSICXML_DIR}`);
71
- console.log(`Output: ${OUTPUT_DIR}\n`);
72
- console.log('='.repeat(80));
73
- const files = fs.readdirSync(MUSICXML_DIR).filter(f => f.endsWith('.xml')).sort();
74
- const results = [];
75
- let passed = 0;
76
- let failed = 0;
77
- for (const file of files) {
78
- const result = await convertFile(file);
79
- results.push(result);
80
- const status = result.success ? '✅' : '❌';
81
- if (result.success) {
82
- console.log(`${status} ${file}`);
83
- console.log(` → ${result.meiFile}, ${result.lylFile} (${result.measures} measures, ${result.notes} notes)`);
84
- passed++;
85
- }
86
- else {
87
- console.log(`${status} ${file}`);
88
- console.log(` Error: ${result.error}`);
89
- failed++;
90
- }
91
- }
92
- console.log('\n' + '='.repeat(80));
93
- console.log(`\nConversion complete: ${passed} succeeded, ${failed} failed`);
94
- console.log(`Output files in: ${OUTPUT_DIR}`);
95
- // Write summary JSON
96
- const summaryFile = path.join(OUTPUT_DIR, '_summary.json');
97
- fs.writeFileSync(summaryFile, JSON.stringify({
98
- timestamp: new Date().toISOString(),
99
- inputDir: MUSICXML_DIR,
100
- outputDir: OUTPUT_DIR,
101
- total: files.length,
102
- passed,
103
- failed,
104
- results,
105
- }, null, 2));
106
- console.log(`Summary: ${summaryFile}`);
107
- // List output files
108
- console.log('\nGenerated files:');
109
- const outputFiles = fs.readdirSync(OUTPUT_DIR).sort();
110
- for (const f of outputFiles) {
111
- const stat = fs.statSync(path.join(OUTPUT_DIR, f));
112
- console.log(` ${f} (${(stat.size / 1024).toFixed(1)} KB)`);
113
- }
114
- }
115
- main();
@@ -1 +0,0 @@
1
- export {};
@@ -1,17 +0,0 @@
1
- import fs from "fs";
2
- import * as lilylet from "../source/lilylet.js";
3
- const parse = (lyl_dir) => {
4
- const files = fs.readdirSync(lyl_dir);
5
- for (const file of files) {
6
- const filePath = `${lyl_dir}/${file}`;
7
- const stat = fs.statSync(filePath);
8
- if (stat.isDirectory())
9
- continue;
10
- if (!file.endsWith('.lyl'))
11
- continue;
12
- const code = fs.readFileSync(filePath, { encoding: "utf-8" });
13
- lilylet.parseCode(code);
14
- console.log(file, "parsing passed.");
15
- }
16
- };
17
- parse("./tests/assets/unit-cases");
@@ -1 +0,0 @@
1
- export {};
@@ -1,33 +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 code = fs.readFileSync("/home/camus/work/openmusictheory-lilylet/Graphics/lilylet/k283.lyl", "utf-8");
8
- const doc = lilylet.parseCode(code);
9
- const mei = lilylet.meiEncoder.encode(doc);
10
- // Save MEI for debugging
11
- fs.writeFileSync("/home/camus/work/openmusictheory-lilylet/Graphics/lilylet/k283.mei", mei);
12
- const measureCount = doc.measures?.length || 1;
13
- let staffCount = 1;
14
- if (doc.measures.length > 0) {
15
- const firstMeasure = doc.measures[0];
16
- staffCount = firstMeasure.parts.reduce((total, part) => {
17
- const maxStaff = part.voices.reduce((max, voice) => Math.max(max, voice.staff || 1), 1);
18
- return total + maxStaff;
19
- }, 0) || 1;
20
- }
21
- const pageHeight = Math.max(2000, Math.ceil(measureCount / 20) * 2000) * 2 * staffCount;
22
- console.log(`Measures: ${measureCount}, Staves: ${staffCount}, PageHeight: ${pageHeight}`);
23
- const VerovioModule = await createVerovioModule();
24
- const vrvToolkit = new VerovioToolkit(VerovioModule);
25
- vrvToolkit.setOptions({ scale: 40, adjustPageHeight: true, pageHeight, pageWidth: 2100 });
26
- const success = vrvToolkit.loadData(mei);
27
- if (success === false) {
28
- console.error("Failed:", vrvToolkit.getLog());
29
- process.exit(1);
30
- }
31
- const svg = vrvToolkit.renderToSVG(1);
32
- fs.writeFileSync("/home/camus/work/openmusictheory-lilylet/Graphics/lilylet/k283.svg", svg);
33
- console.log(`Rendered: ${svg.length} bytes → k283.svg`);
@@ -1 +0,0 @@
1
- export {};
@@ -1,35 +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 lylPath = process.argv[2];
8
- if (!lylPath) {
9
- console.error("Usage: tsx tests/render-lyl.ts <file.lyl> [output.svg]");
10
- process.exit(1);
11
- }
12
- const svgPath = process.argv[3] || lylPath.replace(/\.lyl$/, ".svg");
13
- const code = fs.readFileSync(lylPath, "utf-8");
14
- const doc = lilylet.parseCode(code);
15
- const mei = lilylet.meiEncoder.encode(doc);
16
- const measureCount = doc.measures?.length || 1;
17
- let staffCount = 1;
18
- if (doc.measures.length > 0) {
19
- const fm = doc.measures[0];
20
- staffCount = fm.parts.reduce((t, p) => {
21
- const ms = p.voices.reduce((m, v) => Math.max(m, v.staff || 1), 1);
22
- return t + ms;
23
- }, 0) || 1;
24
- }
25
- const pageHeight = Math.max(2000, Math.ceil(measureCount / 20) * 2000) * 2 * staffCount;
26
- const VM = await createVerovioModule();
27
- const vt = new VerovioToolkit(VM);
28
- vt.setOptions({ scale: 40, adjustPageHeight: true, pageHeight, pageWidth: 2100 });
29
- if (vt.loadData(mei) === false) {
30
- console.error("Verovio failed:", vt.getLog());
31
- process.exit(1);
32
- }
33
- const svg = vt.renderToSVG(1);
34
- fs.writeFileSync(svgPath, svg);
35
- console.log("Rendered:", svgPath);
@@ -1,23 +0,0 @@
1
- /**
2
- * Unit test: \afterGrace / \acciaccatura inside \times 2/3 must NOT leak
3
- * notes outside the tuplet wrapper.
4
- *
5
- * Bug: chopin-25-2.ly measure 68 (voice 1) has the pattern:
6
- *
7
- * \times 2/3 { bf8 [ c8 \afterGrace { \acciaccatura { ef8 } df8 ) ] } { c8 } }
8
- *
9
- * The decoder collects notes backwards when the Tuplet term fires.
10
- * The \afterGrace / \acciaccatura emit their notes through a different
11
- * listener path that bypasses the flat note stream, so bf8 and c8 remain
12
- * in voice.events outside the tuplet while only df8 ends up wrapped.
13
- *
14
- * Expected (measure 68 has 4 × \times 2/3 { 3 eighth notes }, 2/2 time):
15
- * total duration = 4 × 480 = 1920 ticks (exactly 2/2)
16
- *
17
- * Actual (bugged):
18
- * 3 correct tuplets (1440) + bf8(240) + c8(240) + \times 2/3{df8}(160)
19
- * = 2080 ticks → exceeds 1920 capacity
20
- *
21
- * Usage: npx tsx tests/unit/afterGraceInsideTuplet.test.ts
22
- */
23
- export {};