This page started as an experiment in creating an arpeggiator. But it turned out to be something different. It's more of a multi-phrase looper which could be used to create arpeggios or melodic phrases. One of the questions posted at the Tone.js google discussion group asked about a strategy for creating dynamic updates while a loop is playing. An interesting recursive coding technique was suggested as follows:
function scheduleNext(time){
//play note
//schedule the next event relative to the current time by prefixing "+"
Tone.Transport.schedule(scheduleNext, "+" + random(1, 3))
}
The PlayLiveUpdate page uses this basic idea create a basic looper that can be dynamically updated. This page extends that idea to include multiple arrays of notes and durations.
The basic design is to create some input values that have an array of notes and an associated 'number_of_repeats' value. A similar design for durations is used but the duration arrays don't need to match up with the number of notes in the note arrays. Each of the complex arrays will loop again once completed until the stop button is clicked. This page contains four examples and you can see the arrays that are used to create these examples
var example1_durs = [
[["8t","8t","8t","8t","8t","8t"],4]
];
var example1_notes = [
[["C4"],[0,4,7,11,7,4],2],
[["A3"],[0,3,7,12,7,3],2],
[["F3"],[0,7,9,15,9,7],2],
[["G3"],[0,4,10,12,10,4],2]
];
var canon = [
[["D4"],[0,-5,0,4,7,4,0,-5],1],
[["A3"],[0,4,7,12,7,4,0,4],1],
[["B3"],[0,-5,0,3,7,3,0,-5],1],
[["F#3"],[0,3,7,12,7,3,0,3],1],
[["G3"],[0,4,7,12,7,4,0,4],1],
[["D3"],[4,7,12,16,12,7,4,0],1],
[["G3"],[0,4,7,12,7,4,0,4],1],
[["A3"],[0,4,7,12,7,4,0,4],1],
];
var canonRhythm = [
[['16n','16n','16r','16n','16r','16n','16n','16n'],4],
[['16n','16n','16n','16n','16n','16n','16n','16n'],4],
[['16n','16r','16n','16n','16n','16r','16n','16n'],4]
];
var chaos = [
[['A3'],[0,6,2,9,4,7],3],
[['C#4'],[0,6,2,9,4,7],2],
[['F4'],[0,6,2,9,4,7],3],
[['Bb3'],[11,10,9,8,9,10,9,8,6,4,2,0],1]
];
var chaosRhythm = [
[["8n","16n","8n","16n","16n","32n","32n","8n","16n","16n"],3],
[["16n","16n","16n","16n","16n","16n","16n"],2],
[["8n","16n","8r","16n","16n","32n","32n","8r","16n","16n"],3],
[["16n","16n","16r","16n","16n","16r","16n"],2]
];
var example4_notes = [
[['E4'],[12,11,12,7,4,7,0,2,0,-1,0,-5,-8,-5,0,7],1]
];
var example4_durs = [
[['16n','16n','8r','8r','8r','8r','16r','16r','16n','16r','8r','8r','8r','8r','4n','8r'],1],
[['16n','16n','8r','8n','8r','8n','16r','16r','16n','16r','8r','8n','8r','8r','4n','8r'],1],
[['16n','16n','8n','8n','8r','8n','16r','16n','16n','16r','8n','8n','8r','8n','4n','8r'],1],
[['16n','16n','8n','8n','8r','8n','16r','16n','16n','16r','8n','8n','8r','8n','4n','8r'],1],
[['16n','16n','8n','8n','8r','8n','16n','16n','16n','16n','8n','8n','8r','8n','4n','8r'],1],
[['16n','16n','8n','8n','8n','8n','16n','16n','16n','16n','8n','8n','8n','8n','4n','8r'],2],
];
Using nested arrays, the note arrays are comprised of an array of 'three element subarrays'. In the subarray, the first element is a tone center, the second element is an interval array. These two elements are used to create a note array using the function createArpeggio(). The third element of the nested array is the value for number of repeats.
function createArpeggio(rootAndOctave, chordFormulaInput) {
var i;
var noteNames = [];
var midi_notes = [];
// translate rootAndOctave to MIDI Number
var rootMIDI = noteNameToMIDI(rootAndOctave);
// create a MIDI num Array
var midiVal;
for(i=0; i<chordFormulaInput.length; i++) {
midiVal = rootMIDI + chordFormulaInput[i];
midi_notes.push(rootMIDI + chordFormulaInput[i]);
}
// translate to noteNames Array
for(i=0; i<chordFormulaInput.length; i++) {
noteNames.push(MIDIToNoteName(midi_notes[i]));
}
return noteNames;
}
The names of the notes are arbitrarily choosen without regard to the correct enharmonic. So the note E may be spelled Fb and other enharmonic weirdness. The sounds will be correct but the names aren't suitable for standard music notation useage.
The Rhythm arrays are similar but with a two element array, the first element is an array of durations and the second element is the number of repeats for that array of durations. Rests can be created by using a 'r' instead of a 'n' in the notation code (i.e. 8n is an eight note, 8r is an eighth rest). Currently a note will also be skipped during that rest. The functions createArpeggioSequence() and createDursSequence() process these arrays to prepare them for use with scheduleNext() which calls both updateNoteArray() and updateDursArray().
function createArpeggioSequence(score) {
var sequence = [];
var notes = [];
var oneSegment = [];
var total = 0;
var len = score.length;
for(var i=0; i<len; i++) {
oneSegment = [];
notes = createArpeggio(score[i][0], score[i][1]);
total += score[i][2];
oneSegment.push(notes);
oneSegment.push(total);
sequence.push(oneSegment);
}
return sequence;
}
function createDursSequence(score) {
var sequence = [];
var oneSegment = [];
var total = 0;
var len = score.length;
for(var i=0; i<len; i++) {
oneSegment = [];
total += score[i][1];
oneSegment.push(score[i][0]);
oneSegment.push(total);
sequence.push(oneSegment);
}
return sequence;
}
function updateNoteArray(index) {
var newNotes;
var len = notes_sequence.length;
for(var i=0; i<len; i++) {
if(notes_sequence[i][1] == index) {
if((i+1)<len) {
newNotes = notes_sequence[i+1][0];
} else if(i == len-1) {
newNotes = notes_sequence[0][0];
loopNumNotes = 0;
}
}
}
if(newNotes) {
liveNotes = newNotes;
}
return;
}
function updateDursArray(index) {
var newDurs;
var len = durs_sequence.length;
for(var i=0; i<len; i++) {
if(durs_sequence[i][1] == index) {
if((i+1)<len) {
newDurs = durs_sequence[i+1][0];
} else if(i == len-1) {
newDurs = durs_sequence[0][0];
loopNumDurs = 0;
}
}
}
if(newDurs) {
liveDurations = newDurs;
}
return;
}
function scheduleNext(time) {
noteIndex = (noteIndex < notes.length-1)? noteIndex+1 : 0;
if(noteIndex == 0) {
loopNumNotes += 1;
updateNoteArray(loopNumNotes);
}
notes = liveNotes;
myNote = notes[noteIndex];
durationIndex = (durationIndex < durations.length-1)? durationIndex+1 : 0;
if(durationIndex == 0) {
loopNumDurs += 1;
updateDursArray(loopNumDurs);
}
durations = liveDurations;
myDuration = durations[durationIndex];
//play note (only if it's not a rest)
if(myDuration.indexOf('r') == -1) {
synth.triggerAttackRelease(myNote, myDuration, time);
} else {
// fix the rest duration for Tone.Transport.schedule(),
// slice off the 'r' and add 'n'
myDuration = myDuration.slice(0,myDuration.length-1)
myDuration += 'n';
}
//schedule the next event relative to the current time by prefixing "+"
Tone.Transport.schedule(scheduleNext, "+" + myDuration);
}
When the examples menu is changed the function updateExample() loads new notes and rhythms into the arpeggiator.
function updateExample() {
var example = document.myForm.examples.value;
if(example == 'example1') {
durs_sequence = createDursSequence(example1_durs);
notes_sequence = createArpeggioSequence(example1_notes);
} else if(example == 'example2') {
durs_sequence = createDursSequence(chaosRhythm);
notes_sequence = createArpeggioSequence(chaos);
} else if(example == 'example3') {
durs_sequence = createDursSequence(canonRhythm);
notes_sequence = createArpeggioSequence(canon);
} else if(example == 'example4') {
durs_sequence = createDursSequence(example4_durs);
notes_sequence = createArpeggioSequence(example4_notes);
} else {
durs_sequence = createDursSequence(example1_durs);
notes_sequence = createArpeggioSequence(example1_notes);
}
noteIndex = -1;
liveNotes = notes_sequence[0][0];
durationIndex = -1;
liveDurations = durs_sequence[0][0];
loopNumNotes = -1;
loopNumDurs = -1;
}
And the start button sets it all in motion by calling startArpeggiator().
var durs_sequence = createDursSequence(example1_durs);
var notes_sequence = createArpeggioSequence(example1_notes);
var noteIndex = -1;
var notes = notes_sequence[0][0];
var durationIndex = -1;
var durations = durs_sequence[0][0];
var loopNumNotes = -1;
var loopNumDurs = -1;
var liveNotes = [];
var liveDurations = [];
// init
liveNotes = notes;
liveDurations = durations;
var myNote;
var myDuration;
var synth = new Tone.Synth().toMaster();
function startArpeggiator() {
if(Tone.Transport.state != 'started') {
document.querySelector("#isRunning").textContent = "Running";
noteIndex = -1;
durationIndex = -1;
updateVolume();
updateTempo();
scheduleNext('+0.05');
Tone.Transport.start('+0.1');
}
}
The four examples demostrate some different useage of this technique.
Choose from the menu to hear an example of the use of the 'arpeggiator'
Since the user is typing data directly into the program there needs to be some validity check on that input. If the input passes the validity test of checkBackstageNotes() and checkBackstageDurations(), it becomes the new updated data otherwise no changes are made. Tone.js has helper functions for type checking. One of the functions, Tone.isNote(), checks for valid note/octave names. That makes the checkBackstageNotes() function very simple. Since there are several valid ways to enter durations the checkBackstageDurations() function is more involved. This page allows only duration notation (2n, 4n, etc.) in the backstageDuration field enforced by checkBackstageDurations().
Remember to click update after each change as you're just preparing the changes in the backstage, they don't take effect until you click update.