@k-l-lambda/lilylet 0.1.49 → 0.1.50

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 (71) hide show
  1. package/lib/abc/abc.d.ts +102 -0
  2. package/lib/abc/abc.js +25 -0
  3. package/lib/abc/grammar.jison.js +1203 -0
  4. package/lib/abc/parser.d.ts +3 -0
  5. package/lib/abc/parser.js +6 -0
  6. package/lib/abcDecoder.d.ts +1 -0
  7. package/lib/abcDecoder.js +1 -0
  8. package/lib/grammar.jison.js +1 -1303
  9. package/lib/index.d.ts +1 -8
  10. package/lib/index.js +1 -10
  11. package/lib/lilylet/abcDecoder.d.ts +25 -0
  12. package/lib/lilylet/abcDecoder.js +1007 -0
  13. package/lib/lilylet/grammar.jison.js +1308 -0
  14. package/lib/lilylet/index.d.ts +10 -0
  15. package/lib/lilylet/index.js +10 -0
  16. package/lib/lilylet/lilypondDecoder.d.ts +29 -0
  17. package/lib/lilylet/lilypondDecoder.js +1053 -0
  18. package/lib/lilylet/lilypondEncoder.d.ts +34 -0
  19. package/lib/lilylet/lilypondEncoder.js +759 -0
  20. package/lib/lilylet/meiEncoder.d.ts +8 -0
  21. package/lib/lilylet/meiEncoder.js +1808 -0
  22. package/lib/lilylet/musicXmlDecoder.d.ts +20 -0
  23. package/lib/lilylet/musicXmlDecoder.js +1195 -0
  24. package/lib/lilylet/musicXmlEncoder.d.ts +15 -0
  25. package/lib/lilylet/musicXmlEncoder.js +701 -0
  26. package/lib/lilylet/musicXmlTypes.d.ts +199 -0
  27. package/lib/lilylet/musicXmlTypes.js +7 -0
  28. package/lib/lilylet/musicXmlUtils.d.ts +92 -0
  29. package/lib/lilylet/musicXmlUtils.js +469 -0
  30. package/lib/lilylet/parser.d.ts +3 -0
  31. package/lib/lilylet/parser.js +151 -0
  32. package/lib/lilylet/serializer.d.ts +11 -0
  33. package/lib/lilylet/serializer.js +653 -0
  34. package/lib/lilylet/types.d.ts +245 -0
  35. package/lib/lilylet/types.js +99 -0
  36. package/lib/lilypondDecoder.d.ts +1 -29
  37. package/lib/lilypondDecoder.js +1 -1006
  38. package/lib/lilypondEncoder.d.ts +1 -34
  39. package/lib/lilypondEncoder.js +1 -759
  40. package/lib/meiEncoder.d.ts +1 -8
  41. package/lib/meiEncoder.js +1 -1545
  42. package/lib/musicXmlDecoder.d.ts +1 -20
  43. package/lib/musicXmlDecoder.js +1 -1151
  44. package/lib/musicXmlEncoder.d.ts +1 -15
  45. package/lib/musicXmlEncoder.js +1 -666
  46. package/lib/musicXmlTypes.d.ts +1 -199
  47. package/lib/musicXmlTypes.js +1 -7
  48. package/lib/musicXmlUtils.d.ts +1 -81
  49. package/lib/musicXmlUtils.js +1 -435
  50. package/lib/parser.d.ts +1 -3
  51. package/lib/parser.js +1 -151
  52. package/lib/serializer.d.ts +1 -11
  53. package/lib/serializer.js +1 -650
  54. package/lib/types.d.ts +1 -244
  55. package/lib/types.js +1 -99
  56. package/package.json +2 -1
  57. package/source/abc/abc.jison +692 -0
  58. package/source/abc/abc.ts +176 -0
  59. package/source/abc/grammar.jison.js +1203 -0
  60. package/source/abc/parser.ts +12 -0
  61. package/source/lilylet/abcDecoder.ts +1121 -0
  62. package/source/lilylet/grammar.jison.js +170 -165
  63. package/source/lilylet/index.ts +4 -3
  64. package/source/lilylet/lilylet.jison +2 -0
  65. package/source/lilylet/lilypondDecoder.ts +91 -41
  66. package/source/lilylet/meiEncoder.ts +280 -0
  67. package/source/lilylet/musicXmlDecoder.ts +74 -27
  68. package/source/lilylet/musicXmlEncoder.ts +201 -146
  69. package/source/lilylet/musicXmlUtils.ts +46 -4
  70. package/source/lilylet/serializer.ts +3 -0
  71. package/source/lilylet/types.ts +1 -0
