@k-l-lambda/lilylet 0.1.81 → 0.1.83
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/abcDecoder.js +143 -2
- package/lib/lilylet/grammar.jison.js +275 -213
- package/lib/lilylet/highlight.d.ts +1 -1
- package/lib/lilylet/highlight.js +8 -0
- package/lib/lilylet/index.d.ts +1 -0
- package/lib/lilylet/index.js +1 -0
- package/lib/lilylet/measureLayoutFromXml.js +185 -70
- package/lib/lilylet/meiEncoder.js +50 -8
- package/lib/lilylet/onsets.d.ts +53 -0
- package/lib/lilylet/onsets.js +144 -0
- package/lib/lilylet/serializer.js +36 -0
- package/lib/lilylet/types.d.ts +11 -0
- package/lib/onsets.d.ts +1 -0
- package/lib/onsets.js +1 -0
- package/package.json +7 -1
- package/source/lilylet/abcDecoder.ts +138 -2
- package/source/lilylet/grammar.jison.js +275 -213
- package/source/lilylet/highlight.ts +9 -0
- package/source/lilylet/index.ts +1 -0
- package/source/lilylet/lilylet.jison +67 -5
- package/source/lilylet/measureLayoutFromXml.ts +190 -69
- package/source/lilylet/meiEncoder.ts +62 -9
- package/source/lilylet/onsets.ts +158 -0
- package/source/lilylet/serializer.ts +37 -0
- package/source/lilylet/types.ts +24 -0
|
@@ -189,6 +189,70 @@ const convertDuration = (abcDuration, unitLength) => {
|
|
|
189
189
|
const log2 = Math.round(Math.log2(division));
|
|
190
190
|
return { division: Math.pow(2, Math.max(0, log2)), dots: 0 };
|
|
191
191
|
};
|
|
192
|
+
// A whole note is WHOLE_TICKS ticks. 2^7·3^3·5·7 = 120960 divides by every dot (·3/2, ·7/4),
|
|
193
|
+
// tuplet (÷3, ÷5, ÷7), and common division, so a voice's played length is an exact integer —
|
|
194
|
+
// no float rounding when we compare it to the bar length (unlike calculateDuration's Math.round).
|
|
195
|
+
const WHOLE_TICKS = 120960;
|
|
196
|
+
// A resolved Duration's tick length (dots + optional lilylet tuplet ratio num/den applied).
|
|
197
|
+
const durationTicks = (d) => {
|
|
198
|
+
let t = WHOLE_TICKS / d.division;
|
|
199
|
+
if (d.dots) {
|
|
200
|
+
let dot = t / 2;
|
|
201
|
+
for (let i = 0; i < d.dots; i++) {
|
|
202
|
+
t += dot;
|
|
203
|
+
dot /= 2;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (d.tuplet)
|
|
207
|
+
t = t * d.tuplet.numerator / d.tuplet.denominator;
|
|
208
|
+
return t;
|
|
209
|
+
};
|
|
210
|
+
// Sum the played tick length of an event list. Context / barline / markup / dynamic events
|
|
211
|
+
// take no time, and GRACE notes steal none (they carry a nominal duration but do not advance
|
|
212
|
+
// the beat). A tuplet/times group scales its inner notes by ratio num/den (e.g. \tuplet 3/2 of
|
|
213
|
+
// eighths = a quarter). This mirrors the parser's own \partial check so the emitted \partial
|
|
214
|
+
// never trips a partial-mismatch warning. Used to measure a voice against its bar length.
|
|
215
|
+
const eventsTicks = (events) => {
|
|
216
|
+
let total = 0;
|
|
217
|
+
for (const ev of events) {
|
|
218
|
+
if (ev.type === "note" || ev.type === "rest") {
|
|
219
|
+
if (ev.grace)
|
|
220
|
+
continue;
|
|
221
|
+
total += durationTicks(ev.duration);
|
|
222
|
+
}
|
|
223
|
+
else if (ev.type === "tuplet" || ev.type === "times") {
|
|
224
|
+
const t = ev;
|
|
225
|
+
const factor = t.ratio ? t.ratio.numerator / t.ratio.denominator : 1;
|
|
226
|
+
for (const inner of t.events || []) {
|
|
227
|
+
if ((inner.type === "note" || inner.type === "rest") && !inner.grace)
|
|
228
|
+
total += durationTicks(inner.duration) * factor;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (ev.type === "tremolo") {
|
|
232
|
+
const tr = ev;
|
|
233
|
+
total += 2 * (WHOLE_TICKS / tr.division);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return total;
|
|
237
|
+
};
|
|
238
|
+
// Build a \partial Duration (single {division, dots}) for a pickup of `ticks` whole-note ticks.
|
|
239
|
+
// LilyPond's \partial takes one duration token, so only power-of-2 divisions with up to 2 dots
|
|
240
|
+
// are expressible; a length that isn't (e.g. a 5/8 pickup) returns null and no \partial is added
|
|
241
|
+
// (the bar simply renders short) rather than emitting a wrong duration.
|
|
242
|
+
const ticksToPartialDuration = (ticks) => {
|
|
243
|
+
if (ticks <= 0)
|
|
244
|
+
return null;
|
|
245
|
+
for (let division = 1; division <= 128; division *= 2) {
|
|
246
|
+
const base = WHOLE_TICKS / division;
|
|
247
|
+
if (ticks === base)
|
|
248
|
+
return { division, dots: 0 };
|
|
249
|
+
if (ticks === base * 1.5)
|
|
250
|
+
return { division, dots: 1 };
|
|
251
|
+
if (ticks === base * 1.75)
|
|
252
|
+
return { division, dots: 2 };
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
};
|
|
192
256
|
/**
|
|
193
257
|
* Apply broken rhythm adjustment.
|
|
194
258
|
* broken > 0 (A>B): current note gets dotted, next gets halved
|
|
@@ -492,6 +556,12 @@ const collectAbcRepeatInfo = (bars) => {
|
|
|
492
556
|
// ("|:" forward, ":|:"/"::"/":||:" end-and-start).
|
|
493
557
|
const startsNext = trailBar.includes("|:") || isEndStart;
|
|
494
558
|
if (startsNext && idx < n) {
|
|
559
|
+
// A new repeat section opening here also CLOSES any still-open ending
|
|
560
|
+
// (a volta cannot span into the next repeat section). abc2xml back-patches
|
|
561
|
+
// the ending-stop onto this measure; without this a final volta (e.g.
|
|
562
|
+
// ":|2 …|:") would overshoot to the next volta/section bar far ahead.
|
|
563
|
+
if (openEndingStart >= 1 && openEndingStart <= idx)
|
|
564
|
+
closeOpenEnding(idx);
|
|
495
565
|
infos[idx].repeatStart = true;
|
|
496
566
|
}
|
|
497
567
|
// A trailing volta digit ("|2", ":|1,2") opens an ending on the NEXT measure.
|
|
@@ -660,6 +730,13 @@ const abcLayoutToStaves = (layout) => {
|
|
|
660
730
|
* `voiceInstr` maps an ABC voice number to its {name, shortName}. The layout's staff ids
|
|
661
731
|
* are arc-first voice numbers, so a staff id "5" looks up voice 5's name.
|
|
662
732
|
*/
|
|
733
|
+
// ABC quoted strings carry literal escape sequences (e.g. `nm="Violin\nsolo"` is the 11
|
|
734
|
+
// characters V-i-o-l-i-n-\-n-s-o-l-o). The lilylet doc model holds DECODED strings — the
|
|
735
|
+
// serializer's escapeString re-encodes a real newline back to `\n`. Without decoding here,
|
|
736
|
+
// the stored backslash would be escaped a second time (`\n` → `\\n`). Mirror the lilylet
|
|
737
|
+
// grammar's `unq` unescape table so both import paths agree.
|
|
738
|
+
const ABC_UNESCAPE = { n: "\n", r: "\r", t: "\t", '"': '"', "\\": "\\" };
|
|
739
|
+
const unescapeAbcString = (s) => s.replace(/\\(.)/g, (m, c) => (c in ABC_UNESCAPE ? ABC_UNESCAPE[c] : m));
|
|
663
740
|
const abcInstrumentsToLilylet = (stavesCode, voiceInstr) => {
|
|
664
741
|
const layout = parseStaffLayout(stavesCode);
|
|
665
742
|
const result = {};
|
|
@@ -1339,8 +1416,8 @@ const decodeTune = (tune, options = {}) => {
|
|
|
1339
1416
|
continue;
|
|
1340
1417
|
const short = props.snm ?? props.sname;
|
|
1341
1418
|
voiceInstr.set(v, typeof short === "string" && short.length
|
|
1342
|
-
? { name, shortName: short }
|
|
1343
|
-
: { name });
|
|
1419
|
+
? { name: unescapeAbcString(name), shortName: unescapeAbcString(short) }
|
|
1420
|
+
: { name: unescapeAbcString(name) });
|
|
1344
1421
|
}
|
|
1345
1422
|
if (voiceInstr.size > 0) {
|
|
1346
1423
|
const instruments = abcInstrumentsToLilylet(metadata.staves, voiceInstr);
|
|
@@ -1369,6 +1446,10 @@ const decodeTune = (tune, options = {}) => {
|
|
|
1369
1446
|
// index. Built alongside barInfos' navTexts; attached after each measure's
|
|
1370
1447
|
// parts are assembled (mirrors the MusicXML path attaching nav to a note).
|
|
1371
1448
|
const navMarksByMeasure = new Map();
|
|
1449
|
+
// Running effective time signature (seeded from the header M:, updated by any inline
|
|
1450
|
+
// [M:...] change). A bar whose played length is SHORTER than this gets a \partial (see
|
|
1451
|
+
// the pickup/anacrusis handling below the per-measure assembly).
|
|
1452
|
+
let curTime = timeSig;
|
|
1372
1453
|
for (let mi = 0; mi < measures.length; mi++) {
|
|
1373
1454
|
const abcMeasure = measures[mi];
|
|
1374
1455
|
// Group patches by voice number
|
|
@@ -1561,6 +1642,66 @@ const decodeTune = (tune, options = {}) => {
|
|
|
1561
1642
|
break;
|
|
1562
1643
|
}
|
|
1563
1644
|
}
|
|
1645
|
+
// Pickup / short bar → \partial. Update the running time signature from any inline
|
|
1646
|
+
// [M:...] this bar carried (the LAST \time in the bar wins — a bar may restate then
|
|
1647
|
+
// change meter, e.g. \time 3/4 \time 9/8), then compare the bar's played length (the
|
|
1648
|
+
// longest voice, so a bar that is complete in ANY voice is not treated as short)
|
|
1649
|
+
// against the meter's full-bar length. A strictly shorter bar is an anacrusis (bar 1)
|
|
1650
|
+
// or a truncated final bar; emit \partial <bar-length> on the first voice so LilyPond
|
|
1651
|
+
// renders the incomplete bar. A full-measure rest (Z/R) voice is skipped — it fills the
|
|
1652
|
+
// bar by definition, so it must not drag the measured length down.
|
|
1653
|
+
{
|
|
1654
|
+
for (const part of parts) {
|
|
1655
|
+
for (const voice of part.voices) {
|
|
1656
|
+
for (const e of voice.events) {
|
|
1657
|
+
if (e.type === "context" && e.time) {
|
|
1658
|
+
const t = e.time;
|
|
1659
|
+
curTime = { numerator: t.numerator, denominator: t.denominator };
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (curTime) {
|
|
1665
|
+
const barTicks = WHOLE_TICKS * curTime.numerator / curTime.denominator;
|
|
1666
|
+
// \partial is a GLOBAL timing directive — it shortens the bar for every voice at
|
|
1667
|
+
// once. So it is only correct when every sounding voice has the SAME short length.
|
|
1668
|
+
// Collect each voice's played ticks; a full-measure-rest (Z/R) voice fills the bar
|
|
1669
|
+
// and is ignored. If the voices are ragged (different lengths — e.g. a messy meter
|
|
1670
|
+
// change where some voices are 6/8 and others 8/8), no single \partial fits them
|
|
1671
|
+
// all, so we emit none rather than one that mis-sizes some voices.
|
|
1672
|
+
const lengths = [];
|
|
1673
|
+
let anyFull = false;
|
|
1674
|
+
for (const part of parts) {
|
|
1675
|
+
for (const voice of part.voices) {
|
|
1676
|
+
if (voice.events.some(e => e.type === "rest" && e.fullMeasure)) {
|
|
1677
|
+
anyFull = true;
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
const t = eventsTicks(voice.events);
|
|
1681
|
+
if (t > 0)
|
|
1682
|
+
lengths.push(t);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const uniform = lengths.length > 0 && lengths.every(t => t === lengths[0]);
|
|
1686
|
+
const played = uniform ? lengths[0] : 0;
|
|
1687
|
+
if (!anyFull && uniform && played < barTicks) {
|
|
1688
|
+
const partial = ticksToPartialDuration(played);
|
|
1689
|
+
if (partial) {
|
|
1690
|
+
// Insert after any leading key/time context so the emitted order is
|
|
1691
|
+
// \key … \time … \partial … (LilyPond wants \partial after \time).
|
|
1692
|
+
const target = measure.parts[0]?.voices[0];
|
|
1693
|
+
if (target) {
|
|
1694
|
+
let at = 0;
|
|
1695
|
+
while (at < target.events.length && target.events[at].type === "context"
|
|
1696
|
+
&& (target.events[at].key || target.events[at].time)
|
|
1697
|
+
&& !target.events[at].clef)
|
|
1698
|
+
at++;
|
|
1699
|
+
target.events.splice(at, 0, { type: "context", partial });
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1564
1705
|
lilyletMeasures.push(measure);
|
|
1565
1706
|
}
|
|
1566
1707
|
// Add tempo to first measure's first voice if present
|