@k-l-lambda/lilylet 0.1.54 → 0.1.56

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.
@@ -140,7 +140,11 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals)
140
140
  const prevMeasureAccid = measureAccidentals?.get(pitchKey);
141
141
  if (pitch.accidental) {
142
142
  const noteAccid = ACCIDENTALS[pitch.accidental];
143
- if (prevMeasureAccid !== undefined && noteAccid !== prevMeasureAccid) {
143
+ if (pitch.courtesy) {
144
+ // Courtesy accidental (!) - always display
145
+ accid = noteAccid;
146
+ }
147
+ else if (prevMeasureAccid !== undefined && noteAccid !== prevMeasureAccid) {
144
148
  // Previous note in this measure had a different accidental - must re-assert
145
149
  accid = noteAccid;
146
150
  }
@@ -156,7 +160,11 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals)
156
160
  }
157
161
  else if (keyAccid) {
158
162
  // Note has no accidental but key implies one - output natural
159
- if (prevMeasureAccid === 'n') {
163
+ if (pitch.courtesy) {
164
+ // Courtesy accidental (!) - always display
165
+ accid = 'n';
166
+ }
167
+ else if (prevMeasureAccid === 'n') {
160
168
  // Already cancelled earlier in this measure - no need to show again
161
169
  }
162
170
  else {
@@ -166,6 +174,13 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals)
166
174
  if (measureAccidentals)
167
175
  measureAccidentals.set(pitchKey, 'n');
168
176
  }
177
+ else if (pitch.courtesy && prevMeasureAccid && prevMeasureAccid !== 'n') {
178
+ // Courtesy accidental after an in-measure accidental - force natural display
179
+ accid = 'n';
180
+ accidGes = 'n';
181
+ if (measureAccidentals)
182
+ measureAccidentals.set(pitchKey, 'n');
183
+ }
169
184
  else if (measureAccidentals) {
170
185
  // No explicit accidental, no key accidental - check if earlier note in measure had one
171
186
  if (prevMeasureAccid && prevMeasureAccid !== 'n') {
@@ -483,11 +498,14 @@ const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFi
483
498
  };
484
499
  };
485
500
  // Convert RestEvent to MEI
486
- const restEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
501
+ const restEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals, crossStaff) => {
487
502
  const dur = DURATIONS[event.duration.division] || "4";
488
503
  let attrs = `xml:id="${generateId('rest')}" dur="${dur}"`;
489
504
  if (event.duration.dots > 0)
490
505
  attrs += ` dots="${event.duration.dots}"`;
506
+ // Cross-staff attribute
507
+ if (crossStaff)
508
+ attrs += ` staff="${crossStaff}"`;
491
509
  // Pitched rest (positioned at specific pitch)
492
510
  if (event.pitch) {
493
511
  const pitch = encodePitch(event.pitch, keyFifths, ottavaShift);
@@ -899,9 +917,12 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
899
917
  }
900
918
  break;
901
919
  }
902
- case 'rest':
903
- xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals);
920
+ case 'rest': {
921
+ // For cross-staff notation: pass staff number if different from voice's home staff
922
+ const restCrossStaff = currentStaff !== (voice.staff || 1) ? currentStaff : undefined;
923
+ xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals, restCrossStaff);
904
924
  break;
925
+ }
905
926
  case 'tuplet': {
906
927
  // Tuplet can be nested inside beam in MEI: <beam><tuplet>...</tuplet></beam>
907
928
  // Pass beamElementOpen to tuplet so it knows not to create its own beam
@@ -941,9 +962,13 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
941
962
  case 'context': {
942
963
  const ctx = event;
943
964
  // Check for clef changes - emit <clef> element only if different from current
965
+ // and only when on the home staff (don't emit cross-staff clefs into this layer)
944
966
  if (ctx.clef && ctx.clef !== currentClef) {
945
- const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
946
- xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
967
+ const layerStaff = voice.staff || 1;
968
+ if (currentStaff === layerStaff) {
969
+ const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
970
+ xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
971
+ }
947
972
  currentClef = ctx.clef;
948
973
  }
949
974
  // Check for ottava changes
@@ -1466,15 +1491,19 @@ const analyzePartStructure = (doc) => {
1466
1491
  for (let pi = 0; pi < measure.parts.length; pi++) {
1467
1492
  const part = measure.parts[pi];
1468
1493
  for (const voice of part.voices) {
1469
- const localStaff = voice.staff || 1;
1470
- partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, localStaff);
1471
- // Get FIRST clef from context changes (for initial staffDef)
1494
+ let currentStaff = voice.staff || 1;
1495
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1496
+ // Scan context changes for staff switches and clefs
1472
1497
  for (const event of voice.events) {
1473
1498
  if (event.type === 'context') {
1474
1499
  const ctx = event;
1475
- if (ctx.clef && !partInfos[pi].clefs[localStaff]) {
1476
- // Only set if not already set - take the FIRST clef
1477
- partInfos[pi].clefs[localStaff] = ctx.clef;
1500
+ if (ctx.staff !== undefined) {
1501
+ currentStaff = ctx.staff;
1502
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1503
+ }
1504
+ if (ctx.clef && !partInfos[pi].clefs[currentStaff]) {
1505
+ // Only set if not already set - take the FIRST clef per staff
1506
+ partInfos[pi].clefs[currentStaff] = ctx.clef;
1478
1507
  }
1479
1508
  }
1480
1509
  }
@@ -91,6 +91,7 @@ export interface Pitch {
91
91
  phonet: Phonet;
92
92
  accidental?: Accidental;
93
93
  octave: number;
94
+ courtesy?: boolean;
94
95
  }
95
96
  export interface Duration {
96
97
  division: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
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",