@k-l-lambda/lilylet 0.1.34 → 0.1.35

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.
@@ -128,6 +128,7 @@
128
128
  let currentKey = null;
129
129
  let currentTimeSig = null;
130
130
  let currentDuration = { division: 4, dots: 0 }; // default quarter note
131
+ let numericTimeSignature = false; // When true, 4/4 and 2/2 use numeric display instead of C/C|
131
132
 
132
133
  // Reset parser state - call before each parse
133
134
  const resetParserState = () => {
@@ -135,6 +136,7 @@
135
136
  currentKey = null;
136
137
  currentTimeSig = null;
137
138
  currentDuration = { division: 4, dots: 0 };
139
+ numericTimeSignature = false;
138
140
  };
139
141
 
140
142
  // Export reset function
@@ -168,6 +170,8 @@
168
170
  "\\clef" return 'CMD_CLEF'
169
171
  "\\key" return 'CMD_KEY'
170
172
  "\\time" return 'CMD_TIME'
173
+ "\\numericTimeSignature" return 'CMD_NUMERIC_TIME_SIG'
174
+ "\\defaultTimeSignature" return 'CMD_DEFAULT_TIME_SIG'
171
175
  "\\tempo" return 'CMD_TEMPO'
172
176
  "\\staff" return 'CMD_STAFF'
173
177
  "\\grace" return 'CMD_GRACE'
@@ -329,7 +333,7 @@ part_voices
329
333
 
330
334
  voice_events
331
335
  : /* empty */ { $$ = []; }
332
- | voice_events event { $$ = $1.concat(Array.isArray($2) ? $2 : [$2]); }
336
+ | voice_events event { $$ = $2 === null ? $1 : $1.concat(Array.isArray($2) ? $2 : [$2]); }
333
337
  ;
334
338
 
335
339
  event
@@ -413,6 +417,8 @@ context_event
413
417
  | staff_cmd -> contextChange({ staff: $1 })
414
418
  | ottava_cmd -> contextChange({ ottava: $1 })
415
419
  | stem_cmd -> contextChange({ stemDirection: $1 })
420
+ | numeric_time_sig_cmd -> null
421
+ | default_time_sig_cmd -> null
416
422
  ;
417
423
 
418
424
  clef_cmd
@@ -433,7 +439,29 @@ mode
433
439
  ;
434
440
 
435
441
  time_cmd
436
- : CMD_TIME NUMBER '/' NUMBER %{ currentTimeSig = fraction(Number($2), Number($4)); $$ = currentTimeSig; %}
442
+ : CMD_TIME NUMBER '/' NUMBER %{
443
+ const num = Number($2);
444
+ const den = Number($4);
445
+ const timeSig = fraction(num, den);
446
+ // Add symbol for 4/4 (common) and 2/2 (cut) unless numericTimeSignature is set
447
+ if (!numericTimeSignature) {
448
+ if (num === 4 && den === 4) {
449
+ timeSig.symbol = 'common';
450
+ } else if (num === 2 && den === 2) {
451
+ timeSig.symbol = 'cut';
452
+ }
453
+ }
454
+ currentTimeSig = timeSig;
455
+ $$ = currentTimeSig;
456
+ %}
457
+ ;
458
+
459
+ numeric_time_sig_cmd
460
+ : CMD_NUMERIC_TIME_SIG %{ numericTimeSignature = true; $$ = null; %}
461
+ ;
462
+
463
+ default_time_sig_cmd
464
+ : CMD_DEFAULT_TIME_SIG %{ numericTimeSignature = false; $$ = null; %}
437
465
  ;
438
466
 
439
467
  tempo_cmd
