@scorelabs/core 1.0.1
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/dist/importers/MusicXMLParser.d.ts +36 -0
- package/dist/importers/MusicXMLParser.js +610 -0
- package/dist/importers/index.d.ts +1 -0
- package/dist/importers/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/models/Instrument.d.ts +26 -0
- package/dist/models/Instrument.js +56 -0
- package/dist/models/Measure.d.ts +65 -0
- package/dist/models/Measure.js +291 -0
- package/dist/models/Note.d.ts +116 -0
- package/dist/models/Note.js +259 -0
- package/dist/models/NoteSet.d.ts +73 -0
- package/dist/models/NoteSet.js +184 -0
- package/dist/models/Part.d.ts +36 -0
- package/dist/models/Part.js +89 -0
- package/dist/models/Pitch.d.ts +20 -0
- package/dist/models/Pitch.js +110 -0
- package/dist/models/Score.d.ts +71 -0
- package/dist/models/Score.js +284 -0
- package/dist/models/Staff.d.ts +36 -0
- package/dist/models/Staff.js +89 -0
- package/dist/models/index.d.ts +9 -0
- package/dist/models/index.js +9 -0
- package/dist/models/types.d.ts +202 -0
- package/dist/models/types.js +205 -0
- package/package.json +34 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ScoreJSON } from '../models/Score';
|
|
2
|
+
/**
|
|
3
|
+
* MusicXML Parser
|
|
4
|
+
*
|
|
5
|
+
* Shared implementation for both frontend and backend.
|
|
6
|
+
* Expects a DOMParser-compatible constructor in Node.js environments.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MusicXMLParser {
|
|
9
|
+
private currentKeyFifths;
|
|
10
|
+
private currentBeats;
|
|
11
|
+
private currentBeatType;
|
|
12
|
+
private instrumentPitchMap;
|
|
13
|
+
private _domParser;
|
|
14
|
+
constructor(domParserInstance?: any);
|
|
15
|
+
private parseFromString;
|
|
16
|
+
parseBinary(data: ArrayBuffer): Promise<ScoreJSON>;
|
|
17
|
+
parse(xmlString: string): ScoreJSON;
|
|
18
|
+
private parseSubtitle;
|
|
19
|
+
private parseTitle;
|
|
20
|
+
private parseComposer;
|
|
21
|
+
private parsePartList;
|
|
22
|
+
private parseParts;
|
|
23
|
+
private parsePart;
|
|
24
|
+
private parseNote;
|
|
25
|
+
private parseHarmony;
|
|
26
|
+
private parseBarlines;
|
|
27
|
+
private postProcess;
|
|
28
|
+
private cleanAccidentals;
|
|
29
|
+
private pitchToMidi;
|
|
30
|
+
private mapDuration;
|
|
31
|
+
private mapClef;
|
|
32
|
+
private mapAccidental;
|
|
33
|
+
private stepToNumber;
|
|
34
|
+
private getText;
|
|
35
|
+
private getNumber;
|
|
36
|
+
}
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { Clef, Duration, Accidental, Articulation, BarlineStyle, } from '../models/types';
|
|
3
|
+
/**
|
|
4
|
+
* MusicXML Parser
|
|
5
|
+
*
|
|
6
|
+
* Shared implementation for both frontend and backend.
|
|
7
|
+
* Expects a DOMParser-compatible constructor in Node.js environments.
|
|
8
|
+
*/
|
|
9
|
+
export class MusicXMLParser {
|
|
10
|
+
currentKeyFifths = 0;
|
|
11
|
+
currentBeats = 4;
|
|
12
|
+
currentBeatType = 4;
|
|
13
|
+
instrumentPitchMap = new Map();
|
|
14
|
+
_domParser;
|
|
15
|
+
constructor(domParserInstance) {
|
|
16
|
+
this._domParser = domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
17
|
+
}
|
|
18
|
+
parseFromString(xmlString) {
|
|
19
|
+
if (!this._domParser) {
|
|
20
|
+
throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
|
|
21
|
+
}
|
|
22
|
+
return this._domParser.parseFromString(xmlString, 'application/xml');
|
|
23
|
+
}
|
|
24
|
+
async parseBinary(data) {
|
|
25
|
+
const uint8 = new Uint8Array(data);
|
|
26
|
+
if (uint8[0] === 0x50 && uint8[1] === 0x4b) {
|
|
27
|
+
const zip = await JSZip.loadAsync(data);
|
|
28
|
+
const containerXml = await zip.file('META-INF/container.xml')?.async('text');
|
|
29
|
+
if (!containerXml)
|
|
30
|
+
throw new Error('Invalid MXL: Missing META-INF/container.xml');
|
|
31
|
+
const containerDoc = this.parseFromString(containerXml);
|
|
32
|
+
const rootfile = containerDoc.querySelector('rootfile');
|
|
33
|
+
const fullPath = rootfile?.getAttribute('full-path');
|
|
34
|
+
if (!fullPath)
|
|
35
|
+
throw new Error('Invalid MXL: Could not find main score file path');
|
|
36
|
+
const scoreXml = await zip.file(fullPath)?.async('text');
|
|
37
|
+
if (!scoreXml)
|
|
38
|
+
throw new Error(`Invalid MXL: Could not find file ${fullPath}`);
|
|
39
|
+
return this.parse(scoreXml);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const decoder = new TextDecoder();
|
|
43
|
+
return this.parse(decoder.decode(data));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
parse(xmlString) {
|
|
47
|
+
this.instrumentPitchMap.clear();
|
|
48
|
+
const doc = this.parseFromString(xmlString);
|
|
49
|
+
const parseError = doc.querySelector('parsererror');
|
|
50
|
+
if (parseError)
|
|
51
|
+
throw new Error(`XML parsing error: ${parseError.textContent}`);
|
|
52
|
+
const root = doc.documentElement;
|
|
53
|
+
if (root.tagName !== 'score-partwise')
|
|
54
|
+
throw new Error(`Unsupported format: ${root.tagName}`);
|
|
55
|
+
const title = this.parseTitle(doc);
|
|
56
|
+
const subtitle = this.parseSubtitle(doc);
|
|
57
|
+
const composer = this.parseComposer(doc);
|
|
58
|
+
const partNames = this.parsePartList(doc);
|
|
59
|
+
const parts = this.parseParts(doc, partNames);
|
|
60
|
+
let globalBpm = 120;
|
|
61
|
+
let globalTempoDuration = Duration.Quarter;
|
|
62
|
+
let globalTempoIsDotted = false;
|
|
63
|
+
if (parts.length > 0 && parts[0].staves.length > 0 && parts[0].staves[0].measures.length > 0) {
|
|
64
|
+
const firstMeasure = parts[0].staves[0].measures[0];
|
|
65
|
+
if (firstMeasure.tempo) {
|
|
66
|
+
globalBpm = firstMeasure.tempo.bpm;
|
|
67
|
+
globalTempoDuration = firstMeasure.tempo.duration;
|
|
68
|
+
globalTempoIsDotted = firstMeasure.tempo.isDotted;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const score = {
|
|
72
|
+
title, subtitle, composer,
|
|
73
|
+
timeSignature: { beats: this.currentBeats, beatType: this.currentBeatType },
|
|
74
|
+
keySignature: { fifths: this.currentKeyFifths },
|
|
75
|
+
parts,
|
|
76
|
+
bpm: globalBpm,
|
|
77
|
+
tempoDuration: globalTempoDuration,
|
|
78
|
+
tempoIsDotted: globalTempoIsDotted,
|
|
79
|
+
};
|
|
80
|
+
return this.postProcess(score);
|
|
81
|
+
}
|
|
82
|
+
parseSubtitle(doc) {
|
|
83
|
+
let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
|
|
84
|
+
if (subtitle)
|
|
85
|
+
return subtitle;
|
|
86
|
+
const work = doc.querySelector('work');
|
|
87
|
+
if (work) {
|
|
88
|
+
subtitle = this.getText(work, 'work-subtitle');
|
|
89
|
+
if (subtitle)
|
|
90
|
+
return subtitle;
|
|
91
|
+
subtitle = this.getText(work, 'work-number');
|
|
92
|
+
if (subtitle)
|
|
93
|
+
return subtitle;
|
|
94
|
+
}
|
|
95
|
+
const credits = doc.querySelectorAll('credit');
|
|
96
|
+
for (const credit of Array.from(credits)) {
|
|
97
|
+
const type = credit.querySelector('credit-type');
|
|
98
|
+
if (type?.textContent?.toLowerCase() === 'subtitle') {
|
|
99
|
+
const words = credit.querySelector('credit-words');
|
|
100
|
+
if (words?.textContent)
|
|
101
|
+
return words.textContent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
parseTitle(doc) {
|
|
107
|
+
let title = this.getText(doc.documentElement, 'work-title');
|
|
108
|
+
if (!title)
|
|
109
|
+
title = this.getText(doc.documentElement, 'movement-title');
|
|
110
|
+
return title ?? 'Untitled';
|
|
111
|
+
}
|
|
112
|
+
parseComposer(doc) {
|
|
113
|
+
const creators = doc.querySelectorAll('identification creator');
|
|
114
|
+
for (const creator of Array.from(creators)) {
|
|
115
|
+
if (creator.getAttribute('type') === 'composer')
|
|
116
|
+
return creator.textContent ?? 'Unknown';
|
|
117
|
+
}
|
|
118
|
+
return doc.querySelector('identification creator')?.textContent ?? 'Unknown';
|
|
119
|
+
}
|
|
120
|
+
parsePartList(doc) {
|
|
121
|
+
const partNames = new Map();
|
|
122
|
+
const scoreParts = doc.querySelectorAll('part-list score-part');
|
|
123
|
+
for (const scorePart of Array.from(scoreParts)) {
|
|
124
|
+
const id = scorePart.getAttribute('id');
|
|
125
|
+
const name = this.getText(scorePart, 'part-name');
|
|
126
|
+
if (id)
|
|
127
|
+
partNames.set(id, name?.trim() || 'Part');
|
|
128
|
+
const midiInstruments = scorePart.querySelectorAll('midi-instrument');
|
|
129
|
+
for (const midiInst of Array.from(midiInstruments)) {
|
|
130
|
+
const instId = midiInst.getAttribute('id');
|
|
131
|
+
const unpitched = this.getNumber(midiInst, 'midi-unpitched');
|
|
132
|
+
if (instId && unpitched !== null)
|
|
133
|
+
this.instrumentPitchMap.set(instId, unpitched);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return partNames;
|
|
137
|
+
}
|
|
138
|
+
parseParts(doc, partNames) {
|
|
139
|
+
const parts = [];
|
|
140
|
+
const partElements = doc.querySelectorAll('part');
|
|
141
|
+
for (const partElement of Array.from(partElements)) {
|
|
142
|
+
const id = partElement.getAttribute('id');
|
|
143
|
+
const name = partNames.get(id ?? '') ?? 'Part';
|
|
144
|
+
parts.push({ name, staves: this.parsePart(partElement) });
|
|
145
|
+
}
|
|
146
|
+
return parts;
|
|
147
|
+
}
|
|
148
|
+
parsePart(partElement) {
|
|
149
|
+
const measureElements = partElement.querySelectorAll('measure');
|
|
150
|
+
let numStaves = 1;
|
|
151
|
+
const firstMeasure = partElement.querySelector('measure');
|
|
152
|
+
if (firstMeasure) {
|
|
153
|
+
const stavesNode = firstMeasure.querySelector('attributes staves');
|
|
154
|
+
if (stavesNode)
|
|
155
|
+
numStaves = parseInt(stavesNode.textContent || '1');
|
|
156
|
+
}
|
|
157
|
+
const staves = Array.from({ length: numStaves }, () => ({ clef: 'treble', measures: [] }));
|
|
158
|
+
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
159
|
+
for (const measureElement of Array.from(measureElements)) {
|
|
160
|
+
let measureTimeSignature;
|
|
161
|
+
let measureKeySignature;
|
|
162
|
+
const isPickup = measureElement.getAttribute('implicit') === 'yes';
|
|
163
|
+
const attributes = measureElement.querySelector('attributes');
|
|
164
|
+
if (attributes) {
|
|
165
|
+
const fifths = this.getNumber(attributes, 'key fifths');
|
|
166
|
+
if (fifths !== null) {
|
|
167
|
+
this.currentKeyFifths = fifths;
|
|
168
|
+
measureKeySignature = { fifths: this.currentKeyFifths };
|
|
169
|
+
}
|
|
170
|
+
const beats = this.getNumber(attributes, 'time beats');
|
|
171
|
+
const beatType = this.getNumber(attributes, 'time beat-type');
|
|
172
|
+
if (beats !== null) {
|
|
173
|
+
this.currentBeats = beats;
|
|
174
|
+
if (beatType !== null)
|
|
175
|
+
this.currentBeatType = beatType;
|
|
176
|
+
const timeElement = attributes.querySelector('time');
|
|
177
|
+
let symbol = 'normal';
|
|
178
|
+
if (timeElement) {
|
|
179
|
+
const symAttr = timeElement.getAttribute('symbol');
|
|
180
|
+
if (symAttr === 'common')
|
|
181
|
+
symbol = 'common';
|
|
182
|
+
if (symAttr === 'cut')
|
|
183
|
+
symbol = 'cut';
|
|
184
|
+
}
|
|
185
|
+
measureTimeSignature = { beats: this.currentBeats, beatType: this.currentBeatType, symbol };
|
|
186
|
+
}
|
|
187
|
+
const clefElements = attributes.querySelectorAll('clef');
|
|
188
|
+
clefElements.forEach((clef) => {
|
|
189
|
+
const number = parseInt(clef.getAttribute('number') || '1');
|
|
190
|
+
const sign = this.getText(clef, 'sign');
|
|
191
|
+
if (sign && number <= numStaves) {
|
|
192
|
+
const line = this.getNumber(clef, 'line') || 0;
|
|
193
|
+
staffClefs[number - 1] = this.mapClef(sign, line);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const staffDetails = attributes.querySelectorAll('staff-details');
|
|
197
|
+
staffDetails.forEach((sd) => {
|
|
198
|
+
const number = parseInt(sd.getAttribute('number') || '1');
|
|
199
|
+
const lines = this.getNumber(sd, 'staff-lines');
|
|
200
|
+
if (lines !== null && number <= numStaves)
|
|
201
|
+
staves[number - 1].lineCount = lines;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
let measureTempo = undefined;
|
|
205
|
+
let rehearsalMark = undefined;
|
|
206
|
+
let systemText = undefined;
|
|
207
|
+
let contextDynamic = undefined;
|
|
208
|
+
const directions = measureElement.querySelectorAll('direction');
|
|
209
|
+
directions.forEach((dir) => {
|
|
210
|
+
const metronome = dir.querySelector('metronome');
|
|
211
|
+
if (metronome) {
|
|
212
|
+
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
213
|
+
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
214
|
+
const isDotted = metronome.querySelector('beat-unit-dot') !== null;
|
|
215
|
+
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
|
|
216
|
+
}
|
|
217
|
+
const rehearsal = dir.querySelector('rehearsal');
|
|
218
|
+
if (rehearsal)
|
|
219
|
+
rehearsalMark = rehearsal.textContent || undefined;
|
|
220
|
+
const words = dir.querySelector('words');
|
|
221
|
+
if (words)
|
|
222
|
+
systemText = words.textContent || undefined;
|
|
223
|
+
const dynamics = dir.querySelector('dynamics');
|
|
224
|
+
if (dynamics && dynamics.firstElementChild)
|
|
225
|
+
contextDynamic = dynamics.firstElementChild.nodeName.toLowerCase();
|
|
226
|
+
});
|
|
227
|
+
const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
|
|
228
|
+
const measureVoices = Array.from({ length: numStaves }, () => []);
|
|
229
|
+
const children = Array.from(measureElement.children);
|
|
230
|
+
let pendingChord = undefined;
|
|
231
|
+
let currentNoteSetMap = new Map();
|
|
232
|
+
const beamGroupIdMap = new Map();
|
|
233
|
+
let globalBeamId = 1;
|
|
234
|
+
children.forEach((child) => {
|
|
235
|
+
if (child.nodeName === 'harmony')
|
|
236
|
+
pendingChord = this.parseHarmony(child);
|
|
237
|
+
else if (child.nodeName === 'note') {
|
|
238
|
+
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
239
|
+
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
240
|
+
const isChord = child.querySelector('chord') !== null;
|
|
241
|
+
const note = this.parseNote(child);
|
|
242
|
+
if (note) {
|
|
243
|
+
const beamEl = child.querySelector('beam[number="1"]');
|
|
244
|
+
if (beamEl) {
|
|
245
|
+
const beamType = beamEl.textContent;
|
|
246
|
+
if (beamType === 'begin') {
|
|
247
|
+
const newId = globalBeamId++;
|
|
248
|
+
beamGroupIdMap.set(staffIdx, newId);
|
|
249
|
+
note.beamGroup = newId;
|
|
250
|
+
}
|
|
251
|
+
else if (beamType === 'continue' || beamType === 'end') {
|
|
252
|
+
const currentId = beamGroupIdMap.get(staffIdx);
|
|
253
|
+
if (currentId !== undefined) {
|
|
254
|
+
note.beamGroup = currentId;
|
|
255
|
+
if (beamType === 'end')
|
|
256
|
+
beamGroupIdMap.delete(staffIdx);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (pendingChord) {
|
|
261
|
+
note.chord = pendingChord;
|
|
262
|
+
pendingChord = undefined;
|
|
263
|
+
}
|
|
264
|
+
if (contextDynamic) {
|
|
265
|
+
note.dynamic = contextDynamic;
|
|
266
|
+
contextDynamic = undefined;
|
|
267
|
+
}
|
|
268
|
+
if (isChord) {
|
|
269
|
+
const existing = currentNoteSetMap.get(staffIdx);
|
|
270
|
+
if (existing)
|
|
271
|
+
existing.push(note);
|
|
272
|
+
else
|
|
273
|
+
currentNoteSetMap.set(staffIdx, [note]);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const existing = currentNoteSetMap.get(staffIdx);
|
|
277
|
+
if (existing)
|
|
278
|
+
measureVoices[staffIdx].push({ notes: existing });
|
|
279
|
+
currentNoteSetMap.set(staffIdx, [note]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
|
|
284
|
+
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
285
|
+
if (notes.length > 0)
|
|
286
|
+
measureVoices[sIdx].push({ notes });
|
|
287
|
+
}
|
|
288
|
+
currentNoteSetMap.clear();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
292
|
+
if (notes.length > 0)
|
|
293
|
+
measureVoices[sIdx].push({ notes });
|
|
294
|
+
}
|
|
295
|
+
for (let i = 0; i < numStaves; i++) {
|
|
296
|
+
staves[i].measures.push({
|
|
297
|
+
voices: [measureVoices[i]],
|
|
298
|
+
timeSignature: measureTimeSignature,
|
|
299
|
+
keySignature: measureKeySignature,
|
|
300
|
+
isPickup: isPickup || undefined,
|
|
301
|
+
tempo: i === 0 ? measureTempo : undefined,
|
|
302
|
+
repeats: repeats.length > 0 ? repeats : undefined,
|
|
303
|
+
volta,
|
|
304
|
+
rehearsalMark: i === 0 ? rehearsalMark : undefined,
|
|
305
|
+
systemText: i === 0 ? systemText : undefined,
|
|
306
|
+
barlineStyle,
|
|
307
|
+
});
|
|
308
|
+
staves[i].clef = staffClefs[i];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return staves;
|
|
312
|
+
}
|
|
313
|
+
parseNote(noteElement) {
|
|
314
|
+
const isRest = noteElement.querySelector('rest') !== null;
|
|
315
|
+
const type = this.getText(noteElement, 'type');
|
|
316
|
+
const duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
317
|
+
const isDotted = noteElement.querySelector('dot') !== null;
|
|
318
|
+
if (isRest)
|
|
319
|
+
return { duration, isRest: true, isDotted };
|
|
320
|
+
let step = 'C', octave = 4, alter = 0;
|
|
321
|
+
const unpitched = noteElement.querySelector('unpitched');
|
|
322
|
+
if (unpitched) {
|
|
323
|
+
step = this.getText(unpitched, 'display-step') || 'C';
|
|
324
|
+
octave = this.getNumber(unpitched, 'display-octave') ?? 4;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const pitch = noteElement.querySelector('pitch');
|
|
328
|
+
if (!pitch)
|
|
329
|
+
return null;
|
|
330
|
+
step = this.getText(pitch, 'step') || 'C';
|
|
331
|
+
octave = this.getNumber(pitch, 'octave') ?? 4;
|
|
332
|
+
alter = this.getNumber(pitch, 'alter') ?? 0;
|
|
333
|
+
}
|
|
334
|
+
let midiNumber = this.pitchToMidi(step, octave, alter);
|
|
335
|
+
const instrument = noteElement.querySelector('instrument');
|
|
336
|
+
if (instrument) {
|
|
337
|
+
const instId = instrument.getAttribute('id');
|
|
338
|
+
if (instId && this.instrumentPitchMap.has(instId))
|
|
339
|
+
midiNumber = this.instrumentPitchMap.get(instId);
|
|
340
|
+
}
|
|
341
|
+
let accidental = this.mapAccidental(alter);
|
|
342
|
+
const accidentalEl = this.getText(noteElement, 'accidental');
|
|
343
|
+
if (accidentalEl) {
|
|
344
|
+
if (accidentalEl === 'sharp')
|
|
345
|
+
accidental = Accidental.Sharp;
|
|
346
|
+
else if (accidentalEl === 'flat')
|
|
347
|
+
accidental = Accidental.Flat;
|
|
348
|
+
else if (accidentalEl === 'natural')
|
|
349
|
+
accidental = Accidental.Natural;
|
|
350
|
+
else if (accidentalEl === 'double-sharp')
|
|
351
|
+
accidental = Accidental.DoubleSharp;
|
|
352
|
+
else if (accidentalEl === 'flat-flat')
|
|
353
|
+
accidental = Accidental.DoubleFlat;
|
|
354
|
+
}
|
|
355
|
+
let notehead = undefined;
|
|
356
|
+
const noteheadEl = this.getText(noteElement, 'notehead');
|
|
357
|
+
if (noteheadEl) {
|
|
358
|
+
if (noteheadEl === 'x')
|
|
359
|
+
notehead = 'cross';
|
|
360
|
+
else if (['diamond', 'triangle', 'slash', 'square'].includes(noteheadEl))
|
|
361
|
+
notehead = noteheadEl;
|
|
362
|
+
}
|
|
363
|
+
const notations = noteElement.querySelector('notations');
|
|
364
|
+
let slur = undefined;
|
|
365
|
+
const slurEl = notations?.querySelector('slur');
|
|
366
|
+
if (slurEl) {
|
|
367
|
+
const type = slurEl.getAttribute('type');
|
|
368
|
+
if (type === 'start' || type === 'stop') {
|
|
369
|
+
slur = { placement: type };
|
|
370
|
+
const orientation = slurEl.getAttribute('orientation');
|
|
371
|
+
if (orientation === 'over')
|
|
372
|
+
slur.direction = 'up';
|
|
373
|
+
else if (orientation === 'under')
|
|
374
|
+
slur.direction = 'down';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const tie = noteElement.querySelector('tie')?.getAttribute('type') === 'start' || undefined;
|
|
378
|
+
let articulation = undefined;
|
|
379
|
+
const articEl = notations?.querySelector('articulations');
|
|
380
|
+
if (articEl) {
|
|
381
|
+
if (articEl.querySelector('staccato'))
|
|
382
|
+
articulation = Articulation.Staccato;
|
|
383
|
+
else if (articEl.querySelector('accent'))
|
|
384
|
+
articulation = Articulation.Accent;
|
|
385
|
+
else if (articEl.querySelector('tenuto'))
|
|
386
|
+
articulation = Articulation.Tenuto;
|
|
387
|
+
else if (articEl.querySelector('marcato'))
|
|
388
|
+
articulation = Articulation.Marcato;
|
|
389
|
+
else if (articEl.querySelector('staccatissimo'))
|
|
390
|
+
articulation = Articulation.Staccatissimo;
|
|
391
|
+
}
|
|
392
|
+
if (notations?.querySelector('fermata'))
|
|
393
|
+
articulation = Articulation.Fermata;
|
|
394
|
+
const notesDyn = notations?.querySelector('dynamics');
|
|
395
|
+
const dynamic = notesDyn?.firstElementChild?.nodeName.toLowerCase();
|
|
396
|
+
const lyrics = [];
|
|
397
|
+
noteElement.querySelectorAll('lyric').forEach((lyricEl) => {
|
|
398
|
+
let text = this.getText(lyricEl, 'text') || '';
|
|
399
|
+
const syllabic = this.getText(lyricEl, 'syllabic');
|
|
400
|
+
const number = parseInt(lyricEl.getAttribute('number') || '1');
|
|
401
|
+
if (text) {
|
|
402
|
+
if (syllabic === 'begin' || syllabic === 'middle')
|
|
403
|
+
text += '-';
|
|
404
|
+
lyrics[number - 1] = text;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
duration, pitch: { midiNumber, step: this.stepToNumber(step), alter, octave },
|
|
409
|
+
isDotted, accidental, slur, tie, articulation, dynamic,
|
|
410
|
+
lyrics: lyrics.length > 0 ? lyrics : undefined, notehead,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
parseHarmony(harmonyElement) {
|
|
414
|
+
const root = harmonyElement.querySelector('root');
|
|
415
|
+
if (!root)
|
|
416
|
+
return '';
|
|
417
|
+
const step = this.getText(root, 'root-step');
|
|
418
|
+
if (!step)
|
|
419
|
+
return '';
|
|
420
|
+
const alter = this.getNumber(root, 'root-alter') || 0;
|
|
421
|
+
const kind = this.getText(harmonyElement, 'kind') || 'major';
|
|
422
|
+
let accidental = '';
|
|
423
|
+
if (alter === 1)
|
|
424
|
+
accidental = '#';
|
|
425
|
+
else if (alter === -1)
|
|
426
|
+
accidental = 'b';
|
|
427
|
+
else if (alter === 2)
|
|
428
|
+
accidental = '##';
|
|
429
|
+
else if (alter === -2)
|
|
430
|
+
accidental = 'bb';
|
|
431
|
+
let kindNotation = '';
|
|
432
|
+
switch (kind) {
|
|
433
|
+
case 'major':
|
|
434
|
+
kindNotation = '';
|
|
435
|
+
break;
|
|
436
|
+
case 'minor':
|
|
437
|
+
kindNotation = 'm';
|
|
438
|
+
break;
|
|
439
|
+
case 'dominant':
|
|
440
|
+
kindNotation = '7';
|
|
441
|
+
break;
|
|
442
|
+
case 'major-seventh':
|
|
443
|
+
kindNotation = 'maj7';
|
|
444
|
+
break;
|
|
445
|
+
case 'minor-seventh':
|
|
446
|
+
kindNotation = 'm7';
|
|
447
|
+
break;
|
|
448
|
+
case 'diminished':
|
|
449
|
+
kindNotation = 'dim';
|
|
450
|
+
break;
|
|
451
|
+
case 'diminished-seventh':
|
|
452
|
+
kindNotation = 'dim7';
|
|
453
|
+
break;
|
|
454
|
+
case 'half-diminished':
|
|
455
|
+
kindNotation = 'm7b5';
|
|
456
|
+
break;
|
|
457
|
+
case 'augmented':
|
|
458
|
+
kindNotation = '+';
|
|
459
|
+
break;
|
|
460
|
+
case 'suspended-fourth':
|
|
461
|
+
kindNotation = 'sus4';
|
|
462
|
+
break;
|
|
463
|
+
case 'suspended-second':
|
|
464
|
+
kindNotation = 'sus2';
|
|
465
|
+
break;
|
|
466
|
+
case 'major-sixth':
|
|
467
|
+
kindNotation = '6';
|
|
468
|
+
break;
|
|
469
|
+
case 'minor-sixth':
|
|
470
|
+
kindNotation = 'm6';
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
const bass = harmonyElement.querySelector('bass');
|
|
474
|
+
let bassPart = '';
|
|
475
|
+
if (bass) {
|
|
476
|
+
const bStep = this.getText(bass, 'bass-step');
|
|
477
|
+
const bAlter = this.getNumber(bass, 'bass-alter') || 0;
|
|
478
|
+
if (bStep) {
|
|
479
|
+
let bAcc = '';
|
|
480
|
+
if (bAlter === 1)
|
|
481
|
+
bAcc = '#';
|
|
482
|
+
else if (bAlter === -1)
|
|
483
|
+
bAcc = 'b';
|
|
484
|
+
bassPart = '/' + bStep + bAcc;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return step + accidental + kindNotation + bassPart;
|
|
488
|
+
}
|
|
489
|
+
parseBarlines(measureElement) {
|
|
490
|
+
const repeats = [];
|
|
491
|
+
let volta = undefined;
|
|
492
|
+
let barlineStyle = BarlineStyle.Regular;
|
|
493
|
+
measureElement.querySelectorAll('barline').forEach((barline) => {
|
|
494
|
+
const location = barline.getAttribute('location') || 'right';
|
|
495
|
+
const barStyle = this.getText(barline, 'bar-style');
|
|
496
|
+
if (location === 'right') {
|
|
497
|
+
if (barStyle === 'light-light')
|
|
498
|
+
barlineStyle = BarlineStyle.Double;
|
|
499
|
+
else if (barStyle === 'light-heavy')
|
|
500
|
+
barlineStyle = BarlineStyle.Final;
|
|
501
|
+
else if (barStyle === 'dotted')
|
|
502
|
+
barlineStyle = BarlineStyle.Dotted;
|
|
503
|
+
}
|
|
504
|
+
if (barline.querySelector('repeat'))
|
|
505
|
+
repeats.push({ type: barline.querySelector('repeat')?.getAttribute('direction') === 'forward' ? 'start' : 'end' });
|
|
506
|
+
const ending = barline.querySelector('ending');
|
|
507
|
+
if (ending) {
|
|
508
|
+
const type = ending.getAttribute('type');
|
|
509
|
+
const number = parseInt(ending.getAttribute('number') || '1');
|
|
510
|
+
if (type === 'start')
|
|
511
|
+
volta = { type: 'start', number };
|
|
512
|
+
else if (type === 'stop')
|
|
513
|
+
volta = { type: 'stop', number };
|
|
514
|
+
else if (type === 'discontinue')
|
|
515
|
+
volta = { type: 'both', number };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return { repeats, volta, barlineStyle };
|
|
519
|
+
}
|
|
520
|
+
postProcess(score) {
|
|
521
|
+
this.cleanAccidentals(score);
|
|
522
|
+
return score;
|
|
523
|
+
}
|
|
524
|
+
cleanAccidentals(score) {
|
|
525
|
+
const fifths = score.keySignature?.fifths ?? 0;
|
|
526
|
+
const orderOfSharps = ['F', 'C', 'G', 'D', 'A', 'E', 'B'];
|
|
527
|
+
const orderOfFlats = ['B', 'E', 'A', 'D', 'G', 'C', 'F'];
|
|
528
|
+
const getKeyMap = () => {
|
|
529
|
+
const map = { C: 0, D: 0, E: 0, F: 0, G: 0, A: 0, B: 0 };
|
|
530
|
+
if (fifths > 0)
|
|
531
|
+
for (let i = 0; i < Math.min(fifths, 7); i++)
|
|
532
|
+
map[orderOfSharps[i]] = 1;
|
|
533
|
+
else if (fifths < 0)
|
|
534
|
+
for (let i = 0; i < Math.min(Math.abs(fifths), 7); i++)
|
|
535
|
+
map[orderOfFlats[i]] = -1;
|
|
536
|
+
return map;
|
|
537
|
+
};
|
|
538
|
+
const stepNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
|
|
539
|
+
score.parts.forEach((part) => {
|
|
540
|
+
part.staves.forEach((staff) => {
|
|
541
|
+
staff.measures.forEach((measure) => {
|
|
542
|
+
const state = getKeyMap();
|
|
543
|
+
(measure.voices || []).forEach((voice) => {
|
|
544
|
+
voice.forEach((ns) => {
|
|
545
|
+
ns.notes.forEach((note) => {
|
|
546
|
+
if (note.isRest || !note.pitch)
|
|
547
|
+
return;
|
|
548
|
+
const step = stepNames[note.pitch.step];
|
|
549
|
+
const alter = note.pitch.alter ?? 0;
|
|
550
|
+
if (note.accidental) {
|
|
551
|
+
if (state[step] === alter)
|
|
552
|
+
note.accidental = undefined;
|
|
553
|
+
else
|
|
554
|
+
state[step] = alter;
|
|
555
|
+
}
|
|
556
|
+
else if (state[step] !== alter) {
|
|
557
|
+
note.accidental = alter === 0 ? Accidental.Natural : alter === 1 ? Accidental.Sharp : Accidental.Flat;
|
|
558
|
+
state[step] = alter;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
pitchToMidi(step, octave, alter = 0) {
|
|
568
|
+
const steps = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
569
|
+
return (octave + 1) * 12 + (steps[step.toUpperCase()] ?? 0) + alter;
|
|
570
|
+
}
|
|
571
|
+
mapDuration(type) {
|
|
572
|
+
const map = { whole: Duration.Whole, half: Duration.Half, quarter: Duration.Quarter, eighth: Duration.Eighth, '16th': Duration.Sixteenth, '32nd': Duration.ThirtySecond, '64th': Duration.SixtyFourth };
|
|
573
|
+
return map[type] ?? Duration.Quarter;
|
|
574
|
+
}
|
|
575
|
+
mapClef(sign, line = 0) {
|
|
576
|
+
switch (sign.toUpperCase()) {
|
|
577
|
+
case 'G': return Clef.Treble;
|
|
578
|
+
case 'F': return Clef.Bass;
|
|
579
|
+
case 'C': return line === 4 ? Clef.Tenor : Clef.Alto;
|
|
580
|
+
case 'PERCUSSION': return Clef.Percussion;
|
|
581
|
+
case 'TAB': return Clef.Tab;
|
|
582
|
+
default: return Clef.Treble;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
mapAccidental(alter) {
|
|
586
|
+
if (alter === 1)
|
|
587
|
+
return Accidental.Sharp;
|
|
588
|
+
if (alter === -1)
|
|
589
|
+
return Accidental.Flat;
|
|
590
|
+
if (alter === 0)
|
|
591
|
+
return Accidental.Natural;
|
|
592
|
+
if (alter === 2)
|
|
593
|
+
return Accidental.DoubleSharp;
|
|
594
|
+
if (alter === -2)
|
|
595
|
+
return Accidental.DoubleFlat;
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
stepToNumber(step) {
|
|
599
|
+
const map = { C: 0, D: 1, E: 2, F: 3, G: 4, A: 5, B: 6 };
|
|
600
|
+
return map[step.toUpperCase()] ?? 0;
|
|
601
|
+
}
|
|
602
|
+
getText(element, tagName) {
|
|
603
|
+
const child = element.querySelector(tagName);
|
|
604
|
+
return child?.textContent ?? null;
|
|
605
|
+
}
|
|
606
|
+
getNumber(element, tagName) {
|
|
607
|
+
const text = this.getText(element, tagName);
|
|
608
|
+
return text !== null ? parseFloat(text) : null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './MusicXMLParser';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './MusicXMLParser';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare enum InstrumentType {
|
|
2
|
+
String = "string",
|
|
3
|
+
Brass = "brass",
|
|
4
|
+
Woodwind = "woodwind",
|
|
5
|
+
Percussion = "percussion",
|
|
6
|
+
Keyboard = "keyboard",
|
|
7
|
+
Synth = "synth"
|
|
8
|
+
}
|
|
9
|
+
export declare enum InstrumentPreset {
|
|
10
|
+
Piano = "piano",
|
|
11
|
+
Violin = "violin",
|
|
12
|
+
Cello = "cello",
|
|
13
|
+
Guitar = "guitar",
|
|
14
|
+
ElectricGuitar = "electric-guitar",
|
|
15
|
+
Bass = "bass",
|
|
16
|
+
Flute = "flute",
|
|
17
|
+
Trumpet = "trumpet",
|
|
18
|
+
Drums = "drums"
|
|
19
|
+
}
|
|
20
|
+
export interface Instrument {
|
|
21
|
+
name: string;
|
|
22
|
+
midiProgram: number;
|
|
23
|
+
type?: InstrumentType;
|
|
24
|
+
}
|
|
25
|
+
export declare const PRESET_INSTRUMENTS: Record<string, Instrument>;
|
|
26
|
+
export declare function getInstrumentByProgram(program: number): Instrument;
|