There are several programming techniques used in creating the Create Melody page that were relatively new to me. The basic premise of the process is for each note of a user supplied melodic contour phrase swap that note out with a short series of notes . The main function of this process is named createMelodyFromContourNotes(lilynotes, scale). The incoming lilypond code is first translated into the Tone.js format using the lilyPondAdapter.js module. The resulting code contains two arrays, one for the notes and one for the durations. The user can edit an ornament list (one for each note) which is used to determine the array of ornaments applied to the melodic contour. The user can specify 'no ornament' with a code of 'no' or enter a specific ornament code. If the '*' code is used the code will select a random ornament from the object named 'ornments', which is shown below.
const ornaments = {
1: 'no', 2: 'unt', 3: 'lnt', 4: 'ut1', 5: 'ut2',
6: 'lt1', 7: 'lt2', 8: '12321', 9: '1231', 10: '1321',
11: 'ap', 12: 'upt', 13: 'lpt', 14: '13271', 15: '1235',
16: '1345', 17: '1234', 18: '1324', 19: '1356', 20: '1357'
}
As more ornaments are being added to the app the number of elements in ornaments is used to pick a random between 1 and the number of elements. Although javascript objects don't have a .length property, you can find the number of key/value pairs in the ornaments object dynamically as the number of ornaments grows (Object.keys(ornaments).length). This means you don't have to change any code as your number of ornaments increaes. The correct range of random values will update automatically. That number can be used to randomly pick one of the ornament codes.
var upperLimit = Object.keys(ornaments).length;
randomNum = randInt(1,upperLimit);
one_ornament = ornaments[randomNum];
Once the ornament code is determined (either by random or by user choice) the replacement notes and durations can be selected. Once again a basic javascript object is used as a dictionary to help make the translation. For the note transformation we will use a map type function discussed in a moment. But the name of that function is picked using the object shown below. Once the ornament code name is translated into the function name the name is passed as an argument to another function that does the selecting of notes for that ornament. An example is shown later.
var mapOrnamentTagToFunction = {
'unt': upperNeighboringTone,
'lnt': lowerNeighboringTone,
'upt': upperPassingTone,
'lpt': lowerPassingTone,
'ap': appoggiatura,
'no': noOrnament,
'ut1': upperTurn1,
'ut2': upperTurn2,
'lt1': lowerTurn1,
'lt2': lowerTurn2,
'12321': oneTwoThreeTwoOne,
'1231': oneTwoThreeOne,
'1321': oneThreeTwoOne,
'1234': oneTwoThreeFour,
'1324': oneThreeTwoFour,
'1356': oneThreeFiveSix,
'1357': oneThreeFiveSeven,
'13271': oneThreeTwoSevenOne,
'1235': oneTwoThreeFive,
'1345': oneThreeFourFive
}
What I want to do is to translate a duration like a half note (2n in tonejs-speak) into two eights and a quarter ([8n,8n,4n] in tonejs-speak) and other variations. I first created a few variations of note substitution objects like below. For an ornament like a neighboring tone there are a total of three notes. Other ornaments may need a different number of notes.
var oneToThreeDurations = {
... translations of '1n'
"2n": [["4n","8n","8n"],["8n","8n","4n"],["8n","4n","8n"],["4n","8n + 16n","16n"]],
... translations of '4n' and other dotted durations
}
// to be compatible with other duration translation arrays.
// need to wrap the values inside an array of one element
var oneToOneDurations = {
"1n": ["1n"], "2n": ["2n"], "4n": ["4n"],
...
}
// i.e. "1n": "1n", "2n": "2n", ...DOES NOT WORK
// "1n": ["1n"], "2n": ["2n"], ...DOES WORK
Other similar objects named oneToOneDurations oneToTwoDurations oneToFourDurations and oneToFiveDurations are used. The array.length of the value for a key of '2n' is used to randomly select one of the variations.
This part tripped me up:
The oneToOneDurations object needed to have single durations wrapped in an array. Otherwise a duration of '2n' was treated as an array of length 2 (i.e. ['2','n']) instead of a single value, which caused an error in parsing. I spun my wheels for a while figuring out that bug. The code gets a .length property of the value which is an array. But when the value was a string (also has a length property), it returned the length of the string and while it correctly processed elements of arrays for some translations on the oneToOneDurations (the 'no' code) it was seeing a length of two instead of one. Putting the single duration inside of an array (of length 1) was the solution. It seems obvious now.
I wanted to use a similar dictionary object to map the ornament code to the oneToThreeDurations object (or other similar object) but it seems that this technique will return 'strings' and 'references to functions' but not 'references to other objects'. Instead I had to use a function which used a switch statememt to do the same thing. Then using the ornament code we can determine the durations to use.
// this didn't work, it returns undefined
var mapOrnamentTagToDurations = {
'unt': oneToThreeDurations,
'lnt': oneToThreeDurations,
'upt': oneToTwoDurations,
'lpt': oneToTwoDurations,
...
}
// this works
function mapOrnamentTagToDurations(type) {
switch(type) {
case 'unt': return oneToThreeDurations; break;
case 'lnt': return oneToThreeDurations; break;
case 'upt': return oneToTwoDurations; break;
case 'lpt': return oneToTwoDurations; break;
...
default: return noOrnament;
}
oneDurToMultDurs = mapOrnamentTagToDurations(ornament);
myDurationsPool = oneDurToMultDurs[duration]; // i.e. '2n'
durIndex = randInt(0,myDurationsPool.length-1);
myDurations = myDurationsPool[durIndex]; // pick one of the variations
Notice the durIndex is calculated by using a range from 0 to the 'myDurationsPool.length-1' This means that if your add more rhythmic variations to the durations they will dynamically update the length property without any need for a change in the code.
The technique for creating the notes of the ornament is to use a function as a parameter to another function. The function ornamentMap(f,x,s) takes three parameters, (1) a function (f), (2) a note (x), and (3) a scale context (s). The function used as the first parameter is a function like upperNeighboringTone(x,s) which take two parameters, (1) a note and (2) a scale context. This function will return the ornamented version of the note. In the case of upperNeighboringTone() it will return a three note sequence of (1) the note, (2) the next note higher in the scale then (3) back to the original note. Each ornament function has this design.
function ornamentMap(f,x,s) {
return (f(x,s));
}
var upperNeighboringTone = function(x,s) {
var index = s.indexOf(x);
if(index < 0) return x;
var notes = [];
notes.push(x);
notes.push(s[index+1]);
notes.push(s[index])
return notes;
}
// i.e. ornament = 'unt' returns upperNeighboringTone
myfunc = mapOrnamentTagToFunction[ornament];
myNotes = ornamentMap(myfunc, currentNote, myScaleContext);
The notes and durations are reassembled for convenience into arrayOfNoteCells[] with the notes = arrayOfNoteCells[][0] and durations = arrayOfNoteCells[][1] The myNotes and myDurations values are calculated each time through a foreach loop of the arrayOfNoteCells of the melodic contour values to create the entire melody.
inside of createMelodyFromContourNotes()
...
arrayOfNoteCells.forEach( function(currentCell, index, arrayOfNoteCells) {
// calculate myNotes and myDurations for this currentCell
// using the durations and functions that are appropriate for
// each selected ornament
ornament = ornaments[randomNumOrUserChoice];
...
// if ornament = 'unt' this returns upperNeighboringTone
myfunc = mapOrnamentTagToFunction[ornament];
myNotes = ornamentMap(myfunc, currentCell[0], myScaleContext);
oneDurToMultDurs = mapOrnamentTagToDurations(ornament);
myDurationsPool = oneDurToMultDurs[currentCell[1]]; // i.e. '2n'
durIndex = randInt(0,myDurationsPool.length-1);
myDurations = myDurationsPool[durIndex]; // pick one of the variations
newMelody.push(myNotes);
newDurations.push(myDurations);
}
// after foreach loop nest both newMelody and mewDurations
// into a single nested array and return
var combinedNotesAndDurs = [];
combinedNotesAndDurs.push(newMelody);
combinedNotesAndDurs.push(newDurations);
return combinedNotesAndDurs;
Before using the results the nested array needs to be flattened for tonejs to be able to play it properly. This required step was elusive as the logging during debugging didn't always show the nesting of the duration arrays. In other words, I couldn't see the problem using console.log() but until I flatten the nested arrays it didn't work. This is where using the browser dev tools for debugging was helpful as you could see nested arrays more clearly.
Lesson Learned: for debugging USE THE DEV TOOLS available in the javascript console of the the browser.
// this returns both notes and durations
var results = createMelodyFromContourNotes(lilynotes, scale);
var myNotes = flatten(results[0].slice());
var myDurs = flatten(results[1].slice());
Now the data is usable for Tone.js to play.
If more ornaments are added to the code then changes need to made in four places:
If only new rhythmic variations are added to the oneToXXXDurations family of objects then no additional changes are needed beyond those changes. The current code will recognize the new size changes. Nice.
I found that using standard javascript object to make translations was a nice coding pattern. The same results could have been accomplished with a long 'if() else if()...' statement, however I prefer using the javascript object as a dictionary in this situation. I'm not sure why that technique didn't work for returning object (returning strings and function names worked). Perhaps I was doing something wrong, but it was nice to find a quick alternative using a function to return the proper object for the task.
For creating the notes of the ornament the ornamentMap(f, x, s) function (which takes a function as the first parameter) was a good pattern to use in this foreach loop. And using the mapOrnamentTagToFunction object as a dictionary made for a concise way to organize the ornament codes with their respective functions, as it maps the ornament code to the appropriate function to create that ornament. I can see that these types of coding patterns will be useful for future experiments.