@k-l-lambda/lilylet 0.1.69 → 0.1.71
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/abc.d.ts +1 -0
- package/lib/abc/grammar.jison.js +4 -1
- package/lib/gmInstruments.d.ts +1 -0
- package/lib/gmInstruments.js +1 -0
- package/lib/lilylet/abcDecoder.js +225 -1
- package/lib/lilylet/gmInstruments.d.ts +1 -0
- package/lib/lilylet/gmInstruments.js +295 -0
- package/lib/lilylet/grammar.jison.js +239 -207
- package/lib/lilylet/index.d.ts +1 -0
- package/lib/lilylet/index.js +1 -0
- package/lib/lilylet/meiEncoder.js +252 -35
- package/lib/lilylet/musicXmlDecoder.js +9 -0
- package/lib/lilylet/musicXmlEncoder.js +114 -0
- package/lib/lilylet/serializer.js +13 -0
- package/lib/lilylet/staffLayout.d.ts +62 -0
- package/lib/lilylet/staffLayout.js +288 -0
- package/lib/lilylet/types.d.ts +8 -0
- package/lib/staffLayout.d.ts +1 -0
- package/lib/staffLayout.js +1 -0
- package/package.json +1 -1
- package/source/abc/abc.jison +1 -1
- package/source/abc/abc.ts +1 -0
- package/source/abc/grammar.jison.js +4 -1
- package/source/lilylet/abcDecoder.ts +231 -1
- package/source/lilylet/gmInstruments.ts +305 -0
- package/source/lilylet/grammar.jison.js +239 -207
- package/source/lilylet/index.ts +1 -0
- package/source/lilylet/lilylet.jison +28 -2
- package/source/lilylet/meiEncoder.ts +290 -34
- package/source/lilylet/musicXmlDecoder.ts +9 -0
- package/source/lilylet/musicXmlEncoder.ts +134 -0
- package/source/lilylet/serializer.ts +13 -0
- package/source/lilylet/staffLayout.ts +357 -0
- package/source/lilylet/types.ts +10 -0
package/lib/abc/abc.d.ts
CHANGED
package/lib/abc/grammar.jison.js
CHANGED
|
@@ -95,7 +95,7 @@ break;
|
|
|
95
95
|
case 4:
|
|
96
96
|
this.$ = tune($$[$0-1], $$[$0]);
|
|
97
97
|
break;
|
|
98
|
-
case 11: case 12: case 13: case 14: case 16: case
|
|
98
|
+
case 11: case 12: case 13: case 14: case 16: case 67: case 101: case 104: case 105: case 106: case 107: case 136: case 137: case 163: case 164: case 222:
|
|
99
99
|
this.$ = $$[$0-1];
|
|
100
100
|
break;
|
|
101
101
|
case 15:
|
|
@@ -104,6 +104,9 @@ break;
|
|
|
104
104
|
case 17:
|
|
105
105
|
this.$ = ({staffLayout: $$[$0]});
|
|
106
106
|
break;
|
|
107
|
+
case 20:
|
|
108
|
+
if ($$[$0-1].length) $$[$0-1][$$[$0-1].length - 1].barThruAfter = true; this.$ = $$[$0-1];
|
|
109
|
+
break;
|
|
107
110
|
case 21:
|
|
108
111
|
this.$ = staffGroup([$$[$0]]);
|
|
109
112
|
break;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/gmInstruments.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/gmInstruments.js";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import parse from "../abc/parser.js";
|
|
7
7
|
import { Phonet, Accidental, ArticulationType, OrnamentType, DynamicType, HairpinType, PedalType, NavigationMarkType, Placement, } from "./types.js";
|
|
8
|
+
import { parseStaffLayout, StaffGroupType } from "./staffLayout.js";
|
|
8
9
|
// ============ Constants ============
|
|
9
10
|
// NotaGen catalog tags appear as leading single-% comments in ABC files,
|
|
10
11
|
// in the order: period, composer, instrumentation. They are mapped to
|
|
@@ -335,6 +336,9 @@ const convertClef = (clefStr) => {
|
|
|
335
336
|
// Split off an optional ABC octave shift suffix: "treble-8", "bass+8", "treble-15".
|
|
336
337
|
// ABC: "-N" lowers the sounding pitch (small N drawn below), "+N" raises it.
|
|
337
338
|
// LilyPond/Lilylet: "_N" = below, "^N" = above. Translate the sign accordingly.
|
|
339
|
+
// (Only octave amounts 8/15 are handled here; the Lilylet "_N"/"^N" clef suffix
|
|
340
|
+
// itself accepts arbitrary diatonic intervals — see meiEncoder.resolveClef — and a
|
|
341
|
+
// voice's transpose= property is folded into the same suffix via transposeClefSuffix.)
|
|
338
342
|
const shift = clefStr?.match(/^(.*?)([+-])(8|15)$/);
|
|
339
343
|
const base = shift ? shift[1] : clefStr;
|
|
340
344
|
let resolved;
|
|
@@ -355,6 +359,31 @@ const convertClef = (clefStr) => {
|
|
|
355
359
|
resolved += (shift[2] === "-" ? "_" : "^") + shift[3];
|
|
356
360
|
return resolved;
|
|
357
361
|
};
|
|
362
|
+
/**
|
|
363
|
+
* Fold an ABC voice `transpose=N` property (a written→sounding shift in
|
|
364
|
+
* SEMITONES) into the Lilylet clef-suffix form `_M` / `^M`, where M is a
|
|
365
|
+
* diatonic interval number. ABC carries only semitones, so the diatonic
|
|
366
|
+
* interval is approximated by the nearest scale-step count
|
|
367
|
+
* (steps = round(semi * 7/12), interval number = |steps| + 1); `_` lowers,
|
|
368
|
+
* `^` raises. The Lilylet→MEI encoder later expands the suffix back into
|
|
369
|
+
* trans.diat / trans.semi (see meiEncoder.resolveClef). This is exact when the
|
|
370
|
+
* semitone count is a major/perfect interval (e.g. -2, -9, +2, ±12) and a
|
|
371
|
+
* nearest-interval approximation otherwise.
|
|
372
|
+
*/
|
|
373
|
+
const transposeClefSuffix = (clef, semitones) => {
|
|
374
|
+
if (!clef || !semitones)
|
|
375
|
+
return clef;
|
|
376
|
+
// A clef that already carries an octave suffix (treble_8 etc.) is left as-is;
|
|
377
|
+
// stacking another shift on top is not meaningful for ABC sources.
|
|
378
|
+
if (/[_^]\d+$/.test(clef))
|
|
379
|
+
return clef;
|
|
380
|
+
const steps = Math.round((semitones * 7) / 12);
|
|
381
|
+
if (steps === 0)
|
|
382
|
+
return clef;
|
|
383
|
+
const num = Math.abs(steps) + 1;
|
|
384
|
+
const suffix = (steps < 0 ? "_" : "^") + num;
|
|
385
|
+
return (clef + suffix);
|
|
386
|
+
};
|
|
358
387
|
/**
|
|
359
388
|
* Convert ABC barline to Lilylet barline style
|
|
360
389
|
*/
|
|
@@ -431,6 +460,158 @@ const parseScoreLayout = (headers) => {
|
|
|
431
460
|
}
|
|
432
461
|
return voiceMap.size > 0 ? voiceMap : null;
|
|
433
462
|
};
|
|
463
|
+
/**
|
|
464
|
+
* Translate an ABC %%score layout into the equivalent lilylet staves expression.
|
|
465
|
+
*
|
|
466
|
+
* The two models differ in their leaf unit: ABC %%score is VOICE-leaf (an arc `( … )`
|
|
467
|
+
* collapses several voices onto one staff), whereas lilylet staves is STAFF-leaf. So
|
|
468
|
+
* each ABC staff becomes exactly one lilylet staff id, named (per spec) by the FIRST
|
|
469
|
+
* voice token inside its arc.
|
|
470
|
+
*
|
|
471
|
+
* Bracket mapping (ABC → lilylet):
|
|
472
|
+
* ( … ) arc → one staff leaf (id = first voice) e.g. (1 3) → "1"
|
|
473
|
+
* bare leaf → one staff leaf (id = that voice) e.g. 7 → "7"
|
|
474
|
+
* { … } curly → brace (grand staff) → { … }
|
|
475
|
+
* [ … ] square → bracket (orchestral) → < … >
|
|
476
|
+
*
|
|
477
|
+
* Conjunction between two siblings: a trailing `|` in %%score means barlines run through
|
|
478
|
+
* the two staves, mapped to the solid conjunction `-`; otherwise the blank `,` is used.
|
|
479
|
+
*/
|
|
480
|
+
const abcLayoutToStaves = (layout) => {
|
|
481
|
+
// First voice token under a node (the staff's lilylet id).
|
|
482
|
+
const firstVoice = (node) => {
|
|
483
|
+
if (typeof node === "string")
|
|
484
|
+
return node;
|
|
485
|
+
for (const item of node.items || []) {
|
|
486
|
+
const v = firstVoice(item);
|
|
487
|
+
if (v !== null)
|
|
488
|
+
return v;
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
};
|
|
492
|
+
// A node is a single staff (an arc, or a bare leaf) iff it has no curly/square nesting.
|
|
493
|
+
const isStaffLeaf = (node) => {
|
|
494
|
+
if (typeof node === "string")
|
|
495
|
+
return true;
|
|
496
|
+
if (node.bound === "curly" || node.bound === "square")
|
|
497
|
+
return false;
|
|
498
|
+
// arc or unbounded: a staff only if every descendant is too (no nested groups)
|
|
499
|
+
return (node.items || []).every(isStaffLeaf);
|
|
500
|
+
};
|
|
501
|
+
// A square group maps to lilylet Bracket `<>` at the TOP level, but to lilylet Square
|
|
502
|
+
// `[]` when nested inside another group — e.g. ABC `[[1 2] 3 | 4]` → `<[1,2]3-4>`.
|
|
503
|
+
// A curly group always maps to Brace `{}`. `nested` is false for a top-level entry.
|
|
504
|
+
const emit = (node, nested) => {
|
|
505
|
+
if (isStaffLeaf(node))
|
|
506
|
+
return firstVoice(node) || "";
|
|
507
|
+
const group = node;
|
|
508
|
+
const open = group.bound === "curly" ? "{" : (group.bound === "square" && nested) ? "[" : "<";
|
|
509
|
+
const close = group.bound === "curly" ? "}" : (group.bound === "square" && nested) ? "]" : ">";
|
|
510
|
+
const items = group.items || [];
|
|
511
|
+
let inner = "";
|
|
512
|
+
items.forEach((item, i) => {
|
|
513
|
+
inner += emit(item, true);
|
|
514
|
+
if (i < items.length - 1) {
|
|
515
|
+
// A Blank separator (',') is only needed between two bare staff leaves; a
|
|
516
|
+
// grouped neighbour's bracket already delimits the slot, so suppress it there
|
|
517
|
+
// (giving `[1,2]3` not `[1,2],3`). A Solid join ('-', barThru) is always kept.
|
|
518
|
+
const next = items[i + 1];
|
|
519
|
+
if (item.barThruAfter)
|
|
520
|
+
inner += "-";
|
|
521
|
+
else if (isStaffLeaf(item) && isStaffLeaf(next))
|
|
522
|
+
inner += ",";
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return `${open}${inner}${close}`;
|
|
526
|
+
};
|
|
527
|
+
const tops = layout.map((top, i) => {
|
|
528
|
+
let s = emit(top, false);
|
|
529
|
+
// A bare top-level staff leaf (e.g. the `9` in `[ … ] 9 [ … ]`) still occupies a slot;
|
|
530
|
+
// emit() already yields its id with no wrapper, which is the desired output.
|
|
531
|
+
return { s, barThru: !!top.barThruAfter, isLast: i === layout.length - 1 };
|
|
532
|
+
});
|
|
533
|
+
let out = "";
|
|
534
|
+
tops.forEach((t, i) => {
|
|
535
|
+
out += t.s;
|
|
536
|
+
if (i < tops.length - 1)
|
|
537
|
+
out += t.barThru ? " - " : " ";
|
|
538
|
+
});
|
|
539
|
+
out = out.trim();
|
|
540
|
+
return out.length > 0 ? out : null;
|
|
541
|
+
};
|
|
542
|
+
/**
|
|
543
|
+
* Translate ABC voice instrument names (V:n nm="…" snm="…") into a lilylet `instruments`
|
|
544
|
+
* map keyed by staff-layout group key.
|
|
545
|
+
*
|
|
546
|
+
* ABC names sit on individual voices; lilylet staves are staff-leaf (an arc's first voice
|
|
547
|
+
* is the staff id). So we first collect a per-staff name from the staff-id voice. Then,
|
|
548
|
+
* per the user's rule: if a GROUP carries an instrument only on its first staff and the
|
|
549
|
+
* rest of the group is unnamed, the name belongs to the whole group — hoist it to the
|
|
550
|
+
* group key and drop it from the leaf. This matches engraving convention (one bracketed
|
|
551
|
+
* section, e.g. "Violini", named once for the group rather than on its top staff).
|
|
552
|
+
*
|
|
553
|
+
* `voiceInstr` maps an ABC voice number to its {name, shortName}. The layout's staff ids
|
|
554
|
+
* are arc-first voice numbers, so a staff id "5" looks up voice 5's name.
|
|
555
|
+
*/
|
|
556
|
+
const abcInstrumentsToLilylet = (stavesCode, voiceInstr) => {
|
|
557
|
+
const layout = parseStaffLayout(stavesCode);
|
|
558
|
+
const result = {};
|
|
559
|
+
// Per-staff instrument, keyed by staff id (the arc-first voice number as a string).
|
|
560
|
+
const staffInstr = new Map();
|
|
561
|
+
for (const id of layout.staffIds) {
|
|
562
|
+
const v = parseInt(id, 10);
|
|
563
|
+
if (!isNaN(v) && voiceInstr.has(v))
|
|
564
|
+
staffInstr.set(id, voiceInstr.get(v));
|
|
565
|
+
}
|
|
566
|
+
// First staff id under a group (in layout order).
|
|
567
|
+
const firstStaffId = (group) => {
|
|
568
|
+
if (group.staff)
|
|
569
|
+
return group.staff;
|
|
570
|
+
for (const sub of group.subs || []) {
|
|
571
|
+
const id = firstStaffId(sub);
|
|
572
|
+
if (id !== undefined)
|
|
573
|
+
return id;
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
};
|
|
577
|
+
// All staff ids under a group except the first.
|
|
578
|
+
const restStaffIds = (group) => {
|
|
579
|
+
const all = [];
|
|
580
|
+
const collect = (g) => {
|
|
581
|
+
if (g.staff)
|
|
582
|
+
all.push(g.staff);
|
|
583
|
+
(g.subs || []).forEach(collect);
|
|
584
|
+
};
|
|
585
|
+
collect(group);
|
|
586
|
+
return all.slice(1);
|
|
587
|
+
};
|
|
588
|
+
// Walk groups top-down; hoist a first-staff-only name onto the group, else keep leaves.
|
|
589
|
+
const walk = (group) => {
|
|
590
|
+
const isLeaf = !!group.staff && (!group.subs || group.subs.length === 0);
|
|
591
|
+
if (isLeaf) {
|
|
592
|
+
// A plain leaf keeps its own name (assigned in the flush pass below).
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const first = firstStaffId(group);
|
|
596
|
+
const firstInstr = first !== undefined ? staffInstr.get(first) : undefined;
|
|
597
|
+
const rest = restStaffIds(group);
|
|
598
|
+
const restNamed = rest.some(id => staffInstr.has(id));
|
|
599
|
+
// Group with a real grouping symbol, named only on its first staff → hoist.
|
|
600
|
+
const hasSymbol = group.type !== StaffGroupType.Default;
|
|
601
|
+
if (hasSymbol && firstInstr && !restNamed && group.key !== undefined) {
|
|
602
|
+
result[group.key] = firstInstr;
|
|
603
|
+
staffInstr.delete(first); // consumed by the group
|
|
604
|
+
return; // children below the hoisted name carry nothing
|
|
605
|
+
}
|
|
606
|
+
for (const sub of group.subs || [])
|
|
607
|
+
walk(sub);
|
|
608
|
+
};
|
|
609
|
+
walk(layout.group);
|
|
610
|
+
// Flush any per-staff names that weren't hoisted onto a group.
|
|
611
|
+
for (const [id, instr] of staffInstr)
|
|
612
|
+
result[id] = instr;
|
|
613
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
614
|
+
};
|
|
434
615
|
// ============ Marks/Decorations Conversion ============
|
|
435
616
|
const convertArticulationMark = (artName) => {
|
|
436
617
|
switch (artName) {
|
|
@@ -984,7 +1165,17 @@ const decodeTune = (tune, options = {}) => {
|
|
|
984
1165
|
properties: voiceValue?.properties,
|
|
985
1166
|
});
|
|
986
1167
|
if (clefStr) {
|
|
987
|
-
|
|
1168
|
+
let clef = convertClef(clefStr);
|
|
1169
|
+
// Fold a voice transpose= (semitones) into the clef suffix so it
|
|
1170
|
+
// survives to MEI as trans.diat/trans.semi (transposing instruments
|
|
1171
|
+
// like "Clarinet transpose=-2", "Horn transpose=-9").
|
|
1172
|
+
const transposeRaw = voiceValue?.properties?.transpose;
|
|
1173
|
+
const transposeSemi = typeof transposeRaw === "number"
|
|
1174
|
+
? transposeRaw
|
|
1175
|
+
: (transposeRaw != null ? Number(transposeRaw) : NaN);
|
|
1176
|
+
if (clef && Number.isFinite(transposeSemi) && transposeSemi !== 0) {
|
|
1177
|
+
clef = transposeClefSuffix(clef, transposeSemi);
|
|
1178
|
+
}
|
|
988
1179
|
if (clef)
|
|
989
1180
|
voiceClefs.set(voiceId, clef);
|
|
990
1181
|
}
|
|
@@ -995,6 +1186,39 @@ const decodeTune = (tune, options = {}) => {
|
|
|
995
1186
|
}
|
|
996
1187
|
// Parse score layout
|
|
997
1188
|
const scoreLayout = parseScoreLayout(headers);
|
|
1189
|
+
// Translate the ABC %%score layout into a lilylet staves expression (staff-leaf model).
|
|
1190
|
+
const layoutHeader = headers.find((h) => h.staffLayout);
|
|
1191
|
+
if (layoutHeader) {
|
|
1192
|
+
const staves = abcLayoutToStaves(layoutHeader.staffLayout);
|
|
1193
|
+
if (staves)
|
|
1194
|
+
metadata.staves = staves;
|
|
1195
|
+
// Translate per-voice nm/snm into a lilylet instruments map. Collect names by ABC
|
|
1196
|
+
// voice number (the staff id is the arc-first voice), then let abcInstrumentsToLilylet
|
|
1197
|
+
// apply the first-staff-only → whole-group hoisting rule.
|
|
1198
|
+
if (metadata.staves) {
|
|
1199
|
+
const voiceInstr = new Map();
|
|
1200
|
+
for (const [vid, config] of voiceConfigs) {
|
|
1201
|
+
const props = config.properties;
|
|
1202
|
+
if (!props)
|
|
1203
|
+
continue;
|
|
1204
|
+
const name = props.nm ?? props.name;
|
|
1205
|
+
if (typeof name !== "string" || !name.length)
|
|
1206
|
+
continue;
|
|
1207
|
+
const v = typeof vid === "number" ? vid : parseInt(String(vid), 10);
|
|
1208
|
+
if (isNaN(v))
|
|
1209
|
+
continue;
|
|
1210
|
+
const short = props.snm ?? props.sname;
|
|
1211
|
+
voiceInstr.set(v, typeof short === "string" && short.length
|
|
1212
|
+
? { name, shortName: short }
|
|
1213
|
+
: { name });
|
|
1214
|
+
}
|
|
1215
|
+
if (voiceInstr.size > 0) {
|
|
1216
|
+
const instruments = abcInstrumentsToLilylet(metadata.staves, voiceInstr);
|
|
1217
|
+
if (instruments)
|
|
1218
|
+
metadata.instruments = instruments;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
998
1222
|
// Group measures by voice
|
|
999
1223
|
// ABC measures contain BarPatches, each with a voice control V:n
|
|
1000
1224
|
const measures = body.measures;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const gmProgramOf: (name: string | undefined | null) => number | undefined;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// General MIDI program lookup: instrument name → GM program number (0–127).
|
|
2
|
+
//
|
|
3
|
+
// Verovio's MIDI export honors ONLY the numeric `@midi.instrnum` on an MEI
|
|
4
|
+
// <instrDef> (the GM-name attribute `@midi.instrname` is parsed but never used
|
|
5
|
+
// for MIDI). lilylet already carries human instrument names (from ABC voice
|
|
6
|
+
// names, MusicXML part names, etc.) in metadata.instruments; this table maps
|
|
7
|
+
// those names to GM programs so the MEI encoder can emit <instrDef midi.instrnum>
|
|
8
|
+
// and multi-instrument scores get distinct timbres instead of all-piano.
|
|
9
|
+
//
|
|
10
|
+
// The name set is seeded from the notagen dataset (Piano, Violins, Viola,
|
|
11
|
+
// Violoncellos, Oboe, Horn, Flute, Clarinet, Bassoon, Violin, Trombone,
|
|
12
|
+
// Timpani, Voice, Bass, Trumpet, Harp, Contrabasses, Vocal, Organ, …) plus
|
|
13
|
+
// common GM aliases, then matched through a normalizer that handles plurals
|
|
14
|
+
// ("Violins" → violin) and trailing part numbers ("Violin I", "Horn 2").
|
|
15
|
+
// Normalized name → GM program (0-based). Keys are lowercase, singular,
|
|
16
|
+
// whitespace-collapsed. Plural/number variants are resolved by the normalizer.
|
|
17
|
+
const GM_PROGRAMS = {
|
|
18
|
+
// Piano (0–7)
|
|
19
|
+
"piano": 0,
|
|
20
|
+
"acoustic grand piano": 0,
|
|
21
|
+
"grand piano": 0,
|
|
22
|
+
"bright acoustic piano": 1,
|
|
23
|
+
"electric piano": 4,
|
|
24
|
+
"harpsichord": 6,
|
|
25
|
+
"clavichord": 7,
|
|
26
|
+
"clavi": 7,
|
|
27
|
+
// Chromatic percussion (8–15)
|
|
28
|
+
"celesta": 8,
|
|
29
|
+
"glockenspiel": 9,
|
|
30
|
+
"music box": 10,
|
|
31
|
+
"vibraphone": 11,
|
|
32
|
+
"marimba": 12,
|
|
33
|
+
"xylophone": 13,
|
|
34
|
+
"tubular bells": 14,
|
|
35
|
+
"dulcimer": 15,
|
|
36
|
+
// Organ (16–23)
|
|
37
|
+
"organ": 19,
|
|
38
|
+
"hammond organ": 16,
|
|
39
|
+
"percussive organ": 17,
|
|
40
|
+
"rock organ": 18,
|
|
41
|
+
"church organ": 19,
|
|
42
|
+
"pipe organ": 19,
|
|
43
|
+
"reed organ": 20,
|
|
44
|
+
"accordion": 21,
|
|
45
|
+
"harmonica": 22,
|
|
46
|
+
// Guitar (24–31)
|
|
47
|
+
"guitar": 24,
|
|
48
|
+
"acoustic guitar": 24,
|
|
49
|
+
"nylon guitar": 24,
|
|
50
|
+
"steel guitar": 25,
|
|
51
|
+
"electric guitar": 27,
|
|
52
|
+
"guitarre": 24, // fr./de. guitar
|
|
53
|
+
"gitarre": 24, // de.
|
|
54
|
+
"chitarra": 24, // it.
|
|
55
|
+
// Bass (32–39) — orchestral "Bass" means double bass (Contrabass, 43); the
|
|
56
|
+
// electric/acoustic bass-guitar programs live here but are not the default.
|
|
57
|
+
"acoustic bass": 32,
|
|
58
|
+
"electric bass": 33,
|
|
59
|
+
"fretless bass": 35,
|
|
60
|
+
"basso": 43, // it. bass → double bass
|
|
61
|
+
"basse": 43, // fr.
|
|
62
|
+
"bassi": 43, // it. pl.
|
|
63
|
+
"bas": 43, // de./nl. abbrev
|
|
64
|
+
// Strings (40–47)
|
|
65
|
+
"violin": 40,
|
|
66
|
+
"viola": 41,
|
|
67
|
+
"cello": 42,
|
|
68
|
+
"violoncello": 42,
|
|
69
|
+
"contrabass": 43,
|
|
70
|
+
"double bass": 43,
|
|
71
|
+
"bass": 43,
|
|
72
|
+
"tremolo strings": 44,
|
|
73
|
+
"pizzicato strings": 45,
|
|
74
|
+
"harp": 46,
|
|
75
|
+
"orchestral harp": 46,
|
|
76
|
+
"timpani": 47,
|
|
77
|
+
// Ensemble (48–55)
|
|
78
|
+
"strings": 48,
|
|
79
|
+
"string ensemble": 48,
|
|
80
|
+
"string orchestra": 48,
|
|
81
|
+
"synth strings": 50,
|
|
82
|
+
"voice": 52,
|
|
83
|
+
"vocal": 52,
|
|
84
|
+
"voices": 52,
|
|
85
|
+
"choir": 52,
|
|
86
|
+
"choir aahs": 52,
|
|
87
|
+
"soprano": 52,
|
|
88
|
+
"alto": 52,
|
|
89
|
+
"tenor": 52,
|
|
90
|
+
"bass voice": 52,
|
|
91
|
+
"orchestra hit": 55,
|
|
92
|
+
// Brass (56–63)
|
|
93
|
+
"trumpet": 56,
|
|
94
|
+
"trombone": 57,
|
|
95
|
+
"tuba": 58,
|
|
96
|
+
"muted trumpet": 59,
|
|
97
|
+
"horn": 60,
|
|
98
|
+
"french horn": 60,
|
|
99
|
+
"brass": 61,
|
|
100
|
+
"brass section": 61,
|
|
101
|
+
// Reed (64–71)
|
|
102
|
+
"soprano sax": 64,
|
|
103
|
+
"alto sax": 65,
|
|
104
|
+
"tenor sax": 66,
|
|
105
|
+
"baritone sax": 67,
|
|
106
|
+
"saxophone": 66,
|
|
107
|
+
"sax": 66,
|
|
108
|
+
"oboe": 68,
|
|
109
|
+
"english horn": 69,
|
|
110
|
+
"cor anglais": 69,
|
|
111
|
+
"bassoon": 70,
|
|
112
|
+
"clarinet": 71,
|
|
113
|
+
// Pipe (72–79)
|
|
114
|
+
"piccolo": 72,
|
|
115
|
+
"flute": 73,
|
|
116
|
+
"recorder": 74,
|
|
117
|
+
"pan flute": 75,
|
|
118
|
+
// --- Foreign-language names, abbreviations and common spelling variants,
|
|
119
|
+
// harvested from the notagen corpus. Mapped to the nearest GM program.
|
|
120
|
+
// Keyboard
|
|
121
|
+
"pianoforte": 0,
|
|
122
|
+
"fortepiano": 0,
|
|
123
|
+
"klavier": 0,
|
|
124
|
+
"keyboard": 0,
|
|
125
|
+
"cembalo": 6, // it. harpsichord
|
|
126
|
+
"clavicembalo": 6,
|
|
127
|
+
"harpichord": 6, // misspelling
|
|
128
|
+
"organo": 19, // it. organ
|
|
129
|
+
"orgel": 19, // de. organ
|
|
130
|
+
// Strings (it./de./fr./variants)
|
|
131
|
+
"violino": 40,
|
|
132
|
+
"violini": 40,
|
|
133
|
+
"violine": 40, // de.
|
|
134
|
+
"violinen": 40,
|
|
135
|
+
"violon": 40, // fr.
|
|
136
|
+
"violons": 40,
|
|
137
|
+
"violn": 40, // abbrev/OCR variant
|
|
138
|
+
"violno": 40, // OCR variant
|
|
139
|
+
"viole": 41, // it. violas (also fr. "viole")
|
|
140
|
+
"bratsche": 41, // de. viola
|
|
141
|
+
"celli": 42,
|
|
142
|
+
"violoncelli": 42,
|
|
143
|
+
"violoncelle": 42, // fr.
|
|
144
|
+
"violoncelles": 42,
|
|
145
|
+
"violonchelo": 42, // es.
|
|
146
|
+
"soloncello": 42, // OCR variant of violoncello
|
|
147
|
+
"gambe": 42, // fr. viola da gamba
|
|
148
|
+
"gamba": 42, // viola da gamba ≈ cello
|
|
149
|
+
"viola da gamba": 42,
|
|
150
|
+
"contrabasso": 43, // it.
|
|
151
|
+
"contrabassi": 43,
|
|
152
|
+
"contrabbasso": 43, // it.
|
|
153
|
+
"contra-basso": 43,
|
|
154
|
+
"contrabajo": 43, // es.
|
|
155
|
+
"kontrabass": 43, // de.
|
|
156
|
+
"kontrabasse": 43, // de. pl.
|
|
157
|
+
"kontrabasso": 43,
|
|
158
|
+
"contrebasse": 43, // fr.
|
|
159
|
+
"violone": 43, // large bass viol ≈ contrabass
|
|
160
|
+
"arpa": 46, // it./es. harp
|
|
161
|
+
"harfe": 46, // de. harp
|
|
162
|
+
"pauken": 47, // de. timpani
|
|
163
|
+
// Voice (it./de./fr.)
|
|
164
|
+
"canto": 52, // it.
|
|
165
|
+
"coro": 52, // it. choir
|
|
166
|
+
"chorus": 52,
|
|
167
|
+
"chorale": 52,
|
|
168
|
+
"sopran": 52, // de.
|
|
169
|
+
"contralto": 52, // it. alto
|
|
170
|
+
"tenore": 52, // it.
|
|
171
|
+
"tenori": 52,
|
|
172
|
+
"gesang": 52, // de. voice
|
|
173
|
+
"singstimme": 52, // de. voice
|
|
174
|
+
"voce": 52, // it.
|
|
175
|
+
"voix": 52, // fr.
|
|
176
|
+
"chanto": 52, // OCR variant of canto
|
|
177
|
+
"women": 52, // women's voices
|
|
178
|
+
"contra-fagotto": 70, // hyphenated contrabassoon ≈ bassoon
|
|
179
|
+
// Brass
|
|
180
|
+
"tromboni": 57, // it. trombones
|
|
181
|
+
"posaune": 57, // de. trombone
|
|
182
|
+
"posaunen": 57,
|
|
183
|
+
"trombe": 56, // it. trumpets
|
|
184
|
+
"tromba": 56, // it. trumpet
|
|
185
|
+
"trompete": 56, // de. trumpet
|
|
186
|
+
"trompeten": 56,
|
|
187
|
+
"trompette": 56, // fr. trumpet
|
|
188
|
+
"cornetto": 56, // historical cornett ≈ trumpet
|
|
189
|
+
"cornettino": 56,
|
|
190
|
+
"corno": 60, // it. horn
|
|
191
|
+
"corni": 60, // it. horns
|
|
192
|
+
// Reed (it./de./fr.)
|
|
193
|
+
"oboi": 68, // it. oboes
|
|
194
|
+
"oboen": 68, // de.
|
|
195
|
+
"hautbois": 68, // fr. oboe
|
|
196
|
+
"corno inglese": 69, // it. english horn
|
|
197
|
+
"inglese": 69, // "corno inglese" trailing word fallback also covers it
|
|
198
|
+
"ingles": 69, // es. variant
|
|
199
|
+
"fagotto": 70, // it. bassoon
|
|
200
|
+
"fagotti": 70,
|
|
201
|
+
"fagott": 70, // de.
|
|
202
|
+
"fagotte": 70, // de. pl.
|
|
203
|
+
"fagot": 70, // es.
|
|
204
|
+
"basson": 70, // fr. bassoon
|
|
205
|
+
"bassons": 70,
|
|
206
|
+
"contrafagotto": 70, // it. contrabassoon ≈ bassoon timbre
|
|
207
|
+
"contrabassoon": 70,
|
|
208
|
+
"klarinette": 71, // de. clarinet
|
|
209
|
+
"clarinetto": 71, // it.
|
|
210
|
+
"clarinetti": 71,
|
|
211
|
+
"clarinette": 71, // fr.
|
|
212
|
+
// Pipe (it./de.)
|
|
213
|
+
"flauto": 73, // it. flute
|
|
214
|
+
"flauti": 73, // it. flutes
|
|
215
|
+
"flote": 73, // de. Flöte (diacritics stripped by the normalizer)
|
|
216
|
+
"floten": 73, // de. Flöten
|
|
217
|
+
"traverso": 73, // baroque transverse flute
|
|
218
|
+
"flauto traverso": 73,
|
|
219
|
+
};
|
|
220
|
+
// Normalize an instrument name for lookup: lowercase, turn literal "\n" escapes
|
|
221
|
+
// and real newlines into spaces, strip diacritics (Flöte→flote, Hautböis→...),
|
|
222
|
+
// drop a trailing part designator (roman numeral or arabic number — "Violin I",
|
|
223
|
+
// "Horn 2", "Oboe II"), collapse whitespace.
|
|
224
|
+
const normalizeInstrumentName = (raw) => {
|
|
225
|
+
let s = raw.toLowerCase().trim();
|
|
226
|
+
s = s.replace(/\\n/g, " "); // literal backslash-n escape → space
|
|
227
|
+
s = s.normalize("NFD").replace(/[̀-ͯ]/g, ""); // strip diacritics
|
|
228
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
229
|
+
s = s.replace(/\s+(?:[ivx]+|\d+)\.?$/i, "").trim();
|
|
230
|
+
return s;
|
|
231
|
+
};
|
|
232
|
+
// Choral single-letter voice-part abbreviations → "Voice" (GM 52). Matched ONLY
|
|
233
|
+
// against the whole name, never per-word: a bare "S"/"A"/"T"/"B" staff label in a
|
|
234
|
+
// chorale means Soprano/Alto/Tenor/Bass, but the same letters appear as key
|
|
235
|
+
// designators in "Clarinet in B", "Horn in F", "Trumpet in C" — so these must not
|
|
236
|
+
// enter GM_PROGRAMS where the word-scan would misread them.
|
|
237
|
+
const SATB_VOICE = {
|
|
238
|
+
"s": 52,
|
|
239
|
+
"a": 52,
|
|
240
|
+
"t": 52,
|
|
241
|
+
"b": 52,
|
|
242
|
+
};
|
|
243
|
+
// Look up a single normalized name: exact match, else de-pluralized
|
|
244
|
+
// ("violins"→violin, "violoncellos"→violoncello, "contrabasses"→contrabass),
|
|
245
|
+
// else with a trailing attached part-number stripped ("violin1"→violin,
|
|
246
|
+
// "violino2"→violino).
|
|
247
|
+
const lookupNormalized = (norm) => {
|
|
248
|
+
if (norm in GM_PROGRAMS)
|
|
249
|
+
return GM_PROGRAMS[norm];
|
|
250
|
+
// Try "-es" before "-s".
|
|
251
|
+
if (norm.endsWith("es")) {
|
|
252
|
+
const sing = norm.slice(0, -2);
|
|
253
|
+
if (sing in GM_PROGRAMS)
|
|
254
|
+
return GM_PROGRAMS[sing];
|
|
255
|
+
}
|
|
256
|
+
if (norm.endsWith("s")) {
|
|
257
|
+
const sing = norm.slice(0, -1);
|
|
258
|
+
if (sing in GM_PROGRAMS)
|
|
259
|
+
return GM_PROGRAMS[sing];
|
|
260
|
+
}
|
|
261
|
+
// Attached trailing digits ("violin1", "violino2"): strip and retry.
|
|
262
|
+
const deNum = norm.replace(/\d+$/, "");
|
|
263
|
+
if (deNum !== norm && deNum in GM_PROGRAMS)
|
|
264
|
+
return GM_PROGRAMS[deNum];
|
|
265
|
+
return undefined;
|
|
266
|
+
};
|
|
267
|
+
// Resolve an instrument name to a GM program number (0–127), or undefined if no
|
|
268
|
+
// confident match (caller then omits <instrDef>, leaving Verovio's default).
|
|
269
|
+
//
|
|
270
|
+
// Match priority: the full normalized string first (including the SATB
|
|
271
|
+
// single-letter voice abbreviations), then individual words from the last toward
|
|
272
|
+
// the first. Multi-word names ("Singstimme Voice", "First Violins", "Solo Flute")
|
|
273
|
+
// usually put the instrument at the end, so the trailing word is tried before
|
|
274
|
+
// earlier qualifier words. Each word attempt runs through the de-plural path
|
|
275
|
+
// (lookupNormalized); SATB letters are intentionally NOT part of the word scan.
|
|
276
|
+
export const gmProgramOf = (name) => {
|
|
277
|
+
if (!name)
|
|
278
|
+
return undefined;
|
|
279
|
+
const norm = normalizeInstrumentName(name);
|
|
280
|
+
const direct = lookupNormalized(norm);
|
|
281
|
+
if (direct !== undefined)
|
|
282
|
+
return direct;
|
|
283
|
+
// Whole-name-only: a lone S/A/T/B is a chorale voice part.
|
|
284
|
+
if (norm in SATB_VOICE)
|
|
285
|
+
return SATB_VOICE[norm];
|
|
286
|
+
const words = norm.split(" ");
|
|
287
|
+
if (words.length > 1) {
|
|
288
|
+
for (let i = words.length - 1; i >= 0; i--) {
|
|
289
|
+
const hit = lookupNormalized(words[i]);
|
|
290
|
+
if (hit !== undefined)
|
|
291
|
+
return hit;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
};
|