@k-l-lambda/lilylet 0.1.50 → 0.1.51
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/lilylet/grammar.jison.js +98 -98
- package/lib/lilylet/meiEncoder.js +5 -0
- package/lib/lilylet/serializer.js +69 -20
- package/package.json +1 -1
- package/source/lilylet/grammar.jison.js +98 -98
- package/source/lilylet/lilylet.jison +8 -3
- package/source/lilylet/meiEncoder.ts +4 -0
- package/source/lilylet/serializer.ts +72 -21
|
@@ -190,6 +190,9 @@ const serializeMarks = (marks) => {
|
|
|
190
190
|
parts.push(pedalStr);
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
|
+
case 'fingering':
|
|
194
|
+
parts.push('-' + mark.finger);
|
|
195
|
+
break;
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
198
|
return parts.join('');
|
|
@@ -449,8 +452,9 @@ const findVoiceClef = (voice) => {
|
|
|
449
452
|
// Takes currentStaff (what parser thinks staff is) and returns { str, newStaff }
|
|
450
453
|
// If isGrandStaff is true, always output \staff command for clarity
|
|
451
454
|
// measureContext provides key/time for first voice
|
|
452
|
-
//
|
|
453
|
-
|
|
455
|
+
// allStaffClefs is the clef map for all staves (tracked across measures)
|
|
456
|
+
// emittedClefs tracks which clefs have already been output (avoids duplicates)
|
|
457
|
+
const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContext, isFirstVoice = false, allStaffClefs, emittedClefs) => {
|
|
454
458
|
const parts = [];
|
|
455
459
|
let prevDuration;
|
|
456
460
|
// Each voice starts fresh from middle C (step=0, octave=0)
|
|
@@ -480,22 +484,61 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
480
484
|
parts.push('\\time ' + numerator + '/' + denominator);
|
|
481
485
|
}
|
|
482
486
|
}
|
|
483
|
-
// Output clef
|
|
484
|
-
const voiceClef =
|
|
485
|
-
|
|
487
|
+
// Output clef only if not yet emitted or changed for this staff
|
|
488
|
+
const voiceClef = allStaffClefs?.[voice.staff] || findVoiceClef(voice);
|
|
489
|
+
const clefAlreadyEmitted = voiceClef && emittedClefs?.[voice.staff] === voiceClef;
|
|
490
|
+
if (voiceClef && !clefAlreadyEmitted) {
|
|
486
491
|
parts.push('\\clef "' + CLEF_MAP[voiceClef] + '"');
|
|
492
|
+
if (emittedClefs)
|
|
493
|
+
emittedClefs[voice.staff] = voiceClef;
|
|
487
494
|
}
|
|
488
|
-
//
|
|
489
|
-
|
|
495
|
+
// Skip redundant clef context events if this staff's clef is already established
|
|
496
|
+
const clefOutputted = !!voiceClef && !!emittedClefs?.[voice.staff];
|
|
497
|
+
let activeStaff = voice.staff;
|
|
498
|
+
let activeStemDir;
|
|
490
499
|
for (const event of voice.events) {
|
|
491
|
-
|
|
492
|
-
if (clefOutputted && event.type === 'context') {
|
|
500
|
+
if (event.type === 'context') {
|
|
493
501
|
const ctx = event;
|
|
494
|
-
|
|
495
|
-
|
|
502
|
+
// Skip context events that belong to a different staff (cross-staff clef/ottava)
|
|
503
|
+
if (ctx.staff && ctx.staff !== voice.staff) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
// Skip clef-only context events if clef already established for this staff
|
|
507
|
+
if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
|
|
496
508
|
continue;
|
|
497
509
|
}
|
|
498
510
|
}
|
|
511
|
+
if (event.type === 'note') {
|
|
512
|
+
const noteEvt = event;
|
|
513
|
+
// Cross-staff: emit \staff when note's effective staff differs from active
|
|
514
|
+
const effectiveStaff = noteEvt.staff || voice.staff;
|
|
515
|
+
if (effectiveStaff !== activeStaff) {
|
|
516
|
+
activeStaff = effectiveStaff;
|
|
517
|
+
parts.push('\\staff "' + activeStaff + '"');
|
|
518
|
+
// Emit the target staff's clef if it differs from what was last emitted for this staff
|
|
519
|
+
const targetClef = allStaffClefs?.[activeStaff];
|
|
520
|
+
if (targetClef && emittedClefs?.[activeStaff] !== targetClef) {
|
|
521
|
+
parts.push('\\clef "' + CLEF_MAP[targetClef] + '"');
|
|
522
|
+
if (emittedClefs)
|
|
523
|
+
emittedClefs[activeStaff] = targetClef;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Stem direction: emit \stemUp/\stemDown/\stemNeutral on change
|
|
527
|
+
const stemDir = noteEvt.stemDirection;
|
|
528
|
+
if (stemDir !== activeStemDir) {
|
|
529
|
+
if (stemDir === StemDirection.up) {
|
|
530
|
+
parts.push('\\stemUp');
|
|
531
|
+
}
|
|
532
|
+
else if (stemDir === StemDirection.down) {
|
|
533
|
+
parts.push('\\stemDown');
|
|
534
|
+
}
|
|
535
|
+
else if (activeStemDir) {
|
|
536
|
+
// Was set, now undefined → reset to neutral
|
|
537
|
+
parts.push('\\stemNeutral');
|
|
538
|
+
}
|
|
539
|
+
activeStemDir = stemDir;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
499
542
|
const { str: eventStr, newEnv } = serializeEvent(event, pitchEnv, prevDuration);
|
|
500
543
|
pitchEnv = newEnv;
|
|
501
544
|
if (eventStr) {
|
|
@@ -508,12 +551,16 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
508
551
|
else if (event.type === 'rest') {
|
|
509
552
|
prevDuration = event.duration;
|
|
510
553
|
}
|
|
554
|
+
else if (event.type === 'context' && event.clef && emittedClefs) {
|
|
555
|
+
const ctx = event;
|
|
556
|
+
emittedClefs[ctx.staff || activeStaff] = ctx.clef;
|
|
557
|
+
}
|
|
511
558
|
}
|
|
512
559
|
return { str: parts.join(' '), newStaff: voice.staff };
|
|
513
560
|
};
|
|
514
561
|
// Serialize a part, tracking staff state across voices
|
|
515
562
|
// measureContext is passed to all voices (for clef), but key/time only to first voice
|
|
516
|
-
const serializePart = (part, currentStaff, isGrandStaff = false, measureContext, isFirstPart = false, clefsByStaff) => {
|
|
563
|
+
const serializePart = (part, currentStaff, isGrandStaff = false, measureContext, isFirstPart = false, clefsByStaff, emittedClefs) => {
|
|
517
564
|
if (part.voices.length === 0) {
|
|
518
565
|
return { str: '', newStaff: currentStaff };
|
|
519
566
|
}
|
|
@@ -522,10 +569,8 @@ const serializePart = (part, currentStaff, isGrandStaff = false, measureContext,
|
|
|
522
569
|
for (let i = 0; i < part.voices.length; i++) {
|
|
523
570
|
const voice = part.voices[i];
|
|
524
571
|
// Pass measureContext to all voices, isFirstVoice for key/time
|
|
525
|
-
// Pass staff clef from clefsByStaff map
|
|
526
572
|
const isFirstVoice = isFirstPart && i === 0;
|
|
527
|
-
const
|
|
528
|
-
const { str, newStaff } = serializeVoice(voice, staff, isGrandStaff, measureContext, isFirstVoice, staffClef);
|
|
573
|
+
const { str, newStaff } = serializeVoice(voice, staff, isGrandStaff, measureContext, isFirstVoice, clefsByStaff, emittedClefs);
|
|
529
574
|
voiceStrs.push(str);
|
|
530
575
|
staff = newStaff;
|
|
531
576
|
}
|
|
@@ -534,7 +579,7 @@ const serializePart = (part, currentStaff, isGrandStaff = false, measureContext,
|
|
|
534
579
|
};
|
|
535
580
|
// Serialize a measure, tracking staff state across parts
|
|
536
581
|
// Always output key/time at start of each measure
|
|
537
|
-
const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false, currentKey, currentTime, staffClefs) => {
|
|
582
|
+
const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false, currentKey, currentTime, staffClefs, emittedClefs) => {
|
|
538
583
|
const parts = [];
|
|
539
584
|
// Build measure context for all voices (key/time)
|
|
540
585
|
// Key and time are written to first voice, clef to all voices based on staff
|
|
@@ -548,7 +593,7 @@ const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false,
|
|
|
548
593
|
// Parts
|
|
549
594
|
let staff = currentStaff;
|
|
550
595
|
if (measure.parts.length === 1) {
|
|
551
|
-
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true, clefsByStaff);
|
|
596
|
+
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true, clefsByStaff, emittedClefs);
|
|
552
597
|
if (partStr) {
|
|
553
598
|
parts.push(partStr);
|
|
554
599
|
}
|
|
@@ -560,7 +605,7 @@ const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false,
|
|
|
560
605
|
for (let i = 0; i < measure.parts.length; i++) {
|
|
561
606
|
const part = measure.parts[i];
|
|
562
607
|
// Pass measureContext to all parts, isFirstPart to first part only
|
|
563
|
-
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0, clefsByStaff);
|
|
608
|
+
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0, clefsByStaff, emittedClefs);
|
|
564
609
|
if (str) {
|
|
565
610
|
partStrs.push(str);
|
|
566
611
|
}
|
|
@@ -620,6 +665,7 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
620
665
|
let currentKey;
|
|
621
666
|
let currentTime;
|
|
622
667
|
const staffClefs = {}; // Track clef per staff
|
|
668
|
+
const emittedClefs = {}; // Track which clefs have been output
|
|
623
669
|
for (let i = 0; i < doc.measures.length; i++) {
|
|
624
670
|
const measure = doc.measures[i];
|
|
625
671
|
// Update current key/time if measure has them
|
|
@@ -634,12 +680,15 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
634
680
|
for (const voice of part.voices) {
|
|
635
681
|
for (const event of voice.events) {
|
|
636
682
|
if (event.type === 'context' && event.clef) {
|
|
637
|
-
|
|
683
|
+
const ctx = event;
|
|
684
|
+
// Use the event's staff if specified (cross-staff), otherwise the voice's staff
|
|
685
|
+
const clefStaff = ctx.staff || voice.staff;
|
|
686
|
+
staffClefs[clefStaff] = ctx.clef;
|
|
638
687
|
}
|
|
639
688
|
}
|
|
640
689
|
}
|
|
641
690
|
}
|
|
642
|
-
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime, staffClefs);
|
|
691
|
+
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime, staffClefs, emittedClefs);
|
|
643
692
|
// Always include measure, even if empty (use space rest for empty measures)
|
|
644
693
|
measureStrs.push(measureStr || 's1');
|
|
645
694
|
currentStaff = newStaff;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k-l-lambda/lilylet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|