@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
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
PedalType,
|
|
32
32
|
Tempo,
|
|
33
33
|
Placement,
|
|
34
|
+
Metadata,
|
|
34
35
|
} from "./types";
|
|
35
36
|
|
|
36
37
|
|
|
@@ -259,6 +260,9 @@ const serializeMarks = (marks: Mark[]): string => {
|
|
|
259
260
|
if (pedalStr) parts.push(pedalStr);
|
|
260
261
|
break;
|
|
261
262
|
}
|
|
263
|
+
case 'fingering':
|
|
264
|
+
parts.push('-' + mark.finger);
|
|
265
|
+
break;
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
268
|
|
|
@@ -584,14 +588,16 @@ const findVoiceClef = (voice: Voice): Clef | undefined => {
|
|
|
584
588
|
// Takes currentStaff (what parser thinks staff is) and returns { str, newStaff }
|
|
585
589
|
// If isGrandStaff is true, always output \staff command for clarity
|
|
586
590
|
// measureContext provides key/time for first voice
|
|
587
|
-
//
|
|
591
|
+
// allStaffClefs is the clef map for all staves (tracked across measures)
|
|
592
|
+
// emittedClefs tracks which clefs have already been output (avoids duplicates)
|
|
588
593
|
const serializeVoice = (
|
|
589
594
|
voice: Voice,
|
|
590
595
|
currentStaff: number,
|
|
591
596
|
isGrandStaff: boolean = false,
|
|
592
597
|
measureContext?: MeasureContext,
|
|
593
598
|
isFirstVoice: boolean = false,
|
|
594
|
-
|
|
599
|
+
allStaffClefs?: Record<number, Clef>,
|
|
600
|
+
emittedClefs?: Record<number, Clef>
|
|
595
601
|
): { str: string; newStaff: number } => {
|
|
596
602
|
const parts: string[] = [];
|
|
597
603
|
let prevDuration: Duration | undefined;
|
|
@@ -625,25 +631,63 @@ const serializeVoice = (
|
|
|
625
631
|
}
|
|
626
632
|
}
|
|
627
633
|
|
|
628
|
-
// Output clef
|
|
629
|
-
const voiceClef =
|
|
630
|
-
|
|
634
|
+
// Output clef only if not yet emitted or changed for this staff
|
|
635
|
+
const voiceClef = allStaffClefs?.[voice.staff] || findVoiceClef(voice);
|
|
636
|
+
const clefAlreadyEmitted = voiceClef && emittedClefs?.[voice.staff] === voiceClef;
|
|
637
|
+
if (voiceClef && !clefAlreadyEmitted) {
|
|
631
638
|
parts.push('\\clef "' + CLEF_MAP[voiceClef] + '"');
|
|
639
|
+
if (emittedClefs) emittedClefs[voice.staff] = voiceClef;
|
|
632
640
|
}
|
|
641
|
+
// Skip redundant clef context events if this staff's clef is already established
|
|
642
|
+
const clefOutputted = !!voiceClef && !!emittedClefs?.[voice.staff];
|
|
633
643
|
|
|
634
|
-
|
|
635
|
-
let
|
|
644
|
+
let activeStaff = voice.staff;
|
|
645
|
+
let activeStemDir: StemDirection | undefined;
|
|
636
646
|
|
|
637
647
|
for (const event of voice.events) {
|
|
638
|
-
|
|
639
|
-
if (clefOutputted && event.type === 'context') {
|
|
648
|
+
if (event.type === 'context') {
|
|
640
649
|
const ctx = event as ContextChange;
|
|
641
|
-
|
|
642
|
-
|
|
650
|
+
// Skip context events that belong to a different staff (cross-staff clef/ottava)
|
|
651
|
+
if (ctx.staff && ctx.staff !== voice.staff) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
// Skip clef-only context events if clef already established for this staff
|
|
655
|
+
if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
|
|
643
656
|
continue;
|
|
644
657
|
}
|
|
645
658
|
}
|
|
646
659
|
|
|
660
|
+
if (event.type === 'note') {
|
|
661
|
+
const noteEvt = event as NoteEvent;
|
|
662
|
+
|
|
663
|
+
// Cross-staff: emit \staff when note's effective staff differs from active
|
|
664
|
+
const effectiveStaff = noteEvt.staff || voice.staff;
|
|
665
|
+
if (effectiveStaff !== activeStaff) {
|
|
666
|
+
activeStaff = effectiveStaff;
|
|
667
|
+
parts.push('\\staff "' + activeStaff + '"');
|
|
668
|
+
// Emit the target staff's clef if it differs from what was last emitted for this staff
|
|
669
|
+
const targetClef = allStaffClefs?.[activeStaff];
|
|
670
|
+
if (targetClef && emittedClefs?.[activeStaff] !== targetClef) {
|
|
671
|
+
parts.push('\\clef "' + CLEF_MAP[targetClef] + '"');
|
|
672
|
+
if (emittedClefs) emittedClefs[activeStaff] = targetClef;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Stem direction: emit \stemUp/\stemDown/\stemNeutral on change
|
|
677
|
+
const stemDir = noteEvt.stemDirection;
|
|
678
|
+
if (stemDir !== activeStemDir) {
|
|
679
|
+
if (stemDir === StemDirection.up) {
|
|
680
|
+
parts.push('\\stemUp');
|
|
681
|
+
} else if (stemDir === StemDirection.down) {
|
|
682
|
+
parts.push('\\stemDown');
|
|
683
|
+
} else if (activeStemDir) {
|
|
684
|
+
// Was set, now undefined → reset to neutral
|
|
685
|
+
parts.push('\\stemNeutral');
|
|
686
|
+
}
|
|
687
|
+
activeStemDir = stemDir;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
647
691
|
const { str: eventStr, newEnv } = serializeEvent(event, pitchEnv, prevDuration);
|
|
648
692
|
pitchEnv = newEnv;
|
|
649
693
|
|
|
@@ -656,6 +700,9 @@ const serializeVoice = (
|
|
|
656
700
|
prevDuration = (event as NoteEvent).duration;
|
|
657
701
|
} else if (event.type === 'rest') {
|
|
658
702
|
prevDuration = (event as RestEvent).duration;
|
|
703
|
+
} else if (event.type === 'context' && (event as ContextChange).clef && emittedClefs) {
|
|
704
|
+
const ctx = event as ContextChange;
|
|
705
|
+
emittedClefs[ctx.staff || activeStaff] = ctx.clef!;
|
|
659
706
|
}
|
|
660
707
|
}
|
|
661
708
|
|
|
@@ -671,7 +718,8 @@ const serializePart = (
|
|
|
671
718
|
isGrandStaff: boolean = false,
|
|
672
719
|
measureContext?: MeasureContext,
|
|
673
720
|
isFirstPart: boolean = false,
|
|
674
|
-
clefsByStaff?: Record<number, Clef
|
|
721
|
+
clefsByStaff?: Record<number, Clef>,
|
|
722
|
+
emittedClefs?: Record<number, Clef>
|
|
675
723
|
): { str: string; newStaff: number } => {
|
|
676
724
|
if (part.voices.length === 0) {
|
|
677
725
|
return { str: '', newStaff: currentStaff };
|
|
@@ -683,10 +731,8 @@ const serializePart = (
|
|
|
683
731
|
for (let i = 0; i < part.voices.length; i++) {
|
|
684
732
|
const voice = part.voices[i];
|
|
685
733
|
// Pass measureContext to all voices, isFirstVoice for key/time
|
|
686
|
-
// Pass staff clef from clefsByStaff map
|
|
687
734
|
const isFirstVoice = isFirstPart && i === 0;
|
|
688
|
-
const
|
|
689
|
-
const { str, newStaff } = serializeVoice(voice, staff, isGrandStaff, measureContext, isFirstVoice, staffClef);
|
|
735
|
+
const { str, newStaff } = serializeVoice(voice, staff, isGrandStaff, measureContext, isFirstVoice, clefsByStaff, emittedClefs);
|
|
690
736
|
voiceStrs.push(str);
|
|
691
737
|
staff = newStaff;
|
|
692
738
|
}
|
|
@@ -705,7 +751,8 @@ const serializeMeasure = (
|
|
|
705
751
|
isGrandStaff: boolean = false,
|
|
706
752
|
currentKey?: KeySignature,
|
|
707
753
|
currentTime?: { numerator: number; denominator: number; symbol?: 'common' | 'cut' },
|
|
708
|
-
staffClefs?: Record<number, Clef
|
|
754
|
+
staffClefs?: Record<number, Clef>,
|
|
755
|
+
emittedClefs?: Record<number, Clef>
|
|
709
756
|
): { str: string; newStaff: number } => {
|
|
710
757
|
const parts: string[] = [];
|
|
711
758
|
|
|
@@ -723,7 +770,7 @@ const serializeMeasure = (
|
|
|
723
770
|
// Parts
|
|
724
771
|
let staff = currentStaff;
|
|
725
772
|
if (measure.parts.length === 1) {
|
|
726
|
-
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true, clefsByStaff);
|
|
773
|
+
const { str: partStr, newStaff } = serializePart(measure.parts[0], staff, isGrandStaff, measureContext, true, clefsByStaff, emittedClefs);
|
|
727
774
|
if (partStr) {
|
|
728
775
|
parts.push(partStr);
|
|
729
776
|
}
|
|
@@ -734,7 +781,7 @@ const serializeMeasure = (
|
|
|
734
781
|
for (let i = 0; i < measure.parts.length; i++) {
|
|
735
782
|
const part = measure.parts[i];
|
|
736
783
|
// Pass measureContext to all parts, isFirstPart to first part only
|
|
737
|
-
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0, clefsByStaff);
|
|
784
|
+
const { str, newStaff } = serializePart(part, staff, isGrandStaff, measureContext, i === 0, clefsByStaff, emittedClefs);
|
|
738
785
|
if (str) {
|
|
739
786
|
partStrs.push(str);
|
|
740
787
|
}
|
|
@@ -753,7 +800,7 @@ const escapeString = (str: string): string => {
|
|
|
753
800
|
};
|
|
754
801
|
|
|
755
802
|
// Serialize metadata
|
|
756
|
-
const serializeMetadata = (metadata:
|
|
803
|
+
const serializeMetadata = (metadata: Metadata): string => {
|
|
757
804
|
const lines: string[] = [];
|
|
758
805
|
|
|
759
806
|
if (metadata.title) {
|
|
@@ -809,6 +856,7 @@ export const serializeLilyletDoc = (doc: LilyletDoc): string => {
|
|
|
809
856
|
let currentKey: KeySignature | undefined;
|
|
810
857
|
let currentTime: { numerator: number; denominator: number; symbol?: 'common' | 'cut' } | undefined;
|
|
811
858
|
const staffClefs: Record<number, Clef> = {}; // Track clef per staff
|
|
859
|
+
const emittedClefs: Record<number, Clef> = {}; // Track which clefs have been output
|
|
812
860
|
|
|
813
861
|
for (let i = 0; i < doc.measures.length; i++) {
|
|
814
862
|
const measure = doc.measures[i];
|
|
@@ -825,13 +873,16 @@ export const serializeLilyletDoc = (doc: LilyletDoc): string => {
|
|
|
825
873
|
for (const voice of part.voices) {
|
|
826
874
|
for (const event of voice.events) {
|
|
827
875
|
if (event.type === 'context' && (event as ContextChange).clef) {
|
|
828
|
-
|
|
876
|
+
const ctx = event as ContextChange;
|
|
877
|
+
// Use the event's staff if specified (cross-staff), otherwise the voice's staff
|
|
878
|
+
const clefStaff = ctx.staff || voice.staff;
|
|
879
|
+
staffClefs[clefStaff] = ctx.clef!;
|
|
829
880
|
}
|
|
830
881
|
}
|
|
831
882
|
}
|
|
832
883
|
}
|
|
833
884
|
|
|
834
|
-
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime, staffClefs);
|
|
885
|
+
const { str: measureStr, newStaff } = serializeMeasure(measure, i === 0, currentStaff, isGrandStaff, currentKey, currentTime, staffClefs, emittedClefs);
|
|
835
886
|
// Always include measure, even if empty (use space rest for empty measures)
|
|
836
887
|
measureStrs.push(measureStr || 's1');
|
|
837
888
|
currentStaff = newStaff;
|