@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,558 +0,0 @@
1
- /**
2
- * LilyPond Roundtrip Test
3
- *
4
- * Tests the lilylet -> lilypond -> 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, lilypondEncoder } from "../source/lilylet/index.js";
10
- // Import the LilyPond decoder
11
- import { decode as decodeLilypond } from "../source/lilylet/lilypondDecoder.js";
12
- const UNIT_CASES_DIR = path.join(import.meta.dirname, "assets/unit-cases");
13
- const OUTPUT_DIR = path.join(import.meta.dirname, "output/lilypond-roundtrip");
14
- // Ensure output directory exists
15
- if (!fs.existsSync(OUTPUT_DIR)) {
16
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
17
- }
18
- /**
19
- * Normalize lilylet content for comparison
20
- * - Remove comments
21
- * - Normalize whitespace
22
- * - Remove trailing measure markers
23
- */
24
- const normalizeLyl = (content) => {
25
- return content
26
- // Remove comments
27
- .replace(/%[^\n]*/g, '')
28
- // Remove measure markers like "| %1"
29
- .replace(/\|\s*%\d+/g, '|')
30
- // Normalize whitespace
31
- .replace(/\s+/g, ' ')
32
- // Remove leading/trailing whitespace
33
- .trim();
34
- };
35
- /**
36
- * Compare two LilyletDoc structures
37
- *
38
- * Note: Measure boundaries may differ between lilylet parser (doesn't enforce time signatures)
39
- * and lotus parser (enforces time signatures). We compare total events across all measures.
40
- */
41
- const compareDocuments = (doc1, doc2) => {
42
- // Filter helper - remove parser artifacts that don't represent musical content
43
- const filterEvents = (events) => events.filter(e => {
44
- if (e.type === 'pitchReset')
45
- return false;
46
- if (e.type === 'context' && 'staff' in e)
47
- return false;
48
- if (e.type === 'context' && 'stemDirection' in e)
49
- return false;
50
- if (e.type === 'rest' && e.invisible)
51
- return false;
52
- return true;
53
- });
54
- // Remove redundant consecutive context events
55
- const dedupeContextEvents = (events) => {
56
- const result = [];
57
- let lastClef;
58
- let lastKey;
59
- let lastTime;
60
- for (const e of events) {
61
- if (e.type === 'context') {
62
- const ctx = e;
63
- if ('stemDirection' in ctx)
64
- continue; // already filtered
65
- if ('clef' in ctx) {
66
- if (ctx.clef === lastClef)
67
- continue;
68
- lastClef = ctx.clef;
69
- }
70
- if ('key' in ctx) {
71
- const keyStr = JSON.stringify(ctx.key);
72
- if (keyStr === lastKey)
73
- continue;
74
- lastKey = keyStr;
75
- }
76
- if ('time' in ctx && ctx.time) {
77
- const timeStr = `${ctx.time.numerator}/${ctx.time.denominator}`;
78
- if (timeStr === lastTime)
79
- continue;
80
- lastTime = timeStr;
81
- }
82
- if ('timeSig' in ctx && ctx.timeSig) {
83
- const timeSigStr = `${ctx.timeSig.numerator}/${ctx.timeSig.denominator}`;
84
- if (timeSigStr === lastTime)
85
- continue;
86
- lastTime = timeSigStr;
87
- }
88
- }
89
- result.push(e);
90
- }
91
- return result;
92
- };
93
- // Collect musically-significant events (notes, rests, tuplets, times) preserving structure
94
- const collectMusical = (events) => events.filter(e => e.type === 'note' || e.type === 'rest' || e.type === 'tuplet' || e.type === 'times');
95
- // Flatten note/rest events from potentially nested tuplets/times (for pitch/duration checks)
96
- const flattenNoteRests = (events) => {
97
- const result = [];
98
- for (const e of events) {
99
- if (e.type === 'note' || e.type === 'rest') {
100
- result.push(e);
101
- }
102
- else if (e.type === 'tuplet' || e.type === 'times') {
103
- result.push(...flattenNoteRests(e.events));
104
- }
105
- }
106
- return result;
107
- };
108
- // Format a note/rest event for diff display
109
- const describeEvent = (e, index) => {
110
- if (e.type === 'rest') {
111
- const r = e;
112
- if (r.pitch)
113
- return `[${index}] rest(${r.pitch.phonet}${r.pitch.octave}) dur=${r.duration.division}${'.'.repeat(r.duration.dots)}`;
114
- return `[${index}] rest dur=${r.duration.division}${'.'.repeat(r.duration.dots)}`;
115
- }
116
- const n = e;
117
- const pitches = n.pitches.map(p => `${p.phonet}${p.accidental ? '(' + p.accidental + ')' : ''}${p.octave}`).join('+');
118
- return `[${index}] note(${pitches}) dur=${n.duration.division}${'.'.repeat(n.duration.dots)}`;
119
- };
120
- // Compare two note/rest events
121
- const eventsMatch = (a, b) => {
122
- if (a.type !== b.type)
123
- return false;
124
- // Compare duration
125
- if (a.duration.division !== b.duration.division)
126
- return false;
127
- if (a.duration.dots !== b.duration.dots)
128
- return false;
129
- if (a.type === 'note' && b.type === 'note') {
130
- const na = a;
131
- const nb = b;
132
- if (na.pitches.length !== nb.pitches.length)
133
- return false;
134
- for (let i = 0; i < na.pitches.length; i++) {
135
- if (na.pitches[i].phonet !== nb.pitches[i].phonet)
136
- return false;
137
- if (na.pitches[i].octave !== nb.pitches[i].octave)
138
- return false;
139
- // Don't compare accidental - it may differ between parsers due to key context
140
- }
141
- }
142
- if (a.type === 'rest' && b.type === 'rest') {
143
- const ra = a;
144
- const rb = b;
145
- // Both pitched or both unpitched
146
- if (!!ra.pitch !== !!rb.pitch)
147
- return false;
148
- if (ra.pitch && rb.pitch) {
149
- if (ra.pitch.phonet !== rb.pitch.phonet)
150
- return false;
151
- if (ra.pitch.octave !== rb.pitch.octave)
152
- return false;
153
- }
154
- }
155
- return true;
156
- };
157
- // Collect events for a specific voice (by 0-based index within a staff) across all measures.
158
- // Per-voice comparison is robust to measure-boundary drift caused by overfull source measures:
159
- // if voice A's last note overflows into the next measure in the decoder, the comparison
160
- // still sees voice A's events as a unit rather than interleaved with voice B's events.
161
- const collectEventsByVoiceIndex = (measures, partIndex, staff, voiceIndex) => {
162
- const allEvents = [];
163
- for (const m of measures) {
164
- const part = m.parts[partIndex];
165
- if (!part)
166
- continue;
167
- let idx = 0;
168
- for (const voice of part.voices) {
169
- if (voice.staff !== staff)
170
- continue;
171
- if (idx === voiceIndex) {
172
- allEvents.push(...filterEvents(voice.events));
173
- break;
174
- }
175
- idx++;
176
- }
177
- }
178
- return dedupeContextEvents(allEvents);
179
- };
180
- // Count voices per staff across all measures (only voices with non-spacer musical content)
181
- const getVoiceCountByStaff = (measures, partIndex) => {
182
- const maxVoices = new Map();
183
- for (const m of measures) {
184
- const part = m.parts[partIndex];
185
- if (!part)
186
- continue;
187
- const staffCounts = new Map();
188
- for (const voice of part.voices) {
189
- if (!hasNonSpacerContent(voice.events))
190
- continue;
191
- const s = voice.staff || 1;
192
- staffCounts.set(s, (staffCounts.get(s) || 0) + 1);
193
- }
194
- for (const [s, count] of staffCounts) {
195
- maxVoices.set(s, Math.max(maxVoices.get(s) || 0, count));
196
- }
197
- }
198
- return maxVoices;
199
- };
200
- // Check if a voice has any non-spacer musical content
201
- const hasNonSpacerContent = (events) => events.some(e => {
202
- if (e.type === 'note')
203
- return true;
204
- if (e.type === 'rest' && !e.invisible)
205
- return true;
206
- if (e.type === 'tuplet' || e.type === 'times' || e.type === 'tremolo')
207
- return true;
208
- return false;
209
- });
210
- // Get all unique staves used in a part that have at least some real (non-spacer) content.
211
- // Spacer-only staves are filtered because the LilyPond encoder/decoder drops them.
212
- const getStaves = (measures, partIndex) => {
213
- // Collect staff → whether any measure has non-spacer content
214
- const staffHasContent = new Map();
215
- for (const m of measures) {
216
- const part = m.parts[partIndex];
217
- if (part) {
218
- for (const voice of part.voices) {
219
- const s = voice.staff || 1;
220
- if (!staffHasContent.get(s) && hasNonSpacerContent(voice.events)) {
221
- staffHasContent.set(s, true);
222
- }
223
- else if (!staffHasContent.has(s)) {
224
- staffHasContent.set(s, false);
225
- }
226
- }
227
- }
228
- }
229
- // Only return staves that have real content in at least one measure
230
- return Array.from(staffHasContent.entries())
231
- .filter(([, hasContent]) => hasContent)
232
- .map(([s]) => s)
233
- .sort((a, b) => a - b);
234
- };
235
- // Get max parts count
236
- const maxParts1 = Math.max(...doc1.measures.map(m => m.parts.length), 0);
237
- const maxParts2 = Math.max(...doc2.measures.map(m => m.parts.length), 0);
238
- if (maxParts1 !== maxParts2) {
239
- return {
240
- equal: false,
241
- diff: `Part count differs: ${maxParts1} vs ${maxParts2}`
242
- };
243
- }
244
- // Compare each part
245
- for (let pi = 0; pi < maxParts1; pi++) {
246
- const staves1 = getStaves(doc1.measures, pi);
247
- const staves2 = getStaves(doc2.measures, pi);
248
- // Compare staff counts
249
- if (staves1.length !== staves2.length) {
250
- return {
251
- equal: false,
252
- diff: `Part ${pi + 1}: Staff count differs: ${staves1.length} (staves ${staves1.join(',')}) vs ${staves2.length} (staves ${staves2.join(',')})`
253
- };
254
- }
255
- // Compare voice counts per staff
256
- const voices1 = getVoiceCountByStaff(doc1.measures, pi);
257
- const voices2 = getVoiceCountByStaff(doc2.measures, pi);
258
- for (const staff of staves1) {
259
- const v1 = voices1.get(staff) || 0;
260
- const v2 = voices2.get(staff) || 0;
261
- if (v1 !== v2) {
262
- return {
263
- equal: false,
264
- diff: `Part ${pi + 1}, Staff ${staff}: Voice count differs: ${v1} vs ${v2}`
265
- };
266
- }
267
- }
268
- // Compare events for each staff, voice by voice
269
- for (const staff of staves1) {
270
- const numVoices = voices1.get(staff) || 0;
271
- for (let vi = 0; vi < numVoices; vi++) {
272
- const events1 = collectEventsByVoiceIndex(doc1.measures, pi, staff, vi);
273
- const events2 = collectEventsByVoiceIndex(doc2.measures, pi, staff, vi);
274
- // Compare event count
275
- const noteRests1 = flattenNoteRests(events1);
276
- const noteRests2 = flattenNoteRests(events2);
277
- if (noteRests1.length !== noteRests2.length) {
278
- return {
279
- equal: false,
280
- diff: `Part ${pi + 1}, Staff ${staff}, Voice ${vi + 1}: Note/rest count differs: ${noteRests1.length} vs ${noteRests2.length}`
281
- };
282
- }
283
- // Compare each note/rest event content
284
- for (let i = 0; i < noteRests1.length; i++) {
285
- if (!eventsMatch(noteRests1[i], noteRests2[i])) {
286
- return {
287
- equal: false,
288
- diff: `Part ${pi + 1}, Staff ${staff}, Voice ${vi + 1}: Event mismatch at index ${i}: ${describeEvent(noteRests1[i], i)} vs ${describeEvent(noteRests2[i], i)}`
289
- };
290
- }
291
- }
292
- // Compare tuplet/times structure: type, count, and ratios must match
293
- const tuplesLike1 = collectMusical(events1).filter(e => e.type === 'tuplet' || e.type === 'times');
294
- const tuplesLike2 = collectMusical(events2).filter(e => e.type === 'tuplet' || e.type === 'times');
295
- if (tuplesLike1.length !== tuplesLike2.length) {
296
- return {
297
- equal: false,
298
- diff: `Part ${pi + 1}, Staff ${staff}, Voice ${vi + 1}: Tuplet/times count differs: ${tuplesLike1.length} vs ${tuplesLike2.length}`
299
- };
300
- }
301
- for (let i = 0; i < tuplesLike1.length; i++) {
302
- const t1 = tuplesLike1[i], t2 = tuplesLike2[i];
303
- if (t1.type !== t2.type) {
304
- return {
305
- equal: false,
306
- diff: `Part ${pi + 1}, Staff ${staff}, Voice ${vi + 1}: Tuplet ${i} type differs: "${t1.type}" vs "${t2.type}"`
307
- };
308
- }
309
- const r1 = t1.ratio, r2 = t2.ratio;
310
- if (r1.numerator !== r2.numerator || r1.denominator !== r2.denominator) {
311
- return {
312
- equal: false,
313
- diff: `Part ${pi + 1}, Staff ${staff}, Voice ${vi + 1}: Tuplet ${i} ratio differs: ${r1.numerator}/${r1.denominator} vs ${r2.numerator}/${r2.denominator}`
314
- };
315
- }
316
- }
317
- } // end voice loop
318
- }
319
- }
320
- return { equal: true };
321
- };
322
- /**
323
- * Run roundtrip test on a single file
324
- */
325
- const testFile = async (filename) => {
326
- const filepath = path.join(UNIT_CASES_DIR, filename);
327
- try {
328
- // Step 1: Read and parse original lilylet
329
- const originalLyl = fs.readFileSync(filepath, "utf-8");
330
- const doc1 = parseCode(originalLyl);
331
- if (!doc1 || doc1.measures.length === 0) {
332
- return {
333
- filename,
334
- status: "error",
335
- error: "Failed to parse original lilylet file"
336
- };
337
- }
338
- // Step 2: Encode to LilyPond
339
- const generatedLy = lilypondEncoder.encode(doc1, {
340
- paper: { width: 210, height: 297 },
341
- fontSize: 20,
342
- withMIDI: false,
343
- autoBeaming: false
344
- });
345
- // Step 3: Decode LilyPond back to LilyletDoc
346
- let doc2;
347
- try {
348
- doc2 = await decodeLilypond(generatedLy);
349
- }
350
- catch (e) {
351
- // LilyPond decoder might not be fully compatible
352
- // Fall back to comparing serialized output
353
- return {
354
- filename,
355
- status: "error",
356
- error: `LilyPond decode error: ${e instanceof Error ? e.message : String(e)}`,
357
- originalLyl,
358
- generatedLy
359
- };
360
- }
361
- if (!doc2 || doc2.measures.length === 0) {
362
- return {
363
- filename,
364
- status: "error",
365
- error: "Failed to decode LilyPond output",
366
- originalLyl,
367
- generatedLy
368
- };
369
- }
370
- // Step 4: Serialize back to lilylet
371
- const roundtripLyl = serializeLilyletDoc(doc2);
372
- // Step 5: Save .ly and .json for inspection
373
- const baseName = path.basename(filename, ".lyl");
374
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.ly`), generatedLy);
375
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.json`), JSON.stringify(doc1, null, 2));
376
- // Step 6: Compare structures
377
- const comparison = compareDocuments(doc1, doc2);
378
- if (comparison.equal) {
379
- return {
380
- filename,
381
- status: "pass",
382
- originalLyl,
383
- generatedLy,
384
- roundtripLyl
385
- };
386
- }
387
- else {
388
- return {
389
- filename,
390
- status: "fail",
391
- error: comparison.diff,
392
- originalLyl,
393
- generatedLy,
394
- roundtripLyl
395
- };
396
- }
397
- }
398
- catch (e) {
399
- return {
400
- filename,
401
- status: "error",
402
- error: e instanceof Error ? e.message : String(e)
403
- };
404
- }
405
- };
406
- /**
407
- * Run encoding test with full content verification
408
- * Saves both .ly and .json for inspection
409
- */
410
- const testEncoding = (filename) => {
411
- const filepath = path.join(UNIT_CASES_DIR, filename);
412
- const baseName = path.basename(filename, ".lyl");
413
- try {
414
- // Step 1: Read and parse original lilylet
415
- const originalLyl = fs.readFileSync(filepath, "utf-8");
416
- const doc = parseCode(originalLyl);
417
- if (!doc || doc.measures.length === 0) {
418
- return {
419
- filename,
420
- status: "error",
421
- error: "Failed to parse original lilylet file"
422
- };
423
- }
424
- // Step 2: Save LilyletDoc as JSON for inspection
425
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.json`), JSON.stringify(doc, null, 2));
426
- // Step 3: Encode to LilyPond
427
- const generatedLy = lilypondEncoder.encode(doc, {
428
- paper: { width: 210, height: 297 },
429
- fontSize: 20,
430
- withMIDI: false,
431
- autoBeaming: false
432
- });
433
- // Step 4: Check that output is valid LilyPond syntax (basic checks)
434
- if (!generatedLy.includes('\\version')) {
435
- return {
436
- filename,
437
- status: "error",
438
- error: "Generated LilyPond missing version header"
439
- };
440
- }
441
- if (!generatedLy.includes('\\score')) {
442
- return {
443
- filename,
444
- status: "error",
445
- error: "Generated LilyPond missing score block"
446
- };
447
- }
448
- // Step 5: Save LilyPond output for inspection
449
- fs.writeFileSync(path.join(OUTPUT_DIR, `${baseName}.ly`), generatedLy);
450
- // Step 6: Verify content - check pitch encoding and spacer rests
451
- // Each measure should have pitches with correct octave values
452
- let currentTimeSig;
453
- for (let mi = 0; mi < doc.measures.length; mi++) {
454
- const measure = doc.measures[mi];
455
- if (measure.timeSig)
456
- currentTimeSig = measure.timeSig;
457
- for (const part of measure.parts) {
458
- for (const voice of part.voices) {
459
- for (const event of voice.events) {
460
- if (event.type === 'note') {
461
- const noteEvent = event;
462
- for (const pitch of noteEvent.pitches) {
463
- // Verify octave is a valid number (after resolution)
464
- if (typeof pitch.octave !== 'number' || isNaN(pitch.octave)) {
465
- return {
466
- filename,
467
- status: "fail",
468
- error: `Measure ${mi + 1}: Invalid octave value for pitch ${pitch.phonet}`
469
- };
470
- }
471
- }
472
- }
473
- }
474
- }
475
- }
476
- }
477
- // Step 7: Verify spacer rests match time signature
478
- if (currentTimeSig) {
479
- const { numerator, denominator } = currentTimeSig;
480
- const quarterBeats = numerator * (4 / denominator);
481
- // Check for incorrect s1 in non-4/4 time
482
- if (quarterBeats !== 4 && generatedLy.includes('{ s1 }')) {
483
- return {
484
- filename,
485
- status: "fail",
486
- error: `Spacer rest 's1' used in ${numerator}/${denominator} time (should be duration matching ${quarterBeats} quarter beats)`
487
- };
488
- }
489
- }
490
- return {
491
- filename,
492
- status: "pass",
493
- originalLyl,
494
- generatedLy
495
- };
496
- }
497
- catch (e) {
498
- return {
499
- filename,
500
- status: "error",
501
- error: e instanceof Error ? e.message : String(e)
502
- };
503
- }
504
- };
505
- /**
506
- * Main test runner
507
- */
508
- const main = async () => {
509
- console.log("LilyPond Encoder Test\n");
510
- console.log("=".repeat(80));
511
- // Get all .lyl files in unit-cases
512
- const files = fs.readdirSync(UNIT_CASES_DIR)
513
- .filter(f => f.endsWith(".lyl"))
514
- .sort();
515
- console.log(`\nFound ${files.length} test files\n`);
516
- const results = [];
517
- let passed = 0;
518
- let failed = 0;
519
- let errors = 0;
520
- for (const filename of files) {
521
- // Run full roundtrip test
522
- const result = await testFile(filename);
523
- results.push(result);
524
- const statusIcon = result.status === "pass" ? "✅" :
525
- result.status === "fail" ? "❌" : "⚠️";
526
- console.log(`${statusIcon} ${filename}`);
527
- if (result.status === "pass") {
528
- passed++;
529
- }
530
- else if (result.status === "fail") {
531
- failed++;
532
- console.log(` Diff: ${result.error}`);
533
- }
534
- else {
535
- errors++;
536
- console.log(` Error: ${result.error}`);
537
- }
538
- }
539
- console.log("\n" + "=".repeat(80));
540
- console.log(`\nResults: ${passed} passed, ${failed} failed, ${errors} errors`);
541
- console.log(`Output files saved to: ${OUTPUT_DIR}\n`);
542
- // Save summary
543
- const summary = {
544
- total: files.length,
545
- passed,
546
- failed,
547
- errors,
548
- results: results.map(r => ({
549
- filename: r.filename,
550
- status: r.status,
551
- error: r.error
552
- }))
553
- };
554
- fs.writeFileSync(path.join(OUTPUT_DIR, "_summary.json"), JSON.stringify(summary, null, 2));
555
- // Exit with error code if any tests failed
556
- process.exit(failed + errors > 0 ? 1 : 0);
557
- };
558
- main().catch(console.error);
@@ -1,6 +0,0 @@
1
- /**
2
- * Test for LilyPond decoder using MutopiaProject files
3
- *
4
- * Usage: npx ts-node tests/lilypondDecoder.ts <path-to-ly-files> [output-dir] [max-files]
5
- */
6
- export {};
@@ -1,95 +0,0 @@
1
- /**
2
- * Test for LilyPond decoder using MutopiaProject files
3
- *
4
- * Usage: npx ts-node tests/lilypondDecoder.ts <path-to-ly-files> [output-dir] [max-files]
5
- */
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import { decode } from "../source/lilylet/lilypondDecoder.js";
9
- import { serializeLilyletDoc } from "../source/lilylet/serializer.js";
10
- // Get path from command line argument
11
- const args = process.argv.slice(2);
12
- const LY_FILES_PATH = args[0];
13
- const OUTPUT_DIR = args[1] || './tests/output/from-ly';
14
- const MAX_FILES = parseInt(args[2] || '100', 10);
15
- if (!LY_FILES_PATH) {
16
- console.error('Usage: npx ts-node tests/lilypondDecoder.ts <path-to-ly-files> [output-dir] [max-files]');
17
- console.error('Example: npx ts-node tests/lilypondDecoder.ts ~/work/others/MutopiaProject ./output 50');
18
- process.exit(1);
19
- }
20
- // Find .ly files recursively
21
- const findLyFiles = (dir, maxFiles = 100) => {
22
- const files = [];
23
- const walk = (currentDir) => {
24
- if (files.length >= maxFiles)
25
- return;
26
- try {
27
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
28
- for (const entry of entries) {
29
- if (files.length >= maxFiles)
30
- return;
31
- const fullPath = path.join(currentDir, entry.name);
32
- if (entry.isDirectory()) {
33
- walk(fullPath);
34
- }
35
- else if (entry.name.endsWith('.ly')) {
36
- files.push(fullPath);
37
- }
38
- }
39
- }
40
- catch (e) {
41
- // Skip directories we can't read
42
- }
43
- };
44
- walk(dir);
45
- return files;
46
- };
47
- const testDecoder = async () => {
48
- console.log(`Finding LilyPond files in ${LY_FILES_PATH}...`);
49
- console.log(`Output directory: ${OUTPUT_DIR}\n`);
50
- // Create output directories
51
- const jsonDir = path.join(OUTPUT_DIR, 'json');
52
- const lylDir = path.join(OUTPUT_DIR, 'lyl');
53
- if (!fs.existsSync(jsonDir)) {
54
- fs.mkdirSync(jsonDir, { recursive: true });
55
- }
56
- if (!fs.existsSync(lylDir)) {
57
- fs.mkdirSync(lylDir, { recursive: true });
58
- }
59
- const lyFiles = findLyFiles(LY_FILES_PATH, MAX_FILES);
60
- console.log(`Found ${lyFiles.length} .ly files to test\n`);
61
- let passed = 0;
62
- let failed = 0;
63
- for (const filePath of lyFiles) {
64
- const relativePath = path.relative(LY_FILES_PATH, filePath);
65
- const baseName = path.basename(filePath, '.ly');
66
- try {
67
- const source = fs.readFileSync(filePath, 'utf-8');
68
- const doc = await decode(source);
69
- const measureCount = doc.measures.length;
70
- const voiceCount = doc.measures.reduce((sum, m) => sum + m.parts.reduce((psum, p) => psum + p.voices.length, 0), 0);
71
- const noteCount = doc.measures.reduce((sum, m) => sum + m.parts.reduce((psum, p) => psum + p.voices.reduce((vsum, v) => vsum + v.events.filter(e => e.type === 'note').length, 0), 0), 0);
72
- // Write JSON output
73
- const jsonPath = path.join(jsonDir, `${baseName}.json`);
74
- fs.writeFileSync(jsonPath, JSON.stringify(doc, null, 2));
75
- // Write .lyl output using serializer
76
- const lylContent = serializeLilyletDoc(doc);
77
- const lylPath = path.join(lylDir, `${baseName}.lyl`);
78
- fs.writeFileSync(lylPath, lylContent);
79
- console.log(`✓ ${relativePath}`);
80
- console.log(` Measures: ${measureCount}, Voices: ${voiceCount}, Notes: ${noteCount}`);
81
- console.log(` -> ${baseName}.json, ${baseName}.lyl\n`);
82
- passed++;
83
- }
84
- catch (e) {
85
- console.log(`✗ ${relativePath}`);
86
- console.log(` Error: ${e.message}\n`);
87
- failed++;
88
- }
89
- }
90
- console.log('========================================');
91
- console.log(`Total: ${lyFiles.length}, Passed: ${passed}, Failed: ${failed}`);
92
- console.log(`JSON output: ${jsonDir}`);
93
- console.log(`LYL output: ${lylDir}`);
94
- };
95
- testDecoder().catch(console.error);
@@ -1 +0,0 @@
1
- export {};
@@ -1,12 +0,0 @@
1
- import { serializeLilyletDoc } from "../source/lilylet/index.js";
2
- import { decode as decodeLilypond } from "../source/lilylet/lilypondDecoder.js";
3
- import * as fs from "fs";
4
- const file = process.argv[2];
5
- if (!file) {
6
- console.error("Usage: npx tsx tests/ly-to-lyl.ts <file.ly>");
7
- process.exit(1);
8
- }
9
- const source = fs.readFileSync(file, "utf-8");
10
- const doc = decodeLilypond(source);
11
- const lyl = serializeLilyletDoc(doc);
12
- console.log(lyl);
@@ -1 +0,0 @@
1
- export {};