UNPKG

webdaw-modules

Version:

a set of modules for building a web-based DAW

333 lines (297 loc) 11.5 kB
import { getNoteNumber } from "../util/midi"; import { MIDIEvent } from "../MIDIEvent"; import { getVolume } from "./part/getVolume"; import { getPartName } from "./part/getPartName"; import { getChannel } from "./part/getChannel"; import { getInstrument } from "./part/getInstrument"; import { getMeasureNumber } from "./measure/getMeasureNumber"; import { getDivisions } from "./measure/getDivisions"; import { getSignature } from "./measure/getSignature"; import { getTempo } from "./measure/getTempo"; import { getRepeat } from "./measure/getRepeat"; // let n = 0; export type PartData = { id: string; name: string; instrument: string; volume: number; events: MIDIEvent[]; }; type Repeat = { bar: number; type: string; }[]; export type RepeatData = { start: number; end: number; active: boolean; id: string; }; export type ParsedMusicXML = { ppq: number; parts: PartData[]; repeats: RepeatData[]; initialTempo: number; initialNumerator: number; initialDenominator: number; } | null; export const parseMusicXML = (xmlDoc: XMLDocument, ppq: number = 960): ParsedMusicXML => { if (xmlDoc === null) { return null; } let type: string; if (xmlDoc.firstChild !== null && xmlDoc.firstChild.nextSibling !== null) { type = xmlDoc.firstChild.nextSibling.nodeName; // console.log('type', type, nsResolver); if (type === "score-partwise") { return parsePartWise(xmlDoc, ppq); } if (type === "score-timewise") { return parseTimeWise(xmlDoc); } } // console.log('unknown type', type); return null; }; const parsePartWise = (xmlDoc: XMLDocument, ppq: number = 960): ParsedMusicXML => { if (xmlDoc === null) { return null; } // const nsResolver = xmlDoc.createNSResolver( // xmlDoc.ownerDocument === null ? xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement // ); const nsResolver = xmlDoc.createNSResolver(xmlDoc.documentElement); const partIterator = xmlDoc.evaluate("//score-part", xmlDoc, nsResolver, XPathResult.ANY_TYPE, null); const parts: PartData[] = []; const tiedNotes: { [id: string]: number } = {}; // const repeats: Repeat = [{ bar: 1, type: "forward" }]; let repeats: Repeat = []; let initialTempo = -1; let initialNumerator = -1; let initialDenominator = -1; let index = -1; let partNode: Node | null; while ((partNode = partIterator.iterateNext())) { index += 1; const [partId, partName] = getPartName(xmlDoc, partNode, nsResolver); const volume = getVolume(xmlDoc, partNode, nsResolver); const velocity = (volume / 100) * 127; const channel = getChannel(xmlDoc, partNode, nsResolver); const instrument = getInstrument(xmlDoc, partNode, nsResolver); parts.push({ id: partId, name: partName, volume, instrument, events: [] }); const measureIterator = xmlDoc.evaluate('//part[@id="' + partId + '"]/measure', partNode, nsResolver, XPathResult.ANY_TYPE, null); let ticks = 0; let tmp: any; let measureNode: Node | null; let divisions: number = 24; while ((measureNode = measureIterator.iterateNext())) { const measureNumber = getMeasureNumber(xmlDoc, measureNode, nsResolver); divisions = getDivisions(xmlDoc, measureNode, nsResolver, divisions); const signatureEvent = getSignature(xmlDoc, measureNode, nsResolver); if (signatureEvent !== null) { signatureEvent.ticks = ticks; signatureEvent.bar = measureNumber; parts[index].events.push(signatureEvent); if (initialNumerator === -1) { ({ numerator: initialNumerator, denominator: initialDenominator } = signatureEvent); } } const timeEvent = getTempo(xmlDoc, measureNode, nsResolver); if (timeEvent !== null) { timeEvent.ticks = ticks; timeEvent.bar = measureNumber; parts[index].events.push(timeEvent); if (initialTempo === -1) { ({ bpm: initialTempo } = timeEvent); } } const repeat = getRepeat(xmlDoc, measureNode, nsResolver); if (repeat !== null && measureNumber !== 1) { // console.log(repeat, measureNumber); repeats.push({ type: repeat, bar: measureNumber }); } // get all notes and backups const noteIterator = xmlDoc.evaluate("*[self::note or self::backup or self::forward]", measureNode, nsResolver, XPathResult.ANY_TYPE, null); let noteNode: Node | null; while ((noteNode = noteIterator.iterateNext())) { // console.log(noteNode); let noteDuration = 0; let noteDurationTicks = 0; let voice = -1; let staff = -1; let tieStart = false; let tieStop = false; const tieIterator = xmlDoc.evaluate("tie", noteNode, nsResolver, XPathResult.ANY_TYPE, null); let tieNode: Node | null; while ((tieNode = tieIterator.iterateNext())) { const tieType = xmlDoc.evaluate("@type", tieNode, nsResolver, XPathResult.STRING_TYPE, null).stringValue; if (tieType === "start") { tieStart = true; } else if (tieType === "stop") { tieStop = true; } } const rest = xmlDoc.evaluate("rest", noteNode, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; const chord = xmlDoc.evaluate("chord", noteNode, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; const grace = xmlDoc.evaluate("grace", noteNode, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (rest !== null) { noteDuration = xmlDoc.evaluate("duration", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; ticks += (noteDuration / divisions) * ppq; // console.log("rest", ticks); } else if (noteNode.nodeName === "note" && grace === null) { const step = xmlDoc.evaluate("pitch/step", noteNode, nsResolver, XPathResult.STRING_TYPE, null).stringValue; const alter = xmlDoc.evaluate("pitch/alter", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; const octave = xmlDoc.evaluate("pitch/octave", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; tmp = xmlDoc.evaluate("voice", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; if (!isNaN(tmp)) { voice = tmp; } tmp = xmlDoc.evaluate("staff", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; if (!isNaN(tmp)) { staff = tmp; } noteDuration = xmlDoc.evaluate("duration", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; noteDurationTicks = (noteDuration / divisions) * ppq; // const noteType = xmlDoc.evaluate('type', noteNode, nsResolver, XPathResult.STRING_TYPE, null).stringValue; let noteName = step; if (!isNaN(alter)) { switch (alter) { case -2: noteName += "bb"; break; case -1: noteName += "b"; break; case 1: noteName += "#"; break; case 2: noteName += "##"; break; } } // in case of a chord, move the cursor back if (chord !== null) { ticks -= noteDurationTicks; } // console.log(ticks, measureNumber, chord); const noteNumber = getNoteNumber(noteName, octave); // console.log("\t", ticks, "ON", n++); const note = { ticks, descr: "note on", type: 0x90, channel, noteNumber, velocity, bar: measureNumber, }; ticks += noteDurationTicks; parts[index].events.push(note); //console.log('tie', tieStart, tieStop); if (tieStart === false && tieStop === false) { // no ties //console.log('no ties', measureNumber, voice, noteNumber, tiedNotes); // console.log(ticks, "OFF", index); parts[index].events.push({ ticks, descr: "note off", type: 0x80, channel, noteNumber, velocity: 0, bar: measureNumber, }); } else if (tieStart === true && tieStop === false) { // start of tie tiedNotes[`N_${staff}-${voice}-${noteNumber}`] = noteDurationTicks; //console.log('start', measureNumber, voice, noteNumber, tiedNotes); } else if (tieStart === true && tieStop === true) { // tied to yet another note tiedNotes[`N_${staff}-${voice}-${noteNumber}`] += noteDurationTicks; //console.log('thru', measureNumber, voice, noteNumber, tiedNotes); } else if (tieStart === false && tieStop === true) { // end of tie tiedNotes[`N_${staff}-${voice}-${noteNumber}`] += noteDurationTicks; // console.log(ticks, "OFF", index); parts[index].events.push({ // command: NOTE_OFF, ticks: tiedNotes[`N_${staff}-${voice}-${noteNumber}`], descr: "note off", type: 0x80, channel, noteNumber, velocity: 0, bar: measureNumber, }); delete tiedNotes[`N_${staff}-${voice}-${noteNumber}`]; //console.log('end', measureNumber, voice, noteNumber, tiedNotes); } } else if (noteNode.nodeName === "backup") { noteDuration = xmlDoc.evaluate("duration", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; ticks -= (noteDuration / divisions) * ppq; // console.log("backup", ticks); //console.log(noteDuration, divisions); } else if (noteNode.nodeName === "forward") { noteDuration = xmlDoc.evaluate("duration", noteNode, nsResolver, XPathResult.NUMBER_TYPE, null).numberValue; ticks += (noteDuration / divisions) * ppq; // console.log('forward', ticks); //console.log(noteDuration, divisions); } } } } const repeats2: [number, number][] = []; let j: number = -1; if (repeats.length && repeats[0].type !== "forward") { repeats = [{ type: "forward", bar: 1 }, ...repeats]; } // console.log(repeats); const filtered = []; for (let k = 0; k < repeats.length; k++) { const r = repeats[k]; let double = false; for (let k1 = 0; k1 < filtered.length; k1++) { const r1 = filtered[k1]; if (r1.bar === r.bar && r1.type === r.type) { double = true; break; } } if (!double) { filtered.push(r); } } filtered.forEach((t, i) => { if (t.type === "forward") { j++; repeats2[j] = [t.bar, -1]; } else if (t.type === "backward") { repeats2[j][1] = t.bar; } }); // console.log(repeats, repeats2); let i = 0; const repeats3: RepeatData[] = []; repeats2.forEach(([start, end]) => { repeats3.push({ start, end, active: true, id: `score-${i++}`, }); }); // console.log(repeats3); return { ppq, parts, repeats: repeats3, initialTempo, initialNumerator, initialDenominator, }; }; const parseTimeWise = (doc: XMLDocument): ParsedMusicXML => { // to be implemented return null; };