@k-l-lambda/lilylet 0.1.88 → 0.1.90
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/abc/grammar.jison.js +218 -213
- package/lib/clefTransposition.d.ts +1 -0
- package/lib/clefTransposition.js +1 -0
- package/lib/lilylet/abcDecoder.js +60 -35
- package/lib/lilylet/clefTransposition.d.ts +111 -0
- package/lib/lilylet/clefTransposition.js +175 -0
- package/lib/lilylet/index.d.ts +1 -0
- package/lib/lilylet/index.js +1 -0
- package/lib/lilylet/lilypondDecoder.js +88 -2
- package/lib/lilylet/lilypondEncoder.js +32 -2
- package/lib/lilylet/meiEncoder.js +11 -19
- package/lib/lilylet/musicXmlDecoder.js +56 -3
- package/lib/lilylet/musicXmlEncoder.js +33 -1
- package/lib/lilylet/musicXmlTypes.d.ts +6 -0
- package/lib/lilylet/onsets.d.ts +5 -4
- package/lib/lilylet/onsets.js +7 -14
- package/package.json +4 -1
- package/source/abc/abc.jison +2 -0
- package/source/abc/grammar.jison.js +218 -213
- package/source/lilylet/abcDecoder.ts +60 -35
- package/source/lilylet/clefTransposition.ts +197 -0
- package/source/lilylet/index.ts +1 -0
- package/source/lilylet/lilypondDecoder.ts +89 -2
- package/source/lilylet/lilypondEncoder.ts +36 -2
- package/source/lilylet/meiEncoder.ts +13 -18
- package/source/lilylet/musicXmlDecoder.ts +67 -3
- package/source/lilylet/musicXmlEncoder.ts +35 -1
- package/source/lilylet/musicXmlTypes.ts +10 -0
- package/source/lilylet/onsets.ts +7 -14
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { DOMParser } from '@xmldom/xmldom';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
Clef,
|
|
11
12
|
LilyletDoc,
|
|
12
13
|
Measure,
|
|
13
14
|
Part,
|
|
@@ -49,6 +50,8 @@ import {
|
|
|
49
50
|
MusicXmlLyric,
|
|
50
51
|
} from './musicXmlTypes';
|
|
51
52
|
|
|
53
|
+
import { withClefTransposition } from './clefTransposition';
|
|
54
|
+
|
|
52
55
|
import {
|
|
53
56
|
getElementText,
|
|
54
57
|
getElementInt,
|
|
@@ -647,6 +650,21 @@ const parseAttributes = (attrEl: Element): MusicXmlAttributes => {
|
|
|
647
650
|
}
|
|
648
651
|
}
|
|
649
652
|
|
|
653
|
+
// Instrument transposition. This is what carries a transposing part's sounding
|
|
654
|
+
// pitch in MusicXML (<clef-octave-change> is only the glyph), and it was never
|
|
655
|
+
// read, so every transposing instrument decoded as sounding-as-written.
|
|
656
|
+
const transposeEl = getElements(attrEl, 'transpose')[0];
|
|
657
|
+
if (transposeEl) {
|
|
658
|
+
const chromatic = getElementInt(transposeEl, 'chromatic');
|
|
659
|
+
if (chromatic != null) {
|
|
660
|
+
result.transpose = {
|
|
661
|
+
diatonic: getElementInt(transposeEl, 'diatonic') ?? undefined,
|
|
662
|
+
chromatic,
|
|
663
|
+
octaveChange: getElementInt(transposeEl, 'octave-change') ?? undefined,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
650
668
|
// Staves
|
|
651
669
|
const staves = getElementInt(attrEl, 'staves');
|
|
652
670
|
if (staves !== undefined) {
|
|
@@ -1260,6 +1278,38 @@ const forwardGapToRests = (gapTicks: number, divisions: number): RestEvent[] =>
|
|
|
1260
1278
|
return rests;
|
|
1261
1279
|
};
|
|
1262
1280
|
|
|
1281
|
+
/**
|
|
1282
|
+
* Fold a MusicXML transposition onto a lilylet clef as a suffix.
|
|
1283
|
+
*
|
|
1284
|
+
* MusicXML keeps the two apart: <transpose> is the sounding shift and moves pitch,
|
|
1285
|
+
* <clef-octave-change> is the "8" glyph under the clef and moves nothing. A lilylet
|
|
1286
|
+
* suffix means sounding shift, so only ONE of them may be folded in — adding both
|
|
1287
|
+
* would double an octave clef that also declares <transpose> (the shape this
|
|
1288
|
+
* encoder itself now emits) into two octaves.
|
|
1289
|
+
*
|
|
1290
|
+
* <transpose> wins when present, being the explicit statement of sounding pitch.
|
|
1291
|
+
* Absent it, an octave clef is read as implying its own shift, which is what a
|
|
1292
|
+
* tenor-voice or guitar treble_8 part means.
|
|
1293
|
+
*
|
|
1294
|
+
* A shift with no exact suffix (only the tritone) leaves the clef plain rather than
|
|
1295
|
+
* rounding: a silently wrong sounding pitch is worse than a visibly absent shift.
|
|
1296
|
+
*/
|
|
1297
|
+
const clefWithShift = (
|
|
1298
|
+
clef: Clef | undefined,
|
|
1299
|
+
transposeSemitones: number,
|
|
1300
|
+
clefOctaveChange?: number,
|
|
1301
|
+
): Clef | undefined => {
|
|
1302
|
+
if (!clef) return clef;
|
|
1303
|
+
const semitones = transposeSemitones !== 0 ? transposeSemitones : 12 * (clefOctaveChange ?? 0);
|
|
1304
|
+
if (semitones === 0) return clef;
|
|
1305
|
+
const shifted = withClefTransposition(clef as string, semitones);
|
|
1306
|
+
if (shifted === undefined) {
|
|
1307
|
+
console.warn(`musicXmlDecoder: transposition ${semitones} has no exact clef suffix; dropped for clef "${clef}"`);
|
|
1308
|
+
return clef;
|
|
1309
|
+
}
|
|
1310
|
+
return shifted as Clef;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1263
1313
|
/**
|
|
1264
1314
|
* Total time a TupletEvent advances the voice, in voiceTracker duration units.
|
|
1265
1315
|
* Sum the inner note/rest values then apply the tuplet ratio (triplet etc.).
|
|
@@ -1281,7 +1331,10 @@ const convertMeasure = (
|
|
|
1281
1331
|
voiceTracker: VoiceTracker,
|
|
1282
1332
|
spannerTracker: SpannerTracker,
|
|
1283
1333
|
ottavaTracker: { current: number },
|
|
1284
|
-
tupletTracker: TupletTracker
|
|
1334
|
+
tupletTracker: TupletTracker,
|
|
1335
|
+
// A part's <transpose> is declared once, usually in measure 1, and stays in force
|
|
1336
|
+
// for the whole part, so it has to survive across measures like the ottava does.
|
|
1337
|
+
transposeTracker: { semitones: number }
|
|
1285
1338
|
): MeasureConversionResult => {
|
|
1286
1339
|
let key: KeySignature | undefined;
|
|
1287
1340
|
let timeSig: Fraction | undefined;
|
|
@@ -1369,10 +1422,20 @@ const convertMeasure = (
|
|
|
1369
1422
|
timeSig = createFraction(attrs.time.beats, attrs.time.beatType);
|
|
1370
1423
|
}
|
|
1371
1424
|
|
|
1425
|
+
// Instrument transposition, in force from here to the end of the part.
|
|
1426
|
+
if (attrs.transpose) {
|
|
1427
|
+
const { chromatic, octaveChange } = attrs.transpose;
|
|
1428
|
+
transposeTracker.semitones = chromatic + 12 * (octaveChange ?? 0);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1372
1431
|
// Clefs - store by staff number
|
|
1373
1432
|
if (attrs.clefs) {
|
|
1374
1433
|
for (const clefEntry of attrs.clefs) {
|
|
1375
|
-
const clef =
|
|
1434
|
+
const clef = clefWithShift(
|
|
1435
|
+
convertClef(clefEntry.clef.sign, clefEntry.clef.line),
|
|
1436
|
+
transposeTracker.semitones,
|
|
1437
|
+
clefEntry.clef.clefOctaveChange,
|
|
1438
|
+
);
|
|
1376
1439
|
if (clef) {
|
|
1377
1440
|
const staff = clefEntry.staff;
|
|
1378
1441
|
if (staffHasNotes.has(staff)) {
|
|
@@ -1761,6 +1824,7 @@ const convertPart = (partEl: Element): { measures: Measure[]; name?: string } =>
|
|
|
1761
1824
|
const voiceTracker = new VoiceTracker();
|
|
1762
1825
|
const spannerTracker = new SpannerTracker();
|
|
1763
1826
|
const ottavaTracker = { current: 0 };
|
|
1827
|
+
const transposeTracker = { semitones: 0 };
|
|
1764
1828
|
const tupletTracker = new TupletTracker();
|
|
1765
1829
|
|
|
1766
1830
|
let lastKey: KeySignature | undefined;
|
|
@@ -1779,7 +1843,7 @@ const convertPart = (partEl: Element): { measures: Measure[]; name?: string } =>
|
|
|
1779
1843
|
|
|
1780
1844
|
for (const measureEl of measureEls) {
|
|
1781
1845
|
voiceTracker.reset();
|
|
1782
|
-
const { voiceMap, key, timeSig, barline, leadingBarline, harmonies, clefs } = convertMeasure(measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker);
|
|
1846
|
+
const { voiceMap, key, timeSig, barline, leadingBarline, harmonies, clefs } = convertMeasure(measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker, transposeTracker);
|
|
1783
1847
|
|
|
1784
1848
|
// A forward-repeat (or any `location="left"`) barline belongs at the END of
|
|
1785
1849
|
// the PREVIOUS measure in lilylet's "\bar = current measure's end bar" model.
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
|
|
49
49
|
import { parseStaffLayout, StaffGroup, StaffGroupType } from "./staffLayout";
|
|
50
50
|
|
|
51
|
+
import { clefBaseName, clefSuffixSemitones, clefSuffixTransposition } from "./clefTransposition";
|
|
52
|
+
|
|
51
53
|
|
|
52
54
|
// === Constants and Reverse Mappings ===
|
|
53
55
|
|
|
@@ -304,13 +306,45 @@ const encodeAttributes = (
|
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
if (options.clef) {
|
|
307
|
-
|
|
309
|
+
// Look the sign/line up under the BASE name. A lilylet clef may carry a
|
|
310
|
+
// transposition suffix ("treble_8", "treble_m3"), and keying the table on the
|
|
311
|
+
// whole string missed every one of them — `clefInfo` came back undefined and
|
|
312
|
+
// the <clef> element was dropped entirely, sign and line with it.
|
|
313
|
+
const clefStr = options.clef as string;
|
|
314
|
+
const clefInfo = CLEF_TO_SIGN[clefBaseName(clefStr)];
|
|
308
315
|
if (clefInfo) {
|
|
309
316
|
xml += `${indent(level + 1)}<clef>\n`;
|
|
310
317
|
xml += `${indent(level + 2)}<sign>${clefInfo.sign}</sign>\n`;
|
|
311
318
|
xml += `${indent(level + 2)}<line>${clefInfo.line}</line>\n`;
|
|
319
|
+
// A whole-octave shift also has a notational form: the "8" drawn under or
|
|
320
|
+
// over the clef. This is the glyph only — sounding pitch rides on
|
|
321
|
+
// <transpose> below, exactly as in LilyPond.
|
|
322
|
+
const semitones = clefSuffixSemitones(clefStr);
|
|
323
|
+
if (semitones % 12 === 0 && semitones !== 0)
|
|
324
|
+
xml += `${indent(level + 2)}<clef-octave-change>${semitones / 12}</clef-octave-change>\n`;
|
|
312
325
|
xml += `${indent(level + 1)}</clef>\n`;
|
|
313
326
|
}
|
|
327
|
+
|
|
328
|
+
// The written->sounding transposition. MusicXML carries the diatonic step count
|
|
329
|
+
// and the chromatic semitone count separately, which is what lets a minor third
|
|
330
|
+
// (-2 steps, -3 semitones) be expressed exactly. <transpose> follows <clef> in
|
|
331
|
+
// the attributes content order.
|
|
332
|
+
const trans = clefSuffixTransposition(clefStr);
|
|
333
|
+
if (trans) {
|
|
334
|
+
// <diatonic> and <chromatic> hold the WITHIN-OCTAVE remainder and whole
|
|
335
|
+
// octaves go in <octave-change>, so both must be reduced by the same octave
|
|
336
|
+
// count: 7 diatonic steps and 12 semitones each. Reducing only the semitones
|
|
337
|
+
// would make an octave clef read as two octaves.
|
|
338
|
+
const octaveChange = Math.trunc(trans.semi / 12);
|
|
339
|
+
const diatonic = trans.diat - 7 * octaveChange;
|
|
340
|
+
const chromatic = trans.semi - 12 * octaveChange;
|
|
341
|
+
xml += `${indent(level + 1)}<transpose>\n`;
|
|
342
|
+
xml += `${indent(level + 2)}<diatonic>${diatonic}</diatonic>\n`;
|
|
343
|
+
xml += `${indent(level + 2)}<chromatic>${chromatic}</chromatic>\n`;
|
|
344
|
+
if (octaveChange !== 0)
|
|
345
|
+
xml += `${indent(level + 2)}<octave-change>${octaveChange}</octave-change>\n`;
|
|
346
|
+
xml += `${indent(level + 1)}</transpose>\n`;
|
|
347
|
+
}
|
|
314
348
|
}
|
|
315
349
|
|
|
316
350
|
xml += `${indent(level)}</attributes>\n`;
|
|
@@ -41,12 +41,22 @@ export interface MusicXmlClef {
|
|
|
41
41
|
clefOctaveChange?: number; // -1, 0, 1
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// A part's written->sounding transposition. diatonic/chromatic hold the
|
|
45
|
+
// within-octave remainder and octaveChange the whole octaves, so the total shift is
|
|
46
|
+
// chromatic + 12 * octaveChange semitones.
|
|
47
|
+
export interface MusicXmlTranspose {
|
|
48
|
+
diatonic?: number;
|
|
49
|
+
chromatic: number;
|
|
50
|
+
octaveChange?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export interface MusicXmlAttributes {
|
|
45
54
|
divisions?: number; // Divisions per quarter note
|
|
46
55
|
key?: MusicXmlKey;
|
|
47
56
|
time?: MusicXmlTime;
|
|
48
57
|
clefs?: { staff: number; clef: MusicXmlClef }[]; // Clefs by staff number
|
|
49
58
|
staves?: number; // Number of staves
|
|
59
|
+
transpose?: MusicXmlTranspose; // Instrument transposition, folded into the clef suffix
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
// ============ Note Types ============
|
package/source/lilylet/onsets.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { calculateDuration } from "./musicXmlUtils";
|
|
13
13
|
import { Accidental } from "./types";
|
|
14
|
+
import { clefSuffixSemitones } from "./clefTransposition";
|
|
14
15
|
|
|
15
16
|
// Onset resolution in ticks per quarter note. calculateDuration() rounds its result to an
|
|
16
17
|
// integer, so it must run at a resolution fine enough to represent every note value exactly:
|
|
@@ -25,22 +26,14 @@ const ACCIDENTAL_SEMITONE: Record<string, number> = {
|
|
|
25
26
|
[Accidental.doubleSharp]: 2, [Accidental.doubleFlat]: -2,
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
// Diatonic-step -> semitones within an octave (unison..seventh), for "_N"/"^N" clef suffixes.
|
|
29
|
-
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
30
|
-
|
|
31
29
|
/**
|
|
32
|
-
* The written->sounding semitone shift a clef string declares.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* A plain clef
|
|
30
|
+
* The written->sounding semitone shift a clef string declares. A "_N"/"^N" suffix transposes
|
|
31
|
+
* DOWN/UP by the diatonic interval N ("treble_8" = octave down, -12; "treble_5" = fifth down,
|
|
32
|
+
* -7), and an "m" before the number selects that interval's MINOR form ("treble_m3" = minor
|
|
33
|
+
* third down, -3). A plain clef declares 0. See clefTransposition.ts for the grammar; this is
|
|
34
|
+
* a thin alias kept because clefShift is the name the onset API and its tests already use.
|
|
36
35
|
*/
|
|
37
|
-
export const clefShift = (clefStr: string): number =>
|
|
38
|
-
const m = /^.*?([_^])(\d+)$/.exec(clefStr || "");
|
|
39
|
-
if (!m) return 0;
|
|
40
|
-
const k = parseInt(m[2], 10) - 1; // diatonic steps above unison
|
|
41
|
-
const semis = DIATONIC_SEMITONES[k % 7] + 12 * Math.floor(k / 7);
|
|
42
|
-
return (m[1] === "^" ? 1 : -1) * semis;
|
|
43
|
-
};
|
|
36
|
+
export const clefShift = (clefStr: string): number => clefSuffixSemitones(clefStr);
|
|
44
37
|
|
|
45
38
|
/**
|
|
46
39
|
* Sounding MIDI pitch of a resolved (absolute-octave) Lilylet pitch. Octave 0 = middle-C
|