@@ -1586,11 +1586,14 @@ const encodeScoreDef = (
1586
1586
  timeNum: number,
1587
1587
  timeDen: number,
1588
1588
  partInfos: PartInfo[],
1589
- indent: string
1589
+ indent: string,
1590
+ meterSymbol?: 'common' | 'cut'
1590
1591
  ): string => {
1591
1592
  const scoreDefId = generateId("scoredef");
1592
1593
 
1593
- let xml = `${indent}<scoreDef xml:id="${scoreDefId}" key.sig="${keySig}" meter.count="${timeNum}" meter.unit="${timeDen}">\n`;
1594
+ // Build meter attributes
1595
+ const meterSymAttr = meterSymbol ? ` meter.sym="${meterSymbol}"` : '';
1596
+ let xml = `${indent}<scoreDef xml:id="${scoreDefId}" key.sig="${keySig}"${meterSymAttr} meter.count="${timeNum}" meter.unit="${timeDen}">\n`;
1594
1597
  xml += `${indent} <staffGrp xml:id="${generateId("staffgrp")}">\n`;
1595
1598
 
1596
1599
  for (let pi = 0; pi < partInfos.length; pi++) {
@@ -1640,6 +1643,7 @@ const encode = (doc: LilyletDoc, options: MEIEncoderOptions = {}): string => {
1640
1643
  let currentKey = 0;
1641
1644
  let currentTimeNum = 4;
1642
1645
  let currentTimeDen = 4;
1646
+ let currentMeterSymbol: 'common' | 'cut' | undefined = undefined;
1643
1647
 
1644
1648
  const firstMeasure = doc.measures[0];
1645
1649
  if (firstMeasure.key) {
@@ -1648,6 +1652,7 @@ const encode = (doc: LilyletDoc, options: MEIEncoderOptions = {}): string => {
1648
1652
  if (firstMeasure.timeSig) {
1649
1653
  currentTimeNum = firstMeasure.timeSig.numerator;
1650
1654
  currentTimeDen = firstMeasure.timeSig.denominator;
1655
+ currentMeterSymbol = firstMeasure.timeSig.symbol;
1651
1656
  }
1652
1657
 
1653
1658
  const keySig = KEY_SIGS[currentKey] || "0";
@@ -1701,7 +1706,7 @@ const encode = (doc: LilyletDoc, options: MEIEncoderOptions = {}): string => {
1701
1706
  mei += `${indent}${indent}<body>\n`;
1702
1707
  mei += `${indent}${indent}${indent}<mdiv xml:id="${generateId("mdiv")}">\n`;
1703
1708
  mei += `${indent}${indent}${indent}${indent}<score xml:id="${generateId("score")}">\n`;
1704
- mei += encodeScoreDef(keySig, currentTimeNum, currentTimeDen, partInfos, `${indent}${indent}${indent}${indent}${indent}`);
1709
+ mei += encodeScoreDef(keySig, currentTimeNum, currentTimeDen, partInfos, `${indent}${indent}${indent}${indent}${indent}`, currentMeterSymbol);
1705
1710
  mei += `${indent}${indent}${indent}${indent}${indent}<section xml:id="${generateId("section")}">\n`;
1706
1711
 
1707
1712
  // Track tie state across measures for cross-measure ties
@@ -1757,6 +1762,20 @@ const encode = (doc: LilyletDoc, options: MEIEncoderOptions = {}): string => {
1757
1762
  mei += `${indent}${indent}${indent}${indent}${indent}${indent}<scoreDef xml:id="${generateId('scoredef')}" key.sig="${newKeySig}" />\n`;
1758
1763
  }
1759
1764
  }
1765
+ // Check for time signature change and output scoreDef if needed
1766
+ if (measure.timeSig && mi > 0) {
1767
+ const newTimeNum = measure.timeSig.numerator;
1768
+ const newTimeDen = measure.timeSig.denominator;
1769
+ const newMeterSymbol = measure.timeSig.symbol;
1770
+ if (newTimeNum !== currentTimeNum || newTimeDen !== currentTimeDen || newMeterSymbol !== currentMeterSymbol) {
1771
+ currentTimeNum = newTimeNum;
1772
+ currentTimeDen = newTimeDen;
1773
+ currentMeterSymbol = newMeterSymbol;
1774
+ // Output a scoreDef with the new time signature
1775
+ const meterSymAttr = currentMeterSymbol ? ` meter.sym="${currentMeterSymbol}"` : '';
1776
+ mei += `${indent}${indent}${indent}${indent}${indent}${indent}<scoreDef xml:id="${generateId('scoredef')}"${meterSymAttr} meter.count="${currentTimeNum}" meter.unit="${currentTimeDen}" />\n`;
1777
+ }
1778
+ }
1760
1779
  mei += encodeMeasure(measure, mi + 1, `${indent}${indent}${indent}${indent}${indent}${indent}`, totalStaves, tieState, slurState, hairpinState, currentKey, partInfos, clefState);
1761
1780
  });
1762
1781
 
@@ -550,7 +550,7 @@ const serializeEvent = (
550
550
  // Key/time signature info to inject into first voice
551
551
  interface MeasureContext {
552
552
  key?: KeySignature;
553
- time?: { numerator: number; denominator: number };
553
+ time?: { numerator: number; denominator: number; symbol?: 'common' | 'cut' };
554
554
  }
555
555
 
556
556
  // Serialize a voice with pitch environment tracking
@@ -585,7 +585,13 @@ const serializeVoice = (
585
585
  parts.push('\\key ' + keyStr);
586
586
  }
587
587
  if (measureContext.time) {
588
- parts.push('\\time ' + measureContext.time.numerator + '/' + measureContext.time.denominator);
588
+ const { numerator, denominator, symbol } = measureContext.time;
589
+ // Output \numericTimeSignature before 4/4 or 2/2 if no symbol is set
590
+ // (meaning numeric display was explicitly requested)
591
+ if (!symbol && ((numerator === 4 && denominator === 4) || (numerator === 2 && denominator === 2))) {
592
+ parts.push('\\numericTimeSignature');
593
+ }
594
+ parts.push('\\time ' + numerator + '/' + denominator);
589
595
  }
590
596
  }
591
597
 
@@ -99,6 +99,12 @@ export interface Fraction {
99
99
  denominator: number;
100
100
  }
101
101
 
102
+ // Time signature with optional symbol display
103
+ // symbol: 'common' for C (4/4), 'cut' for C| (2/2), undefined for numeric
104
+ export interface TimeSig extends Fraction {
105
+ symbol?: 'common' | 'cut';
106
+ }
107
+
102
108
  export interface Pitch {
103
109
  phonet: Phonet;
104
110
  accidental?: Accidental;
@@ -292,7 +298,7 @@ export interface Part {
292
298
  // Measure contains parts separated by \\\
293
299
  export interface Measure {
294
300
  key?: KeySignature;
295
- timeSig?: Fraction;
301
+ timeSig?: TimeSig;
296
302
  parts: Part[];
297
303
  partial?: boolean;
298
304
  }
@@ -1,28 +0,0 @@
1
- /**
2
- * LilyPond to Lilylet Decoder
3
- *
4
- * Converts LilyPond notation files to Lilylet document format using the lotus parser.
5
- * This module is browser-compatible - it uses pre-compiled parser from lotus.
6
- */
7
- import * as lilyParser from "@k-l-lambda/lotus/lib/inc/lilyParser/index.js";
8
- import { LilyletDoc, Event, Fraction } from "./types";
9
- interface ParsedMeasure {
10
- key: number | null;
11
- timeSig: Fraction | null;
12
- voices: ParsedVoice[];
13
- partial: boolean;
14
- }
15
- interface ParsedVoice {
16
- staff: number;
17
- events: Event[];
18
- }
19
- declare const parseLilyDocument: (lilyDocument: lilyParser.LilyDocument) => ParsedMeasure[];
20
- /**
21
- * Decode a LilyPond string to LilyletDoc (synchronous, browser-compatible)
22
- */
23
- declare const decode: (lilypondSource: string) => LilyletDoc;
24
- /**
25
- * Decode from pre-parsed LilyDocument (synchronous, for when you already have parsed data)
26
- */
27
- declare const decodeFromDocument: (lilyDocument: lilyParser.LilyDocument) => LilyletDoc;
28
- export { decode, decodeFromDocument, parseLilyDocument, };