Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Score iterator #82

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
4 changes: 0 additions & 4 deletions JS/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ module.exports = {
},
"rules": {
"no-console":0,
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
Expand Down
271 changes: 271 additions & 0 deletions JS/API/ScoreIterator.js
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 20 additions & 0 deletions JS/API/ScoreIterator.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
49 changes: 49 additions & 0 deletions JS/API/strategy.txt
Original file line number Diff line number Diff line change
@@ -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
Loading