@k-l-lambda/lilylet 0.1.86 → 0.1.89
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 +87 -0
- package/lib/lilylet/clefTransposition.js +133 -0
- package/lib/lilylet/index.d.ts +2 -0
- package/lib/lilylet/index.js +2 -0
- package/lib/lilylet/meiEncoder.js +11 -19
- package/lib/lilylet/onsets.d.ts +5 -4
- package/lib/lilylet/onsets.js +7 -14
- package/lib/lilylet/transposition.d.ts +5 -0
- package/lib/lilylet/transposition.js +238 -0
- package/lib/transposition.d.ts +1 -0
- package/lib/transposition.js +1 -0
- 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 +149 -0
- package/source/lilylet/index.ts +2 -0
- package/source/lilylet/meiEncoder.ts +13 -18
- package/source/lilylet/onsets.ts +7 -14
- package/source/lilylet/transposition.ts +280 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/clefTransposition.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/clefTransposition.js";
|
|
@@ -7,6 +7,7 @@ import parse from "../abc/parser.js";
|
|
|
7
7
|
import { Phonet, Accidental, ArticulationType, OrnamentType, DynamicType, HairpinType, PedalType, NavigationMarkType, Placement, } from "./types.js";
|
|
8
8
|
import { parseStaffLayout, StaffGroupType } from "./staffLayout.js";
|
|
9
9
|
import { buildMeasureLayout, applyNavText } from "./measureLayoutFromXml.js";
|
|
10
|
+
import { parseClefSuffix, withClefTransposition } from "./clefTransposition.js";
|
|
10
11
|
// ============ Constants ============
|
|
11
12
|
// NotaGen catalog tags appear as leading single-% comments in ABC files,
|
|
12
13
|
// in the order: period, composer, instrumentation. They are mapped to
|
|
@@ -426,28 +427,34 @@ const convertClef = (clefStr) => {
|
|
|
426
427
|
};
|
|
427
428
|
/**
|
|
428
429
|
* Fold an ABC voice `transpose=N` property (a written→sounding shift in
|
|
429
|
-
* SEMITONES) into the Lilylet clef
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
430
|
+
* SEMITONES) into the Lilylet clef suffix, which the →MEI encoder later expands
|
|
431
|
+
* into trans.diat / trans.semi (see meiEncoder.resolveClef).
|
|
432
|
+
*
|
|
433
|
+
* The conversion is EXACT: clefTransposition.semitonesToClefSuffix returns the
|
|
434
|
+
* suffix for the interval that spans exactly this many semitones, using the `m`
|
|
435
|
+
* (minor) form where the major/perfect interval does not fit — so -3 becomes
|
|
436
|
+
* `_m3` rather than the nearest major third `_3`. It previously approximated with
|
|
437
|
+
* round(semi * 7/12), which put a one-semitone error on the sounding pitch of
|
|
438
|
+
* every minor-interval transposition (-3 A clarinet, -8, -10 E-flat instruments:
|
|
439
|
+
* 37% of transposing voices in the corpora checked).
|
|
440
|
+
*
|
|
441
|
+
* A semitone count with no exact suffix (only the tritone, ±6, within an octave)
|
|
442
|
+
* is dropped with a warning rather than rounded: a silent one-semitone pitch error
|
|
443
|
+
* is worse than a visibly absent transposition.
|
|
437
444
|
*/
|
|
438
445
|
const transposeClefSuffix = (clef, semitones) => {
|
|
439
446
|
if (!clef || !semitones)
|
|
440
447
|
return clef;
|
|
441
|
-
// A clef that already carries
|
|
442
|
-
// stacking another shift on top is not meaningful for ABC sources.
|
|
443
|
-
if (
|
|
448
|
+
// A clef that already carries a transposition suffix (treble_8 etc.) is left
|
|
449
|
+
// as-is; stacking another shift on top is not meaningful for ABC sources.
|
|
450
|
+
if (parseClefSuffix(clef))
|
|
444
451
|
return clef;
|
|
445
|
-
const
|
|
446
|
-
if (
|
|
452
|
+
const transposed = withClefTransposition(clef, semitones);
|
|
453
|
+
if (transposed === undefined) {
|
|
454
|
+
console.warn(`abcDecoder: transpose=${semitones} has no exact clef suffix; transposition dropped for clef "${clef}"`);
|
|
447
455
|
return clef;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return (clef + suffix);
|
|
456
|
+
}
|
|
457
|
+
return transposed;
|
|
451
458
|
};
|
|
452
459
|
/**
|
|
453
460
|
* Convert ABC barline to Lilylet barline style
|
|
@@ -1450,6 +1457,14 @@ const decodeTune = (tune, options = {}) => {
|
|
|
1450
1457
|
// [M:...] change). A bar whose played length is SHORTER than this gets a \partial (see
|
|
1451
1458
|
// the pickup/anacrusis handling below the per-measure assembly).
|
|
1452
1459
|
let curTime = timeSig;
|
|
1460
|
+
// ABC voice numbers whose declared clef has already been emitted. A voice gets its
|
|
1461
|
+
// clef on the measure where it FIRST carries music, which is not always measure 0:
|
|
1462
|
+
// a header field the grammar does not consume (e.g. `I:percmap`) degenerates into a
|
|
1463
|
+
// phantom measure 0 holding a single patch with no voice number, and the emission
|
|
1464
|
+
// was previously gated on `mi === 0`, so every real voice lost its clef — and with
|
|
1465
|
+
// it its transposition. Keying on the voice number instead of the measure index
|
|
1466
|
+
// makes the emission independent of what measure 0 happens to contain.
|
|
1467
|
+
const emittedClefVoices = new Set();
|
|
1453
1468
|
for (let mi = 0; mi < measures.length; mi++) {
|
|
1454
1469
|
const abcMeasure = measures[mi];
|
|
1455
1470
|
// Group patches by voice number
|
|
@@ -1585,29 +1600,39 @@ const decodeTune = (tune, options = {}) => {
|
|
|
1585
1600
|
for (const pi of sortedPartIndices) {
|
|
1586
1601
|
const voicesMap = partVoicesMap.get(pi);
|
|
1587
1602
|
const voices = [];
|
|
1603
|
+
// Voice -> the ABC voice number it came from, recovered from the map key
|
|
1604
|
+
// (voiceKey = staffInPart * 1000 + voiceNum). Voice itself carries no voice
|
|
1605
|
+
// number, and the clef below is a per-voice property, so the association has
|
|
1606
|
+
// to be kept explicitly rather than searched for.
|
|
1607
|
+
const voiceNumOf = new Map();
|
|
1588
1608
|
const sortedKeys = Array.from(voicesMap.keys()).sort((a, b) => a - b);
|
|
1589
1609
|
for (const key of sortedKeys) {
|
|
1590
|
-
|
|
1610
|
+
const voice = voicesMap.get(key);
|
|
1611
|
+
voices.push(voice);
|
|
1612
|
+
voiceNumOf.set(voice, key % 1000);
|
|
1591
1613
|
}
|
|
1592
1614
|
const part = { voices };
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1615
|
+
// Give each voice its OWN declared clef on the measure where it first
|
|
1616
|
+
// appears. A clef must be per voice, not per staff: an ABC voice folds its
|
|
1617
|
+
// `transpose=` into the clef suffix, so two voices sharing a staff can
|
|
1618
|
+
// legitimately differ, and lilylet's model is per-voice throughout
|
|
1619
|
+
// (onsets.measureOnsets tracks clefByVoice, serializer.findVoiceClef reads
|
|
1620
|
+
// one clef per Voice).
|
|
1621
|
+
//
|
|
1622
|
+
// This used to scan for the FIRST voice whose scoreLayout assignment matched
|
|
1623
|
+
// (partIndex, staff) and break, so every voice on a shared staff inherited
|
|
1624
|
+
// the first one's clef — the WRONG clef, taking its transposition with it.
|
|
1625
|
+
// A `%%score` arc such as `( 1 2 )` puts both voices on one staff and is
|
|
1626
|
+
// exactly that shape.
|
|
1627
|
+
for (const voice of voices) {
|
|
1628
|
+
const vn = voiceNumOf.get(voice);
|
|
1629
|
+
if (emittedClefVoices.has(vn))
|
|
1630
|
+
continue;
|
|
1631
|
+
const clef = voiceClefs.get(vn);
|
|
1632
|
+
if (!clef)
|
|
1633
|
+
continue;
|
|
1634
|
+
voice.events.unshift({ type: "context", clef });
|
|
1635
|
+
emittedClefVoices.add(vn);
|
|
1611
1636
|
}
|
|
1612
1637
|
parts.push(part);
|
|
1613
1638
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical grammar for Lilylet's transposing-clef suffix, and the only place
|
|
3
|
+
* that converts between a suffix and a semitone count.
|
|
4
|
+
*
|
|
5
|
+
* A Lilylet clef may carry a written→sounding transposition as a suffix:
|
|
6
|
+
*
|
|
7
|
+
* treble_8 an octave down (-12)
|
|
8
|
+
* treble^2 a major second up (+2)
|
|
9
|
+
* treble_5 a perfect fifth down (-7)
|
|
10
|
+
* treble_m3 a MINOR third down (-3)
|
|
11
|
+
* treble_m7 a minor seventh down (-10)
|
|
12
|
+
*
|
|
13
|
+
* `_` lowers, `^` raises, the number is a diatonic interval, and an `m` before the
|
|
14
|
+
* number selects the MINOR form of that interval. Without `m` the interval is the
|
|
15
|
+
* major/perfect one, which is what every suffix written before the `m` form existed
|
|
16
|
+
* means — so plain `_N`/`^N` keeps its original value and old documents are read
|
|
17
|
+
* exactly as before.
|
|
18
|
+
*
|
|
19
|
+
* Why `m` exists: the diatonic interval number alone cannot express a minor
|
|
20
|
+
* interval, so the semitone counts 1, 3, 8 and 10 were unreachable. Those are not
|
|
21
|
+
* exotic — a minor third is the A clarinet and a minor seventh is a common brass
|
|
22
|
+
* transposition, so an ABC `transpose=-3` had no exact suffix and was rounded to
|
|
23
|
+
* `_3` (-4), putting a one-semitone error on the sounding pitch. With `m` the only
|
|
24
|
+
* remaining gap is 6 semitones, the tritone, which is neither major/perfect nor
|
|
25
|
+
* minor (it is an augmented fourth or diminished fifth). No instrument transposes
|
|
26
|
+
* by a tritone and the shape does not occur in any corpus checked, so it is left
|
|
27
|
+
* unrepresentable rather than given a spelling that would have to be arbitrary.
|
|
28
|
+
*
|
|
29
|
+
* NOTE ON LILYPOND: LilyPond's own `\clef "treble_8"` suffix is a NOTATIONAL
|
|
30
|
+
* device — it repositions middle C on the staff and does not change sounding pitch
|
|
31
|
+
* at all (LilyPond expresses instrument transposition with the separate
|
|
32
|
+
* `\transposition <pitch>`, which takes a pitch and so reaches every semitone).
|
|
33
|
+
* Lilylet deliberately diverges: here the suffix DOES carry the sounding shift, and
|
|
34
|
+
* `m` is the extension that makes that carrying lossless. LilyPond rejects `_m3` as
|
|
35
|
+
* an unknown clef type, so a Lilylet clef using `m` is not portable to LilyPond
|
|
36
|
+
* as-is.
|
|
37
|
+
*/
|
|
38
|
+
/** A parsed transposing-clef suffix. */
|
|
39
|
+
export interface ClefSuffix {
|
|
40
|
+
/** The clef name with the suffix removed, e.g. "treble". */
|
|
41
|
+
base: string;
|
|
42
|
+
/** Diatonic interval number as written: 8 for an octave, 3 for a third. */
|
|
43
|
+
interval: number;
|
|
44
|
+
/** True when the suffix selected the minor form (`m` present). */
|
|
45
|
+
minor: boolean;
|
|
46
|
+
/** true = `^` (up), false = `_` (down). */
|
|
47
|
+
up: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse a clef string into its base name and transposition suffix.
|
|
51
|
+
* Returns undefined when the clef carries no suffix, or when the suffix is
|
|
52
|
+
* malformed (including `m` on a perfect interval such as `_m4`).
|
|
53
|
+
*/
|
|
54
|
+
export declare const parseClefSuffix: (clefStr: string) => ClefSuffix | undefined;
|
|
55
|
+
/** The clef name with any transposition suffix stripped. */
|
|
56
|
+
export declare const clefBaseName: (clefStr: string) => string;
|
|
57
|
+
/**
|
|
58
|
+
* The written→sounding semitone shift a clef string declares; 0 when it declares
|
|
59
|
+
* none (no suffix, or a suffix that cannot be interpreted).
|
|
60
|
+
*/
|
|
61
|
+
export declare const clefSuffixSemitones: (clefStr: string) => number;
|
|
62
|
+
/**
|
|
63
|
+
* The written→sounding transposition as MEI's att.transposition pair, in the
|
|
64
|
+
* `{diat, semi}` form: diatonic steps and semitones, both signed. A minor third
|
|
65
|
+
* down is `{diat: -2, semi: -3}` — the two differ, which is exactly the
|
|
66
|
+
* information the diatonic number alone cannot carry.
|
|
67
|
+
*/
|
|
68
|
+
export declare const clefSuffixTransposition: (clefStr: string) => {
|
|
69
|
+
diat: number;
|
|
70
|
+
semi: number;
|
|
71
|
+
} | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The suffix that expresses `semitones` EXACTLY, or undefined when no suffix can.
|
|
74
|
+
* Never approximates: a caller that gets undefined must handle the gap rather than
|
|
75
|
+
* emit a shift it cannot honour. The only unrepresentable interval within an octave
|
|
76
|
+
* is 6 semitones (the tritone) — see the module comment.
|
|
77
|
+
*
|
|
78
|
+
* Returns the suffix alone (e.g. "_m3"), to be appended to a clef base name.
|
|
79
|
+
*/
|
|
80
|
+
export declare const semitonesToClefSuffix: (semitones: number) => string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Apply a semitone transposition to a clef name, returning the suffixed clef.
|
|
83
|
+
* Returns undefined when the shift has no exact suffix, so the caller can decide
|
|
84
|
+
* between dropping the transposition and reporting it — silently rounding is what
|
|
85
|
+
* produced wrong sounding pitches before this module existed.
|
|
86
|
+
*/
|
|
87
|
+
export declare const withClefTransposition: (base: string, semitones: number) => string | undefined;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical grammar for Lilylet's transposing-clef suffix, and the only place
|
|
3
|
+
* that converts between a suffix and a semitone count.
|
|
4
|
+
*
|
|
5
|
+
* A Lilylet clef may carry a written→sounding transposition as a suffix:
|
|
6
|
+
*
|
|
7
|
+
* treble_8 an octave down (-12)
|
|
8
|
+
* treble^2 a major second up (+2)
|
|
9
|
+
* treble_5 a perfect fifth down (-7)
|
|
10
|
+
* treble_m3 a MINOR third down (-3)
|
|
11
|
+
* treble_m7 a minor seventh down (-10)
|
|
12
|
+
*
|
|
13
|
+
* `_` lowers, `^` raises, the number is a diatonic interval, and an `m` before the
|
|
14
|
+
* number selects the MINOR form of that interval. Without `m` the interval is the
|
|
15
|
+
* major/perfect one, which is what every suffix written before the `m` form existed
|
|
16
|
+
* means — so plain `_N`/`^N` keeps its original value and old documents are read
|
|
17
|
+
* exactly as before.
|
|
18
|
+
*
|
|
19
|
+
* Why `m` exists: the diatonic interval number alone cannot express a minor
|
|
20
|
+
* interval, so the semitone counts 1, 3, 8 and 10 were unreachable. Those are not
|
|
21
|
+
* exotic — a minor third is the A clarinet and a minor seventh is a common brass
|
|
22
|
+
* transposition, so an ABC `transpose=-3` had no exact suffix and was rounded to
|
|
23
|
+
* `_3` (-4), putting a one-semitone error on the sounding pitch. With `m` the only
|
|
24
|
+
* remaining gap is 6 semitones, the tritone, which is neither major/perfect nor
|
|
25
|
+
* minor (it is an augmented fourth or diminished fifth). No instrument transposes
|
|
26
|
+
* by a tritone and the shape does not occur in any corpus checked, so it is left
|
|
27
|
+
* unrepresentable rather than given a spelling that would have to be arbitrary.
|
|
28
|
+
*
|
|
29
|
+
* NOTE ON LILYPOND: LilyPond's own `\clef "treble_8"` suffix is a NOTATIONAL
|
|
30
|
+
* device — it repositions middle C on the staff and does not change sounding pitch
|
|
31
|
+
* at all (LilyPond expresses instrument transposition with the separate
|
|
32
|
+
* `\transposition <pitch>`, which takes a pitch and so reaches every semitone).
|
|
33
|
+
* Lilylet deliberately diverges: here the suffix DOES carry the sounding shift, and
|
|
34
|
+
* `m` is the extension that makes that carrying lossless. LilyPond rejects `_m3` as
|
|
35
|
+
* an unknown clef type, so a Lilylet clef using `m` is not portable to LilyPond
|
|
36
|
+
* as-is.
|
|
37
|
+
*/
|
|
38
|
+
// Semitones spanned by the MAJOR/PERFECT form of each diatonic step count
|
|
39
|
+
// (0 = unison, 1 = second, … 6 = seventh).
|
|
40
|
+
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
41
|
+
// Diatonic step counts that have a minor form: seconds, thirds, sixths, sevenths.
|
|
42
|
+
// Unison, fourth and fifth are perfect and take no `m`.
|
|
43
|
+
const MINOR_CAPABLE_STEPS = new Set([1, 2, 5, 6]);
|
|
44
|
+
// `_`/`^`, an optional `m`, then a positive interval number with no leading zero.
|
|
45
|
+
const SUFFIX_RE = /^(.*?)([_^])(m?)([1-9][0-9]*)$/;
|
|
46
|
+
/** Semitones spanned by a diatonic interval number, or undefined if `m` is invalid for it. */
|
|
47
|
+
const intervalSemitones = (interval, minor) => {
|
|
48
|
+
const steps = interval - 1; // diatonic steps above unison
|
|
49
|
+
const within = steps % 7;
|
|
50
|
+
if (minor && !MINOR_CAPABLE_STEPS.has(within))
|
|
51
|
+
return undefined;
|
|
52
|
+
const semis = DIATONIC_SEMITONES[within] + 12 * Math.floor(steps / 7);
|
|
53
|
+
return minor ? semis - 1 : semis;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Parse a clef string into its base name and transposition suffix.
|
|
57
|
+
* Returns undefined when the clef carries no suffix, or when the suffix is
|
|
58
|
+
* malformed (including `m` on a perfect interval such as `_m4`).
|
|
59
|
+
*/
|
|
60
|
+
export const parseClefSuffix = (clefStr) => {
|
|
61
|
+
const match = SUFFIX_RE.exec(clefStr || "");
|
|
62
|
+
if (!match)
|
|
63
|
+
return undefined;
|
|
64
|
+
const interval = parseInt(match[4], 10);
|
|
65
|
+
const minor = match[3] === "m";
|
|
66
|
+
// Reject a suffix we cannot interpret rather than silently mis-reading it.
|
|
67
|
+
if (intervalSemitones(interval, minor) === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { base: match[1], interval, minor, up: match[2] === "^" };
|
|
70
|
+
};
|
|
71
|
+
/** The clef name with any transposition suffix stripped. */
|
|
72
|
+
export const clefBaseName = (clefStr) => parseClefSuffix(clefStr)?.base ?? clefStr;
|
|
73
|
+
/**
|
|
74
|
+
* The written→sounding semitone shift a clef string declares; 0 when it declares
|
|
75
|
+
* none (no suffix, or a suffix that cannot be interpreted).
|
|
76
|
+
*/
|
|
77
|
+
export const clefSuffixSemitones = (clefStr) => {
|
|
78
|
+
const parsed = parseClefSuffix(clefStr);
|
|
79
|
+
if (!parsed)
|
|
80
|
+
return 0;
|
|
81
|
+
const semis = intervalSemitones(parsed.interval, parsed.minor);
|
|
82
|
+
return parsed.up ? semis : -semis;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The written→sounding transposition as MEI's att.transposition pair, in the
|
|
86
|
+
* `{diat, semi}` form: diatonic steps and semitones, both signed. A minor third
|
|
87
|
+
* down is `{diat: -2, semi: -3}` — the two differ, which is exactly the
|
|
88
|
+
* information the diatonic number alone cannot carry.
|
|
89
|
+
*/
|
|
90
|
+
export const clefSuffixTransposition = (clefStr) => {
|
|
91
|
+
const parsed = parseClefSuffix(clefStr);
|
|
92
|
+
if (!parsed)
|
|
93
|
+
return undefined;
|
|
94
|
+
const semis = intervalSemitones(parsed.interval, parsed.minor);
|
|
95
|
+
const sign = parsed.up ? 1 : -1;
|
|
96
|
+
return { diat: sign * (parsed.interval - 1), semi: sign * semis };
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The suffix that expresses `semitones` EXACTLY, or undefined when no suffix can.
|
|
100
|
+
* Never approximates: a caller that gets undefined must handle the gap rather than
|
|
101
|
+
* emit a shift it cannot honour. The only unrepresentable interval within an octave
|
|
102
|
+
* is 6 semitones (the tritone) — see the module comment.
|
|
103
|
+
*
|
|
104
|
+
* Returns the suffix alone (e.g. "_m3"), to be appended to a clef base name.
|
|
105
|
+
*/
|
|
106
|
+
export const semitonesToClefSuffix = (semitones) => {
|
|
107
|
+
if (!Number.isInteger(semitones) || semitones === 0)
|
|
108
|
+
return undefined;
|
|
109
|
+
const magnitude = Math.abs(semitones);
|
|
110
|
+
const sign = semitones < 0 ? "_" : "^";
|
|
111
|
+
// Search interval numbers covering the requested magnitude; octaves shift by 7
|
|
112
|
+
// diatonic steps, so an octave-and-a-bit needs interval numbers beyond 8.
|
|
113
|
+
const maxInterval = Math.floor(magnitude / 12) * 7 + 8;
|
|
114
|
+
for (let interval = 1; interval <= maxInterval; interval++) {
|
|
115
|
+
for (const minor of [false, true]) {
|
|
116
|
+
if (intervalSemitones(interval, minor) === magnitude)
|
|
117
|
+
return `${sign}${minor ? "m" : ""}${interval}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Apply a semitone transposition to a clef name, returning the suffixed clef.
|
|
124
|
+
* Returns undefined when the shift has no exact suffix, so the caller can decide
|
|
125
|
+
* between dropping the transposition and reporting it — silently rounding is what
|
|
126
|
+
* produced wrong sounding pitches before this module existed.
|
|
127
|
+
*/
|
|
128
|
+
export const withClefTransposition = (base, semitones) => {
|
|
129
|
+
if (semitones === 0)
|
|
130
|
+
return base;
|
|
131
|
+
const suffix = semitonesToClefSuffix(semitones);
|
|
132
|
+
return suffix === undefined ? undefined : base + suffix;
|
|
133
|
+
};
|
package/lib/lilylet/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from "./serializer";
|
|
|
4
4
|
export * from "./staffLayout";
|
|
5
5
|
export * from "./measureLayout";
|
|
6
6
|
export * from "./onsets";
|
|
7
|
+
export * from "./clefTransposition";
|
|
8
|
+
export * from "./transposition";
|
|
7
9
|
import * as meiEncoder from "./meiEncoder";
|
|
8
10
|
import * as musicXmlDecoder from "./musicXmlDecoder";
|
|
9
11
|
import * as lilypondEncoder from "./lilypondEncoder";
|
package/lib/lilylet/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from "./serializer.js";
|
|
|
4
4
|
export * from "./staffLayout.js";
|
|
5
5
|
export * from "./measureLayout.js";
|
|
6
6
|
export * from "./onsets.js";
|
|
7
|
+
export * from "./clefTransposition.js";
|
|
8
|
+
export * from "./transposition.js";
|
|
7
9
|
import * as meiEncoder from "./meiEncoder.js";
|
|
8
10
|
import * as musicXmlDecoder from "./musicXmlDecoder.js";
|
|
9
11
|
import * as lilypondEncoder from "./lilypondEncoder.js";
|
|
@@ -2,6 +2,7 @@ import { Clef, Accidental, OrnamentType, StemDirection, HairpinType, PedalType,
|
|
|
2
2
|
import { parseStaffLayout, StaffGroupType } from "./staffLayout.js";
|
|
3
3
|
import { gmProgramOf } from "./gmInstruments.js";
|
|
4
4
|
import { parseMeasureLayout, expandMeasureLayout, decomposeToSegments } from "./measureLayout.js";
|
|
5
|
+
import { clefBaseName, clefSuffixTransposition } from "./clefTransposition.js";
|
|
5
6
|
// MEI key signatures: positive = sharps, negative = flats
|
|
6
7
|
const KEY_SIGS = {
|
|
7
8
|
0: "0",
|
|
@@ -49,36 +50,27 @@ const CLEF_SHAPES = {
|
|
|
49
50
|
};
|
|
50
51
|
// Semitone offsets of the major/perfect intervals within one diatonic octave,
|
|
51
52
|
// indexed by diatonic step (0 = unison, 1 = 2nd, … 6 = 7th).
|
|
52
|
-
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
53
53
|
// Convert a LilyPond clef interval-number suffix into an MEI written→sounding
|
|
54
54
|
// transposition. The number N is a diatonic interval number (2 = 2nd, 3 = 3rd,
|
|
55
55
|
// 5 = 5th, 8 = octave, 15 = two octaves); "_" lowers the sounding pitch, "^"
|
|
56
56
|
// raises it. Returns { diat, semi } where diat is the diatonic step shift
|
|
57
57
|
// (N - 1, signed) and semi is the corresponding chromatic shift in semitones,
|
|
58
58
|
// extended octave-wise for compound intervals.
|
|
59
|
-
const clefTransposition = (intervalNumber, up) => {
|
|
60
|
-
const k = intervalNumber - 1; // diatonic steps
|
|
61
|
-
const semis = DIATONIC_SEMITONES[k % 7] + 12 * Math.floor(k / 7);
|
|
62
|
-
const sign = up ? 1 : -1;
|
|
63
|
-
return { diat: sign * k, semi: sign * semis };
|
|
64
|
-
};
|
|
65
59
|
// Resolve a clef string into MEI shape/line plus optional written→sounding
|
|
66
|
-
// transposition.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
60
|
+
// transposition. A "_N"/"^N" suffix transposes the clef down/up by the diatonic
|
|
61
|
+
// interval N ("treble_8" octave down, "treble_5" fifth down), and "_mN"/"^mN"
|
|
62
|
+
// selects that interval's minor form ("treble_m3" = minor third down) — see
|
|
63
|
+
// clefTransposition.ts. MEI's clef.dis only covers octave displacement
|
|
64
|
+
// (8|15|22), so all clef transposition — octaves included — is encoded uniformly
|
|
65
|
+
// via att.transposition (trans.diat / trans.semi) on staffDef. A minor interval is
|
|
66
|
+
// exactly why both fields are needed: a minor third is {diat:-2, semi:-3}.
|
|
71
67
|
const resolveClef = (clefStr) => {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const clefInfo = CLEF_SHAPES[base] || CLEF_SHAPES.treble;
|
|
75
|
-
if (!match)
|
|
76
|
-
return { shape: clefInfo.shape, line: clefInfo.line };
|
|
77
|
-
const trans = clefTransposition(Number(match[3]), match[2] === "^");
|
|
68
|
+
const clefInfo = CLEF_SHAPES[clefBaseName(clefStr)] || CLEF_SHAPES.treble;
|
|
69
|
+
const trans = clefSuffixTransposition(clefStr);
|
|
78
70
|
return {
|
|
79
71
|
shape: clefInfo.shape,
|
|
80
72
|
line: clefInfo.line,
|
|
81
|
-
trans,
|
|
73
|
+
...(trans ? { trans } : {}),
|
|
82
74
|
};
|
|
83
75
|
};
|
|
84
76
|
// Attributes for a standalone <clef> element (mid-measure clef change).
|
package/lib/lilylet/onsets.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* Shared by tools/astServer.ts (the HTTP onset API) and the clef-onset unit test.
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* The written->sounding semitone shift a clef string declares.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* A plain clef
|
|
12
|
+
* The written->sounding semitone shift a clef string declares. A "_N"/"^N" suffix transposes
|
|
13
|
+
* DOWN/UP by the diatonic interval N ("treble_8" = octave down, -12; "treble_5" = fifth down,
|
|
14
|
+
* -7), and an "m" before the number selects that interval's MINOR form ("treble_m3" = minor
|
|
15
|
+
* third down, -3). A plain clef declares 0. See clefTransposition.ts for the grammar; this is
|
|
16
|
+
* a thin alias kept because clefShift is the name the onset API and its tests already use.
|
|
16
17
|
*/
|
|
17
18
|
export declare const clefShift: (clefStr: string) => number;
|
|
18
19
|
/**
|
package/lib/lilylet/onsets.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { calculateDuration } from "./musicXmlUtils.js";
|
|
12
12
|
import { Accidental } from "./types.js";
|
|
13
|
+
import { clefSuffixSemitones } from "./clefTransposition.js";
|
|
13
14
|
// Onset resolution in ticks per quarter note. calculateDuration() rounds its result to an
|
|
14
15
|
// integer, so it must run at a resolution fine enough to represent every note value exactly:
|
|
15
16
|
// the shared DIVISIONS=4 (MusicXML export) rounds a 32nd (0.5) and a dotted 16th (1.5) to
|
|
@@ -21,22 +22,14 @@ const ACCIDENTAL_SEMITONE = {
|
|
|
21
22
|
[Accidental.natural]: 0, [Accidental.sharp]: 1, [Accidental.flat]: -1,
|
|
22
23
|
[Accidental.doubleSharp]: 2, [Accidental.doubleFlat]: -2,
|
|
23
24
|
};
|
|
24
|
-
// Diatonic-step -> semitones within an octave (unison..seventh), for "_N"/"^N" clef suffixes.
|
|
25
|
-
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
26
25
|
/**
|
|
27
|
-
* The written->sounding semitone shift a clef string declares.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* A plain clef
|
|
26
|
+
* The written->sounding semitone shift a clef string declares. A "_N"/"^N" suffix transposes
|
|
27
|
+
* DOWN/UP by the diatonic interval N ("treble_8" = octave down, -12; "treble_5" = fifth down,
|
|
28
|
+
* -7), and an "m" before the number selects that interval's MINOR form ("treble_m3" = minor
|
|
29
|
+
* third down, -3). A plain clef declares 0. See clefTransposition.ts for the grammar; this is
|
|
30
|
+
* a thin alias kept because clefShift is the name the onset API and its tests already use.
|
|
31
31
|
*/
|
|
32
|
-
export const clefShift = (clefStr) =>
|
|
33
|
-
const m = /^.*?([_^])(\d+)$/.exec(clefStr || "");
|
|
34
|
-
if (!m)
|
|
35
|
-
return 0;
|
|
36
|
-
const k = parseInt(m[2], 10) - 1; // diatonic steps above unison
|
|
37
|
-
const semis = DIATONIC_SEMITONES[k % 7] + 12 * Math.floor(k / 7);
|
|
38
|
-
return (m[1] === "^" ? 1 : -1) * semis;
|
|
39
|
-
};
|
|
32
|
+
export const clefShift = (clefStr) => clefSuffixSemitones(clefStr);
|
|
40
33
|
/**
|
|
41
34
|
* Sounding MIDI pitch of a resolved (absolute-octave) Lilylet pitch. Octave 0 = middle-C
|
|
42
35
|
* octave = MIDI 60 (see parser.resolveRelativePitch). `shift` is the active clef's
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { LilyletDoc, Pitch } from "./types";
|
|
2
|
+
/** Transpose one resolved Lilylet pitch by an integer number of semitones. */
|
|
3
|
+
export declare const transposePitch: (pitch: Pitch, semitones: number) => Pitch;
|
|
4
|
+
/** Return a new Lilylet document with every AST pitch shifted chromatically. */
|
|
5
|
+
export declare const transposeDoc: (doc: LilyletDoc, semitones: number) => LilyletDoc;
|