Play Arpeggios

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'

Examples:

tempo: | volume:

status:Stopped

You can make momentary changes on the fly using the following menus. (Still kind of buggy). The changes don't persist.

backstage Notes:

Current Notes:C4,E4,G4,B4,G4,E4

backstage Durations:

Current Durations:8t,8t,8t,8t,8t,8t

  1. Enter notes and/or durations into the 'Add a menu item' field(s), then press return. That will add to the existing menu and set it up in the 'backStage' area.

  2. You can type directly into the 'backStage' field, but it won't save it to the menu for reuse.

  3. When finished entering what you want into the 'backStage' (via the menu or typing in new values) click the Update Notes button to shift that data from 'backstage' into the active field (the span that shows the Current Notes or Current Durations on the screen) where it is read by scheduleNext() at the next scheduled note. As mentioned, any changes made are not saved and don't persist on the next loop.

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.

Back to the Tone.js Setup page.