diff --git a/JS/.eslintrc.js b/JS/.eslintrc.js index 9bbda1f..19136ed 100644 --- a/JS/.eslintrc.js +++ b/JS/.eslintrc.js @@ -11,10 +11,6 @@ module.exports = { }, "rules": { "no-console":0, - "quotes": [ - "error", - "double" - ], "semi": [ "error", "always" diff --git a/JS/API/ScoreIterator.js b/JS/API/ScoreIterator.js new file mode 100644 index 0000000..6d3554d --- /dev/null +++ b/JS/API/ScoreIterator.js @@ -0,0 +1,271 @@ +"use strict"; + +//"private static" utility definitions========================================= +const xml2js = require("xml2js"); +const parser = new xml2js.Parser({explicitArray: false, mergeAttrs: true}); +const pitchToMidiNum = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A":9, "B": 11}; + +function traverse(obj,func) +{ + for (let i in obj) + { + func.apply(this,[i, obj[i]]); + if (obj[i] !== null && typeof(obj[i])==="object") traverse(obj[i],func); + } +} + +//create objects for each part, this will reduce searching whole score. +//part being the full name of parts, ex: Solo Violin, Violin I, Violin II +function makeInstrumentObjects(musicObj) +{ + let partNames = []; + let instrumentObjects = {}; //will be like {"Flute" [..array of measures]} + let measureArraysSeen = 0; + + function process(key, value) //builds array of instrument objects + { + //first find the part names as they"re always towards the top of file + //This will be pushed in the correct order as we see them: + if (key === "part-name") partNames.push(value); + if (key === "measure") + { + const instrumentName = partNames[measureArraysSeen]; + instrumentObjects[instrumentName] = []; + + for (const measure of value) + { + instrumentObjects[instrumentName].push(measure); + } + measureArraysSeen++; + } + } + + traverse(musicObj, process); + return instrumentObjects; +} + +function ScoreIterable(instrumentObjects) +{ + let scoreIterable = {}; + let part = []; + + for (let instrumentName in instrumentObjects) + { + for (let measure of instrumentObjects[instrumentName]) + { + let midiNum = 0; + let timeNotesMap = new Map(); //contains {"default-x", [pitches]} + //^ MUST USE MAP NOT OBJECT to ensure notes are always in correct order + //strategy: loop through measure to see symbols happening + //at same points in time (default-x) measureArraysSeen++; + + let voiceTimes = []; + // ^^^ [5, 2] means voice 1 currently at 5, voice 2 currently at 2 + //voiceTimes[voice] => gives the time in beats the voice is from + //beginning of measure + let bassNoteDuration = -123; //bass note of potential chord! + let notes = measure["note"]; + + if (notes !== undefined) //NOTE: returns an array of note tags for a measure + { + for (let singleNote of notes) + { + let voice = parseInt(singleNote["voice"]); + + //check if first time seeing this voice + if (voice === undefined) + { + throw new Error("No voice tag??"); + } + else + { + while (voiceTimes.length < voice) + { + voiceTimes.push(0); + } + } + + if (singleNote["pitch"] !== undefined) + { + //1) Calculate midinum + //TODO: make helper + midiNum += pitchToMidiNum[singleNote["pitch"]["step"]]; + if (singleNote["pitch"]["alter"] !== undefined) + midiNum += parseInt(singleNote["pitch"]["alter"]); + midiNum += parseInt(singleNote["pitch"]["octave"]) * 12; + + let note = {}; + note.midiNum = midiNum; + note.duration = parseInt(singleNote["duration"]); + + let currentTime = voiceTimes[voice - 1]; + + //NOTE:two notes of same duration at same time can be same voice + //two notes of different duration at same start time are two voices + //^ this is mentioned in the musicxml standard! + //only single voice playing multiple notes has chord tag + if (singleNote["chord"] !== undefined) + { + currentTime = currentTime - bassNoteDuration; + } + // console.log("currentTime", currentTime); + // console.log("note", note); + let existingVal = timeNotesMap.get(currentTime); + // console.log("existing", existingVal); + + if (existingVal) + { + // console.log("existingVal", existingVal); + existingVal.push(note); + timeNotesMap.set(currentTime, existingVal); + } + else + { + let arr = []; + arr.push(note); + timeNotesMap.set(currentTime, arr); + } + + if (singleNote["chord"] === undefined) + { + voiceTimes[voice - 1] += note.duration; + bassNoteDuration = note.duration; + } + midiNum = 0; + } + else if (singleNote["rest"] !== undefined) + { + let currentTime = voiceTimes[voice - 1]; + let existingVal = timeNotesMap.get(currentTime); + + if (existingVal) + { + timeNotesMap.set(currentTime, existingVal); + } + else + { + let arr = []; + arr.push(parseInt(singleNote["duration"])); + timeNotesMap.set(currentTime, arr); + } + + part.push(singleNote["duration"]); //TODO + } + } //loop through measure + + let sortedKeys = []; + + for (let key of timeNotesMap.keys()) + { + sortedKeys.push(key); + } + sortedKeys.sort(); + + for (let key of sortedKeys) + { + let timeStampedMap = new Map(); + timeStampedMap.set(key, timeNotesMap.get(key)); + part.push(timeStampedMap); + } + + console.log("timeNotesMap", timeNotesMap); + } //if note + } //instrumentName + + scoreIterable[instrumentName] = part; //TODO + } //loop through instruments + + return scoreIterable; +} + +const errors = +{ + "noValidInstrumentSelected": 'No valid instrument selected, ex: ("Flute")!', + "noNext": "no next exists", + "noPrev": "no prev exists!", + "invalidPosition": "setPosition to invalid index" +}; + +//============================================================================= +const factoryScoreIterator = (MusicXML) => +{ + let musicObj; + parser.parseString(MusicXML, function (err, jsObj) + { + if (err) throw err; + musicObj = jsObj; + }); + + const instrumentObjects = makeInstrumentObjects(musicObj); + const scoreIterator = {}; + let scoreIterable = ScoreIterable(instrumentObjects); + // console.dir(scoreIterable); + + scoreIterable["Classical Guitar"].forEach((map) => console.log(map)); + let selectedInstrument = "NONE"; + let currentIndex = -1; + + scoreIterator.selectInstrument = (instrumentName) => + { + selectedInstrument = instrumentName; + }; + + scoreIterator.next = () => + { + if (currentIndex === scoreIterable[selectedInstrument].length - 1) + { + throw new Error(errors.noNext); + } + else + { + currentIndex++; + } + if (scoreIterable[selectedInstrument] === undefined) + throw new Error(errors.noValidInstrumentSelected); + return scoreIterable[selectedInstrument][currentIndex]; + }; + + scoreIterator.prev = () => + { + if (currentIndex === 0) + { + throw new Error("No prev exists!"); + } + else + { + currentIndex--; + } + + if (scoreIterable[selectedInstrument] === undefined) + throw new Error(errors.noValidInstrumentSelected); + return scoreIterable[selectedInstrument][currentIndex]; + }; + + scoreIterator.hasNext = () => + { + if (scoreIterable[selectedInstrument] === undefined) + throw new Error(errors.noValidInstrumentSelected); + + return (currentIndex < scoreIterable[selectedInstrument].length - 1); + }; + + scoreIterator.hasPrev = () => + { + if (scoreIterable[selectedInstrument] === undefined) + throw new Error(errors.noValidInstrumentSelected); + + return (currentIndex > 0); + }; + + scoreIterator.getPosition = () => currentIndex; + + scoreIterator.setPosition = (position) => + { + if (position > scoreIterable.length -1) + throw new Error(errors.invalidPosition); + currentIndex = position; + }; + return scoreIterator; +}; //end of factory + +module.exports = factoryScoreIterator; diff --git a/JS/API/ScoreIterator.spec.js b/JS/API/ScoreIterator.spec.js new file mode 100644 index 0000000..f245455 --- /dev/null +++ b/JS/API/ScoreIterator.spec.js @@ -0,0 +1,20 @@ +"use strict"; +const fs = require("fs"); +const test = require("tape").test; +const ScoreIterator = require("./ScoreIterator"); + +//TODO: test selectInstrument + +let musicXML = fs.readFileSync("../scores/guitar_two_voices.xml"); +// {"Classical Guitar":[[{"pitch":45}],[{"pitch":50}],[{"pitch":47},{"pitch":50}],[{"pitch":47}],"2",[{"pitch":41},{"pitch":50}],[{"pitch":41},{"pitch":47}]]} + +test("next", function(t) +{ + const scoreIterator = ScoreIterator(musicXML); + + scoreIterator.selectInstrument("Classical Guitar"); + // t.deepEqual(scoreIterator.next(), [], "next 1"); + + + t.end(); +}); diff --git a/JS/API/strategy.txt b/JS/API/strategy.txt new file mode 100644 index 0000000..65ba4fa --- /dev/null +++ b/JS/API/strategy.txt @@ -0,0 +1,49 @@ +GOAL: +next() => {"pitch": "A", "duration": 2, ... } // use pitch and octave instead of midi num? +// internally this sets an index + + +instrumentObjects = +{ + "flute": {"notes": [.......], "durations": [......], ...} + ... +} + +return Object.assign({}, instrumentObjects.flute.notes[iterator]...) + +OR + +instrumentObjects = +{ + "flute": [[{"note": 23, "duration": 2}, {"note": 27, "duration": 2}], []] + + ... +} + +OR + +instrumentObjects = +{ + "flute": [{23: 2, 27:2}, []] + ^ NO. Cant use map because what if the chord has two of the same note? + ... +} + +can we assume if there are two notes play at once in one part there are two voices? +^ in reality no.. guitars can play two notes at once + +=====aside==== +should makeInstrumentObjects really be inside the factoryScoreSearcher and contained within a closure? +From a scoping perspective, this is less static parent lookup and provents makeInstrumentObjects from being called twice... + +then again, should all private functions be static since their definition is consistent..? less memory? + +Why aren't you using map instead of object in makeInstrumentObjects + + +========================= +strategy to create chord iterable: +loop through all notes of a measure and if they occur at same default-x (position), +they are part of a chord. Clear upon new measure. What about ties??? + +can this be used to create getNextchord diff --git a/JS/scores/guitar.xml b/JS/scores/guitar.xml new file mode 100644 index 0000000..fe1a0b3 --- /dev/null +++ b/JS/scores/guitar.xml @@ -0,0 +1,201 @@ + + + + + guitar + + + + MuseScore 2.0.3 + 2017-03-28 + + + + + + + + + + 7.05556 + 40 + + + 1683.36 + 1190.88 + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + + + + + guitar + + + + Classical Guitar + Guit. + + Classical Guitar + + + + 1 + 25 + 78.7402 + 0 + + + + + + + + + -0.00 + 608.23 + + 170.00 + + + + 1 + + 0 + + + + G + 2 + -1 + + + + + A + 3 + + 1 + 1 + quarter + up + + + + D + 4 + + 1 + 1 + quarter + down + + + + B + 3 + + 1 + 1 + quarter + down + + + + + D + 4 + + 1 + 1 + quarter + down + + + + E + 4 + + 1 + 1 + quarter + down + + + + + + F + 3 + + 2 + 1 + half + down + + + + E + 3 + + 2 + 1 + half + + + 4 + + + + D + 4 + + 1 + 2 + quarter + down + + + + F + 3 + + 1 + 2 + quarter + down + + + + + B + 3 + + 1 + 2 + quarter + down + + + 2 + + + light-heavy + + + + diff --git a/JS/scores/guitar_two_voices.xml b/JS/scores/guitar_two_voices.xml new file mode 100644 index 0000000..111348d --- /dev/null +++ b/JS/scores/guitar_two_voices.xml @@ -0,0 +1,191 @@ + + + + + guitar + + + + MuseScore 2.0.3 + 2017-03-29 + + + + + + + + + + 7.05556 + 40 + + + 1683.36 + 1190.88 + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + + + + + guitar + + + + Classical Guitar + Guit. + + Classical Guitar + + + + 1 + 25 + 78.7402 + 0 + + + + + + + + + -0.00 + 608.23 + + 170.00 + + + + 1 + + 0 + + + + G + 2 + -1 + + + + + A + 3 + + 1 + 1 + quarter + up + + + + D + 4 + + 1 + 1 + quarter + down + + + + B + 3 + + 1 + 1 + quarter + down + + + + + D + 4 + + 1 + 1 + quarter + down + + + + E + 4 + + 1 + 1 + quarter + down + + + + + + F + 3 + + 2 + 1 + half + down + + + + E + 3 + + 2 + 1 + half + + + 3 + + + + F + 3 + + 1 + 2 + quarter + down + + + + + B + 3 + + 1 + 2 + quarter + down + + + 2 + + + light-heavy + + + + diff --git a/JS/scores/guitar_two_voices2.xml b/JS/scores/guitar_two_voices2.xml new file mode 100644 index 0000000..e268ec3 --- /dev/null +++ b/JS/scores/guitar_two_voices2.xml @@ -0,0 +1,211 @@ + + + + + guitar + + + + MuseScore 2.0.3 + 2017-03-29 + + + + + + + + + + 7.05556 + 40 + + + 1683.36 + 1190.88 + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + + + + + guitar + + + + Classical Guitar + Guit. + + Classical Guitar + + + + 1 + 25 + 78.7402 + 0 + + + + + + + + + -0.00 + 561.84 + + 170.00 + + + + 1 + + 0 + + + + G + 2 + -1 + + + + + A + 3 + + 1 + 1 + quarter + up + + + + D + 4 + + 1 + 1 + quarter + down + + + + B + 3 + + 1 + 1 + quarter + down + + + + + D + 4 + + 1 + 1 + quarter + down + + + + E + 4 + + 1 + 1 + quarter + down + + + + + + F + 3 + + 2 + 1 + half + down + + + + B + 2 + + 1 + 1 + quarter + up + + + + E + 3 + + 1 + 1 + quarter + + + 4 + + + + E + 4 + + 1 + 2 + quarter + up + + + + A + 3 + + 1 + 2 + quarter + down + + + + + B + 3 + + 1 + 2 + quarter + down + + + 2 + + + light-heavy + + + +