@@ -1,759 +1 @@
1
- /**
2
- * Lilylet to LilyPond Encoder
3
- *
4
- * Converts LilyletDoc to LilyPond (.ly) format.
5
- * Uses relative pitch mode matching LilyPond's default behavior.
6
- */
7
- import { Accidental, } from "./types.js";
8
- // === Constants and Mappings ===
9
- const PHONETS = "cdefgab";
10
- // Key signature to LilyPond notation (using English note names)
11
- const KEY_MAP = {
12
- c: { major: "c \\major", minor: "c \\minor" },
13
- d: { major: "d \\major", minor: "d \\minor" },
14
- e: { major: "e \\major", minor: "e \\minor" },
15
- f: { major: "f \\major", minor: "f \\minor" },
16
- g: { major: "g \\major", minor: "g \\minor" },
17
- a: { major: "a \\major", minor: "a \\minor" },
18
- b: { major: "b \\major", minor: "b \\minor" },
19
- };
20
- // Accidentals for key signatures
21
- const KEY_ACCIDENTAL_MAP = {
22
- sharp: "s",
23
- flat: "f",
24
- doubleSharp: "ss",
25
- doubleFlat: "ff",
26
- };
27
- // Clef names
28
- const CLEF_MAP = {
29
- treble: "treble",
30
- bass: "bass",
31
- alto: "alto",
32
- };
33
- // Accidental to LilyPond notation
34
- const ACCIDENTAL_MAP = {
35
- natural: "!",
36
- sharp: "s",
37
- flat: "f",
38
- doubleSharp: "ss",
39
- doubleFlat: "ff",
40
- };
41
- // Articulation to LilyPond notation
42
- const ARTICULATION_MAP = {
43
- staccato: "-.",
44
- staccatissimo: "-!",
45
- tenuto: "--",
46
- marcato: "-^",
47
- accent: "->",
48
- portato: "-_",
49
- };
50
- // Ornament to LilyPond notation
51
- const ORNAMENT_MAP = {
52
- trill: "\\trill",
53
- turn: "\\turn",
54
- mordent: "\\mordent",
55
- prall: "\\prall",
56
- fermata: "\\fermata",
57
- shortFermata: "\\shortfermata",
58
- arpeggio: "\\arpeggio",
59
- };
60
- // Dynamic to LilyPond notation
61
- const DYNAMIC_MAP = {
62
- ppp: "\\ppp",
63
- pp: "\\pp",
64
- p: "\\p",
65
- mp: "\\mp",
66
- mf: "\\mf",
67
- f: "\\f",
68
- ff: "\\ff",
69
- fff: "\\fff",
70
- sfz: "\\sfz",
71
- rfz: "\\rfz",
72
- };
73
- // Hairpin to LilyPond notation
74
- const HAIRPIN_MAP = {
75
- crescendoStart: "\\<",
76
- crescendoEnd: "\\!",
77
- diminuendoStart: "\\>",
78
- diminuendoEnd: "\\!",
79
- };
80
- // Pedal to LilyPond notation
81
- const PEDAL_MAP = {
82
- sustainOn: "\\sustainOn",
83
- sustainOff: "\\sustainOff",
84
- sostenutoOn: "\\sostenutoOn",
85
- sostenutoOff: "\\sostenutoOff",
86
- unaCordaOn: "\\unaCorda",
87
- unaCordaOff: "\\treCorde",
88
- };
89
- // Stem direction
90
- const STEM_MAP = {
91
- up: "\\stemUp",
92
- down: "\\stemDown",
93
- auto: "\\stemNeutral",
94
- };
95
- // Barline styles
96
- const BARLINE_MAP = {
97
- "|": "|",
98
- "||": "||",
99
- "|.": "|.",
100
- ".|:": ".|:",
101
- ":|.": ":|.",
102
- ":..:": ":..:",
103
- ":..:|": ":..:|",
104
- };
105
- // === Helper Functions ===
106
- /**
107
- * Generate a spacer rest that fills a measure based on time signature.
108
- * Uses multiplication syntax: s{denominator}*{numerator}
109
- * @param timeSig - Time signature { numerator, denominator }
110
- * @returns LilyPond spacer rest string (e.g., "s4*3" for 3/4, "s8*6" for 6/8)
111
- */
112
- const getSpacerRest = (timeSig) => {
113
- if (!timeSig)
114
- return 's1';
115
- const { numerator, denominator } = timeSig;
116
- return `s${denominator}*${numerator}`;
117
- };
118
- /**
119
- * Calculate the octave markers needed to serialize a pitch in relative mode.
120
- */
121
- const getRelativeOctaveMarkers = (env, pitch) => {
122
- const step = PHONETS.indexOf(pitch.phonet);
123
- if (step === -1) {
124
- return { markers: '', newEnv: env };
125
- }
126
- const interval = step - env.step;
127
- // Parser's octave adjustment calculation
128
- const octInc = Math.floor(Math.abs(interval) / 4) * -Math.sign(interval);
129
- // Without any markers, parser would calculate:
130
- const baseOctave = env.octave + octInc;
131
- // We need markers to reach pitch.octave from baseOctave
132
- const markerCount = pitch.octave - baseOctave;
133
- let markers = '';
134
- if (markerCount > 0) {
135
- markers = "'".repeat(markerCount);
136
- }
137
- else if (markerCount < 0) {
138
- markers = ",".repeat(-markerCount);
139
- }
140
- // Update environment
141
- const newEnv = {
142
- step: step,
143
- octave: pitch.octave
144
- };
145
- return { markers, newEnv };
146
- };
147
- const DEFAULT_OPTIONS = {
148
- paper: { width: 210, height: 297 }, // A4 size in mm
149
- fontSize: 20,
150
- withMIDI: false,
151
- autoBeaming: false,
152
- };
153
- // === Encoding Functions ===
154
- /**
155
- * Encode key signature to LilyPond
156
- */
157
- const encodeKey = (key) => {
158
- let keyStr = key.pitch;
159
- if (key.accidental) {
160
- keyStr += KEY_ACCIDENTAL_MAP[key.accidental] || '';
161
- }
162
- return `\\key ${keyStr} \\${key.mode}`;
163
- };
164
- /**
165
- * Encode time signature to LilyPond
166
- */
167
- const encodeTimeSig = (timeSig) => {
168
- if (timeSig.symbol === 'common') {
169
- return "\\time 4/4"; // LilyPond handles C automatically
170
- }
171
- if (timeSig.symbol === 'cut') {
172
- return "\\time 2/2"; // LilyPond handles C| automatically
173
- }
174
- return `\\time ${timeSig.numerator}/${timeSig.denominator}`;
175
- };
176
- /**
177
- * Encode clef to LilyPond
178
- */
179
- const encodeClef = (clef) => {
180
- return `\\clef ${CLEF_MAP[clef] || clef}`;
181
- };
182
- /**
183
- * Encode tempo to LilyPond
184
- */
185
- const encodeTempo = (tempo) => {
186
- let result = "\\tempo";
187
- if (tempo.text) {
188
- result += ` "${tempo.text}"`;
189
- }
190
- if (tempo.beat && tempo.bpm) {
191
- const beatValue = tempo.beat.division;
192
- let dots = "";
193
- if (tempo.beat.dots) {
194
- dots = ".".repeat(tempo.beat.dots);
195
- }
196
- result += ` ${beatValue}${dots} = ${tempo.bpm}`;
197
- }
198
- return result;
199
- };
200
- /**
201
- * Encode a single pitch in relative mode
202
- */
203
- const encodePitch = (pitch, env) => {
204
- let result = pitch.phonet;
205
- // Add accidental
206
- if (pitch.accidental && pitch.accidental !== Accidental.natural) {
207
- result += ACCIDENTAL_MAP[pitch.accidental] || '';
208
- }
209
- else if (pitch.accidental === Accidental.natural) {
210
- result += '!';
211
- }
212
- // Calculate relative octave markers
213
- const { markers, newEnv } = getRelativeOctaveMarkers(env, pitch);
214
- result += markers;
215
- return { str: result, newEnv };
216
- };
217
- /**
218
- * Encode duration to LilyPond
219
- */
220
- const encodeDuration = (duration) => {
221
- let result = String(duration.division);
222
- if (duration.dots) {
223
- result += ".".repeat(duration.dots);
224
- }
225
- return result;
226
- };
227
- /**
228
- * Encode marks (articulations, dynamics, etc.) to LilyPond
229
- */
230
- const encodeMarks = (marks) => {
231
- let result = '';
232
- for (const mark of marks) {
233
- switch (mark.markType) {
234
- case 'articulation':
235
- result += ARTICULATION_MAP[mark.type] || '';
236
- break;
237
- case 'ornament':
238
- result += ORNAMENT_MAP[mark.type] || '';
239
- break;
240
- case 'dynamic':
241
- result += DYNAMIC_MAP[mark.type] || '';
242
- break;
243
- case 'hairpin':
244
- result += HAIRPIN_MAP[mark.type] || '';
245
- break;
246
- case 'tie':
247
- if (mark.start) {
248
- result += '~';
249
- }
250
- break;
251
- case 'slur':
252
- result += mark.start ? '(' : ')';
253
- break;
254
- case 'beam':
255
- result += mark.start ? '[' : ']';
256
- break;
257
- case 'pedal':
258
- result += PEDAL_MAP[mark.type] || '';
259
- break;
260
- case 'fingering':
261
- result += `-${mark.finger}`;
262
- break;
263
- case 'navigation':
264
- if (mark.type === 'coda') {
265
- result += '\\coda';
266
- }
267
- else if (mark.type === 'segno') {
268
- result += '\\segno';
269
- }
270
- break;
271
- case 'markup':
272
- const placement = mark.placement === 'below' ? '_' : '^';
273
- result += `${placement}\\markup { ${mark.content} }`;
274
- break;
275
- }
276
- }
277
- return result;
278
- };
279
- /**
280
- * Encode a note event
281
- */
282
- const encodeNoteEvent = (event, env, lastDuration) => {
283
- let result = '';
284
- let newEnv = env;
285
- // Grace note
286
- if (event.grace) {
287
- result += '\\grace ';
288
- }
289
- // Stem direction
290
- if (event.stemDirection) {
291
- result += STEM_MAP[event.stemDirection] + ' ';
292
- }
293
- // Pitches (chord or single note)
294
- if (event.pitches.length > 1) {
295
- result += '<';
296
- const pitchStrs = [];
297
- for (const pitch of event.pitches) {
298
- const { str, newEnv: ne } = encodePitch(pitch, newEnv);
299
- pitchStrs.push(str);
300
- newEnv = ne;
301
- }
302
- result += pitchStrs.join(' ');
303
- result += '>';
304
- }
305
- else if (event.pitches.length === 1) {
306
- const { str, newEnv: ne } = encodePitch(event.pitches[0], newEnv);
307
- result += str;
308
- newEnv = ne;
309
- }
310
- // Duration (only if different from last)
311
- const needDuration = !lastDuration ||
312
- lastDuration.division !== event.duration.division ||
313
- lastDuration.dots !== event.duration.dots;
314
- if (needDuration) {
315
- result += encodeDuration(event.duration);
316
- }
317
- // Tremolo
318
- if (event.tremolo) {
319
- result += `:${event.tremolo}`;
320
- }
321
- // Marks
322
- if (event.marks) {
323
- result += encodeMarks(event.marks);
324
- }
325
- return { str: result, newEnv, newDuration: event.duration };
326
- };
327
- /**
328
- * Encode a rest event
329
- */
330
- const encodeRestEvent = (event, env, lastDuration) => {
331
- let result = '';
332
- // Rest type
333
- if (event.fullMeasure) {
334
- result += 'R';
335
- }
336
- else if (event.invisible) {
337
- result += 's';
338
- }
339
- else {
340
- result += 'r';
341
- }
342
- // Duration
343
- const needDuration = !lastDuration ||
344
- lastDuration.division !== event.duration.division ||
345
- lastDuration.dots !== event.duration.dots;
346
- if (needDuration) {
347
- result += encodeDuration(event.duration);
348
- }
349
- // Positioned rest
350
- if (event.pitch && !event.fullMeasure && !event.invisible) {
351
- const { str } = encodePitch(event.pitch, env);
352
- result = str + result.slice(1); // Replace 'r' with pitch
353
- result += '\\rest';
354
- }
355
- return { str: result, newEnv: env, newDuration: event.duration };
356
- };
357
- /**
358
- * Encode a context change event
359
- */
360
- const encodeContextChange = (event) => {
361
- const parts = [];
362
- if (event.key) {
363
- parts.push(encodeKey(event.key));
364
- }
365
- if (event.time) {
366
- parts.push(encodeTimeSig(event.time));
367
- }
368
- if (event.clef) {
369
- parts.push(encodeClef(event.clef));
370
- }
371
- if (event.ottava !== undefined) {
372
- parts.push(`\\ottava #${event.ottava}`);
373
- }
374
- if (event.stemDirection) {
375
- parts.push(STEM_MAP[event.stemDirection]);
376
- }
377
- if (event.tempo) {
378
- parts.push(encodeTempo(event.tempo));
379
- }
380
- if (event.staff) {
381
- parts.push(`\\change Staff = "${event.staff}"`);
382
- }
383
- return parts.join(' ');
384
- };
385
- /**
386
- * Encode a tuplet event
387
- */
388
- const encodeTupletEvent = (event, env, lastDuration) => {
389
- let result = `\\tuplet ${event.ratio.denominator}/${event.ratio.numerator} { `;
390
- let newEnv = env;
391
- let newDuration = lastDuration;
392
- for (const subEvent of event.events) {
393
- if (subEvent.type === 'note') {
394
- const { str, newEnv: ne, newDuration: nd } = encodeNoteEvent(subEvent, newEnv, newDuration);
395
- result += str + ' ';
396
- newEnv = ne;
397
- newDuration = nd;
398
- }
399
- else if (subEvent.type === 'rest') {
400
- const { str, newDuration: nd } = encodeRestEvent(subEvent, newEnv, newDuration);
401
- result += str + ' ';
402
- newDuration = nd;
403
- }
404
- }
405
- result += '}';
406
- return { str: result, newEnv, newDuration };
407
- };
408
- /**
409
- * Encode a tremolo event
410
- */
411
- const encodeTremoloEvent = (event, env) => {
412
- let newEnv = env;
413
- // First chord/note
414
- let pitchA = '';
415
- if (event.pitchA.length > 1) {
416
- pitchA += '<';
417
- const pitchStrs = [];
418
- for (const pitch of event.pitchA) {
419
- const { str, newEnv: ne } = encodePitch(pitch, newEnv);
420
- pitchStrs.push(str);
421
- newEnv = ne;
422
- }
423
- pitchA += pitchStrs.join(' ');
424
- pitchA += '>';
425
- }
426
- else if (event.pitchA.length === 1) {
427
- const { str, newEnv: ne } = encodePitch(event.pitchA[0], newEnv);
428
- pitchA += str;
429
- newEnv = ne;
430
- }
431
- // Second chord/note
432
- let pitchB = '';
433
- if (event.pitchB.length > 1) {
434
- pitchB += '<';
435
- const pitchStrs = [];
436
- for (const pitch of event.pitchB) {
437
- const { str, newEnv: ne } = encodePitch(pitch, newEnv);
438
- pitchStrs.push(str);
439
- newEnv = ne;
440
- }
441
- pitchB += pitchStrs.join(' ');
442
- pitchB += '>';
443
- }
444
- else if (event.pitchB.length === 1) {
445
- const { str, newEnv: ne } = encodePitch(event.pitchB[0], newEnv);
446
- pitchB += str;
447
- newEnv = ne;
448
- }
449
- const result = `\\repeat tremolo ${event.count} { ${pitchA}${event.division} ${pitchB}${event.division} }`;
450
- return { str: result, newEnv };
451
- };
452
- /**
453
- * Encode a barline event
454
- */
455
- const encodeBarlineEvent = (event) => {
456
- const style = BARLINE_MAP[event.style] || event.style;
457
- if (style === '|') {
458
- return ''; // Default barline, no need to encode
459
- }
460
- return `\\bar "${style}"`;
461
- };
462
- /**
463
- * Encode a harmony event (chord symbol)
464
- * Uses the lilylet extension: \chords "text" (parsed by modified lotus grammar)
465
- */
466
- const encodeHarmonyEvent = (event) => {
467
- return `\\chords "${event.text}"`;
468
- };
469
- /**
470
- * Encode a markup event
471
- */
472
- const encodeMarkupEvent = (event) => {
473
- const placement = event.placement === 'below' ? '_' : '^';
474
- return `${placement}\\markup { ${event.content} }`;
475
- };
476
- /**
477
- * Encode a voice to LilyPond
478
- */
479
- const encodeVoice = (voice, measureContext, voiceIndex) => {
480
- let result = '';
481
- let env = { step: 0, octave: 0 }; // Start at middle C
482
- let lastDuration = null;
483
- for (const event of voice.events) {
484
- switch (event.type) {
485
- case 'note': {
486
- const { str, newEnv, newDuration } = encodeNoteEvent(event, env, lastDuration);
487
- result += str + ' ';
488
- env = newEnv;
489
- lastDuration = newDuration;
490
- break;
491
- }
492
- case 'rest': {
493
- const { str, newDuration } = encodeRestEvent(event, env, lastDuration);
494
- result += str + ' ';
495
- lastDuration = newDuration;
496
- break;
497
- }
498
- case 'context': {
499
- result += encodeContextChange(event) + ' ';
500
- break;
501
- }
502
- case 'tuplet': {
503
- const { str, newEnv, newDuration } = encodeTupletEvent(event, env, lastDuration);
504
- result += str + ' ';
505
- env = newEnv;
506
- lastDuration = newDuration;
507
- break;
508
- }
509
- case 'tremolo': {
510
- const { str, newEnv } = encodeTremoloEvent(event, env);
511
- result += str + ' ';
512
- env = newEnv;
513
- break;
514
- }
515
- case 'barline': {
516
- const str = encodeBarlineEvent(event);
517
- if (str) {
518
- result += str + ' ';
519
- }
520
- break;
521
- }
522
- case 'harmony': {
523
- result += encodeHarmonyEvent(event) + ' ';
524
- break;
525
- }
526
- case 'markup': {
527
- result += encodeMarkupEvent(event) + ' ';
528
- break;
529
- }
530
- case 'pitchReset': {
531
- env = { step: 0, octave: 0 };
532
- break;
533
- }
534
- }
535
- }
536
- return result.trim();
537
- };
538
- /**
539
- * Encode metadata to LilyPond header block
540
- */
541
- const encodeMetadata = (metadata) => {
542
- const entries = [];
543
- if (metadata.title) {
544
- entries.push(` title = "${metadata.title}"`);
545
- }
546
- if (metadata.subtitle) {
547
- entries.push(` subtitle = "${metadata.subtitle}"`);
548
- }
549
- if (metadata.composer) {
550
- entries.push(` composer = "${metadata.composer}"`);
551
- }
552
- if (metadata.arranger) {
553
- entries.push(` arranger = "${metadata.arranger}"`);
554
- }
555
- if (metadata.lyricist) {
556
- entries.push(` poet = "${metadata.lyricist}"`);
557
- }
558
- if (metadata.opus) {
559
- entries.push(` opus = "${metadata.opus}"`);
560
- }
561
- if (metadata.instrument) {
562
- entries.push(` instrument = "${metadata.instrument}"`);
563
- }
564
- entries.push(' tagline = ##f');
565
- return entries.join('\n');
566
- };
567
- /**
568
- * Encode a complete LilyletDoc to LilyPond format
569
- *
570
- * Structure:
571
- * - Multiple parts → outer <<>>
572
- * - Part with multiple staves → GrandStaff
573
- * - Part with single staff → standalone Staff
574
- */
575
- export const encode = (doc, options = {}) => {
576
- const opts = { ...DEFAULT_OPTIONS, ...options };
577
- // Filter out trailing empty measures (measures with no musical content)
578
- const hasMusicContent = (measure) => {
579
- for (const part of measure.parts) {
580
- for (const voice of part.voices) {
581
- for (const event of voice.events) {
582
- if (event.type === 'note' || event.type === 'rest' || event.type === 'tuplet' || event.type === 'tremolo') {
583
- return true;
584
- }
585
- }
586
- }
587
- }
588
- return false;
589
- };
590
- // Trim trailing empty measures
591
- let measureCount = doc.measures.length;
592
- while (measureCount > 0 && !hasMusicContent(doc.measures[measureCount - 1])) {
593
- measureCount--;
594
- }
595
- const measures = doc.measures.slice(0, measureCount);
596
- // Determine number of parts from the document
597
- const partCount = Math.max(...measures.map(m => m.parts.length), 1);
598
- const partVoices = [];
599
- for (let pi = 0; pi < partCount; pi++) {
600
- partVoices.push(new Map());
601
- }
602
- // Track time signature for each measure (for spacer rests)
603
- const measureTimeSigs = [];
604
- let currentKey;
605
- let currentTimeSig;
606
- for (let mi = 0; mi < measures.length; mi++) {
607
- const measure = measures[mi];
608
- // Update context from measure
609
- if (measure.key)
610
- currentKey = measure.key;
611
- if (measure.timeSig)
612
- currentTimeSig = measure.timeSig;
613
- // Store time signature for this measure
614
- measureTimeSigs[mi] = currentTimeSig;
615
- // Process each part
616
- for (let pi = 0; pi < measure.parts.length; pi++) {
617
- const part = measure.parts[pi];
618
- const staffMap = partVoices[pi];
619
- for (let vi = 0; vi < part.voices.length; vi++) {
620
- const voice = part.voices[vi];
621
- const staff = voice.staff || 1;
622
- if (!staffMap.has(staff)) {
623
- staffMap.set(staff, []);
624
- }
625
- const staffMeasures = staffMap.get(staff);
626
- // Ensure we have enough measure slots
627
- while (staffMeasures.length <= mi) {
628
- staffMeasures.push([]);
629
- }
630
- // Encode voice content
631
- const voiceContent = encodeVoice(voice, {
632
- key: currentKey,
633
- timeSig: currentTimeSig,
634
- isFirst: mi === 0
635
- }, vi);
636
- staffMeasures[mi].push(voiceContent);
637
- }
638
- }
639
- }
640
- // Build a staff string (used for both GrandStaff children and standalone Staff)
641
- // Staff ID format: "partIndex_staffIndex" (e.g., "1_1", "1_2", "2_1") for multi-part decoding
642
- const buildStaffString = (measures, staffId, indent) => {
643
- // Find max voices per measure for this staff
644
- const maxVoices = Math.max(...measures.map(m => m.length), 1);
645
- // Build voice lines
646
- const voiceLines = [];
647
- for (let vi = 0; vi < maxVoices; vi++) {
648
- const measureContents = measures.map((m, mi) => {
649
- // Use correct spacer rest based on time signature
650
- const spacer = getSpacerRest(measureTimeSigs[mi]);
651
- const content = m[vi] || spacer;
652
- // Wrap each measure in its own \relative c' to reset pitch context
653
- return `${indent} \\relative c' { ${content} } | % ${mi + 1}`;
654
- });
655
- voiceLines.push(`${indent} \\new Voice {\n${measureContents.join('\n')}\n${indent} }`);
656
- }
657
- return `${indent}\\new Staff = "${staffId}" <<\n${voiceLines.join('\n')}\n${indent}>>`;
658
- };
659
- // Build music content for each part
660
- const partStrings = [];
661
- for (let pi = 0; pi < partCount; pi++) {
662
- const staffMap = partVoices[pi];
663
- const staffNums = Array.from(staffMap.keys()).sort((a, b) => a - b);
664
- if (staffNums.length === 0) {
665
- // Empty part, skip
666
- continue;
667
- }
668
- const partIndex = pi + 1; // 1-based part index
669
- if (staffNums.length === 1) {
670
- // Single staff part → standalone Staff
671
- const staffNum = staffNums[0];
672
- const measures = staffMap.get(staffNum);
673
- const staffId = `${partIndex}_${staffNum}`;
674
- const staffStr = buildStaffString(measures, staffId, ' ');
675
- partStrings.push(staffStr);
676
- }
677
- else {
678
- // Multiple staves → GrandStaff
679
- const staffStrings = [];
680
- for (const staffNum of staffNums) {
681
- const measures = staffMap.get(staffNum);
682
- const staffId = `${partIndex}_${staffNum}`;
683
- const staffStr = buildStaffString(measures, staffId, ' ');
684
- staffStrings.push(staffStr);
685
- }
686
- partStrings.push(` \\new GrandStaff <<\n${staffStrings.join('\n')}\n >>`);
687
- }
688
- }
689
- const musicContent = partStrings.join('\n');
690
- // Determine outer wrapper
691
- // - Single part with single staff → just Staff (no outer <<>>)
692
- // - Single part with multiple staves → GrandStaff (no extra outer <<>>)
693
- // - Multiple parts → outer <<>>
694
- let scoreContent;
695
- if (partCount === 1 && partStrings.length === 1) {
696
- // Single part - use as-is (already has Staff or GrandStaff)
697
- scoreContent = musicContent;
698
- }
699
- else {
700
- // Multiple parts - wrap in <<>>
701
- scoreContent = ` <<\n${musicContent}\n >>`;
702
- }
703
- // Build header
704
- const headerContent = doc.metadata ? encodeMetadata(doc.metadata) : ' tagline = ##f';
705
- // Build document
706
- const paperWidth = typeof opts.paper?.width === 'number' ? `${opts.paper.width}\\mm` : opts.paper?.width || '210\\mm';
707
- const paperHeight = typeof opts.paper?.height === 'number' ? `${opts.paper.height}\\mm` : opts.paper?.height || '297\\mm';
708
- const lyDoc = `\\version "2.22.0"
709
-
710
- \\language "english"
711
-
712
- \\header {
713
- ${headerContent}
714
- }
715
-
716
- #(set-global-staff-size ${opts.fontSize})
717
-
718
- \\paper {
719
- paper-width = ${paperWidth}
720
- paper-height = ${paperHeight}
721
- ragged-last = ##t
722
- ragged-last-bottom = ##f
723
- }
724
-
725
- \\layout {
726
- \\context {
727
- \\Score
728
- autoBeaming = ##${opts.autoBeaming ? 't' : 'f'}
729
- }
730
- }
731
-
732
- \\score {
733
- ${scoreContent}
734
-
735
- \\layout { }${opts.withMIDI ? '\n \\midi { }' : ''}
736
- }
737
- `;
738
- return lyDoc;
739
- };
740
- /**
741
- * Encode LilyletDoc to minimal LilyPond (music content only, no headers)
742
- */
743
- export const encodeMinimal = (doc) => {
744
- const parts = [];
745
- for (const measure of doc.measures) {
746
- for (const part of measure.parts) {
747
- for (const voice of part.voices) {
748
- const content = encodeVoice(voice, { isFirst: false }, 0);
749
- parts.push(content);
750
- }
751
- }
752
- parts.push('|');
753
- }
754
- return parts.join(' ');
755
- };
756
- export default {
757
- encode,
758
- encodeMinimal,
759
- };
1
+ export * from "./lilylet/lilypondEncoder.js";