Chords and Arpeggiators with Audiolib.js

So, as you may have noticed with my recent flurry of blog posts I've been playing with Audiolib.js.  I can't thank the author Jussi Kalliokoski enough for getting this project going.  When I did my Flash version, I spent many moons getting the audio buffer working correctly, then the oscillators, and I hadn't even gotten started on effects so much yet.

But audiolib.js has all of this, which means I can focus on the usage with the API rather than making the low level audio stuff work.

For example, Chords and Arpeggiation! Sorry! This only works in the latest version of Chrome

After a couple nights with the project, I extended out the standard Oscillator with a Note.  I've since refactored a bit to do the minimum amount of music theory in the Note Oscillator object itself, and offload into a different object - but the net effect is that instead of instantiating a new Oscillator with a frequency, we can instantiate a new Note with a musical notation.  For example with Oscillator, we'd say 440hz, with Note, we'd say "A" or "A4" (if you don't want to assume the 4th octave).

The obvious extension to this is groups of notes - or rather Chords.  There are two challenges here.  The first is to convert a chord notation to array of notes.  For example, if I say "Cm", or C Minor, I want an array such as ["C", "Eb", "G"].  We need to convert the C Minor notation into the implied triad of 3 notes using the minor scale.  Also possible is a 6th, 7th, 9th, 11th, or 13th.  In fact, now that I type, I realize I never did a 5th, which if I recall correctly, omits the middle note of a triad.

It all came out fairly well.  I don't think augmentation, sustain, or diminish work properly yet, but the chord structures are there. The simplest way to use this is to use ChordFactory instead of Chord. Simply call ChordFactory.createNotations("C", 4) to get a list of notes (strings) or ChordFactory.createNotes("C", 4) to get an array of Note Oscillators.

ChordConstants = { MAJOR_TRIAD: "maj", MINOR_TRIAD: "m", SEVENTH: "7", MINOR_SEVENTH: "m7", MAJOR_SEVENTH: "maj7", NINTH: "9", MINOR_NINTH: "m9", MAJOR_NINTH: "maj9", ELEVENTH: "11", THIRTEENTH: "13", SIXTH: "6", MINOR_SIXTH: "m6", SUSTAIN: "sus", AUGMENTED: "aug", DIMINISHED: "dim" }; ChordFactory = { /** * create a list of notations from chord * @param chord notation * @param notation array (individual notes) */ createNotations: function createNotations(notation, octave) { var chord = new Chord(notation, octave); return chord.getNotations(); }, /** * create an array of note oscillators using the audiolib framework * @param sampleRate * @param notation */ createNotes: function createNotes(sampleRate, notation) { var chord = new Chord(notation); nts = chord.getNotations(); var osc; var oscs = []; for (var nt in nts) { osc = audioLib.generators.Note(sampleRate, nts[nt]) oscs.push(osc); } return oscs; } } function Chord(notation, octave) { var that = this; /** root note of chord */ that._root = "C"; /** octave of root */ if (octave) { that._rootOctave = octave; } else { that._rootOctave = null; } /** chord notation */ that._notation = notation ? notation : "Cmaj"; /** notes in built chord */ that._notes = []; /** * get notes from built chords * * @return notes */ this.getNotations = function() { return this._notes; } /** * chord notation setter * * @param notation */ this.setNotation = function(value) { this._notation = value; this.buildChord(); } /** * chord notation getter * * @return notation */ this.getNotation = function() { return this._notation; } /** * root note setter * * @param root */ this.setRoot = function(value) { this._root = value; this.buildChord(); } /** * root note getter * * @return root note */ this.getRoot = function() { return this._root; } /** * root octave setter * * @param octave */ this.setRootOctave = function(value) { this._rootOctave = value; this.buildChord(); } /** * root octave getter * * @return root octave */ this.getRootOctave = function() { return this._rootOctave; } /** * get notes in major triad * * @param root note * @param root octave * @return notes */ this.majorTriad = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,3); } /** * get notes in minor triad * * @param root note * @param root octave * @return notes */ this.minorTriad = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, false, false, rootOctave).slice(0,3); } /** * get notes in seventh chord * * @param root note * @param root octave * @return notes */ this.seventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,4); } /** * get notes in major seventh chord * * @param root note * @param root octave * @return notes */ this.majorSeventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,4); } /** * get notes in minor seventh chord * * @param root note * @param root octave * @return notes */ this.minorSeventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, false, false, rootOctave).slice(0,4); } /** * get notes in ninth chord * * @param root note * @param root octave * @return notes */ this.ninth = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,5); } /** * get notes in major ninth chord * * @param root note * @param root octave * @return notes */ this.majorNinth = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,5); } /** * get notes in minor ninth chord * * @param root note * @param root octave * @return notes */ this.minorNinth = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, false, false, rootOctave).slice(0,5); } /** * get notes in eleventh chord * * @param root note * @param root octave * @return notes */ this.eleventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,6); } /** * get notes in major eleventh chord * * @param root note * @param root octave * @return notes */ this.majorEleventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,6); } /** * get notes in minor eleventh chord * * @param root note * @param root octave * @return notes */ this.minorEleventh = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, false, false, rootOctave).slice(0,6); } /** * get notes in thirteenth chord * * @param root note * @param root octave * @return notes */ this.thirteenth = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,7); } /** * get notes in major thirteenth chord * * @param root note * @param root octave * @return notes */ this.majorThirteenth = function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, true, false, rootOctave).slice(0,7); } /** * get notes in minor thirteenth chord * * @param root note * @param root octave * @return notes */ this.minorThirteenth= function(root, rootOctave) { return this.getStandardNotesInChordMakeup(root, false, false, rootOctave).slice(0,7); } /** * get notes in sixth chord * * @param root note * @param root octave * @return notes */ this.sixth = function(root, rootOctave) { var keySig = Note.notesInKeySignature(root, true, rootOctave); var keys = new Array(); keys.push(keySig[0], keySig[2], keySig[4], keySig[5]); return keys; } /** * get notes in minor sixth chord * * @param root note * @param root octave * @return notes */ this.minorSixth = function(root, rootOctave) { var keySig = Note.notesInKeySignature(root, false, rootOctave); var keys = new Array(); keys.push(keySig[0], keySig[2], keySig[4], keySig[5]); return keys; } /** * sustain chord * * @param notes * @param direction to sustain * @return notes */ this.sustain = function(notes, sus) { sus = (sus == undefined) ? 4 : sus; // grab the third in the chord var third = notes[1]; var notations = Note.sharpNotations; var thirdIndex = Note.sharpNotations.indexOf(third); if (thirdIndex == -1) { notations = Note.flatNotations; thirdIndex = Note.flatNotations.indexOf(third); } if (sus==2) { // lower the third one half step if (thirdIndex-1 = notations.length) { notes[1] = notations[0]; } else { notes[1] = notations[thirdIndex+1]; } } return notes; } /** * augment chord * * @param notes * @return notes */ this.augment = function(notes) { // grab the fifth in the chord var fifth = notes[2]; var notations = Note.sharpNotations; var fifthIndex = Note.sharpNotations.indexOf(fifth); if (fifthIndex == -1) { notations = Note.flatNotations; fifthIndex = Note.flatNotations.indexOf(fifth); } // raise the fifth one half step if (fifthIndex+1 >= notations.length) { notes[2] = notations[0]; } else { notes[2] = notations[fifthIndex+1]; } return notes; } /** * get all standard notes in a chord, from triad to thirteenth * * @param root note * @param major key (true/false) * @param major chord (true/false) * @param root octave * @return notes array */ this.getStandardNotesInChordMakeup = function(root, majorKey, majorChord, octave) { majorKey = (majorKey == undefined) ? true : majorKey; majorChord = (majorChord == undefined) ? false : majorChord; var majKeySig = Note.notesInKeySignature(root, true, octave); var minKeySig = Note.notesInKeySignature(root, false, octave); // grab the next octave if we need it var majKeySig2 = Note.notesInKeySignature(root, true, octave+1); var minKeySig2 = Note.notesInKeySignature(root, false, octave+1); var notes; if (majorKey && majorChord) { // C Major Seventh for example notes = [ majKeySig[0], majKeySig[2], majKeySig[4], majKeySig[6], majKeySig2[1], majKeySig2[3] ]; } else if (!majorKey && majorChord) { // C Minor Seventh for example notes = [ minKeySig[0], minKeySig[2], minKeySig[4], minKeySig[6], minKeySig2[1], minKeySig2[3] ]; } else if (majorKey && !majorChord) { // C Seventh for example notes = [ majKeySig[0], majKeySig[2], majKeySig[4], minKeySig[6], majKeySig2[1], minKeySig2[3] ]; } else if (!majorKey && !majorChord) { // C Seventh for example notes = [ majKeySig[0], minKeySig[2], majKeySig[4], minKeySig[6], majKeySig2[1], minKeySig2[3] ]; } return notes; } /** * convert notation to note list * * @param notation * @param use the octave in the notation * @return note list */ this.notesFromChordNotation = function(notation, octave) { var root; var major = 0; var chordType; // find root if (notation.charAt(1) == "#" || notation.charAt(1) == "b") { root = notation.substring(0, 2); notation = notation.substring(2); } else { root = notation.substring(0, 1); notation = notation.substring(1); } // major or minor? (3 states - 1 is on, -1 is off, 0 is unspecified) if ( notation.substr(0, 3) == "maj") { major = 1; notation = notation.substring(3); } else if (notation.substr(0, 1) == "m") { major = -1; notation = notation.substring(1); } // set chord type if ( notation.charAt(0) == "6" ) { if (major == -1) { chordType = ChordConstants.MINOR_SIXTH; } else { chordType = ChordConstants.SIXTH; } notation = notation.substring(2); } else if ( notation.charAt(0) == "7" ) { if (major == 0) { chordType = ChordConstants.SEVENTH; } else if (major == 1) { chordType = ChordConstants.MAJOR_SEVENTH; } else if (major == -1) { chordType = ChordConstants.MINOR_SEVENTH; } notation = notation.substring(1); } else if ( notation.charAt(0) == "9" ) { if (major == 0) { chordType = ChordConstants.NINTH; } else if (major == 1) { chordType = ChordConstants.MAJOR_NINTH; } else if (major == -1) { chordType = ChordConstants.MINOR_NINTH; } notation = notation.substring(1); } else if ( notation.substr(0,2) == "11" ) { chordType = ChordConstants.ELEVENTH; notation = notation.substring(2); } else if ( notation.substr(0,2) == "13" ) { chordType = ChordConstants.THIRTEENTH; notation = notation.substring(2); } else { if (major == 1 || major == 0) { chordType = ChordConstants.MAJOR_TRIAD; } else { chordType = ChordConstants.MINOR_TRIAD; } } var notes = this.notesFromChordType(chordType, root, octave); // modify note set if needed var modifier = notation; switch ( modifier.substr(0,3) ) { case ChordConstants.AUGMENTED: notes = augment(notes); break; case ChordConstants.DIMINISHED: // to do break; case ChordConstants.SUSTAIN: var param = int(modifier.charAt(3)); notes = sustain(notes, param); break; } return notes; } /** * get notes from chord types * * @param type * @param chord root * @return notes */ this.notesFromChordType = function(type, root, rootOctave) { switch ( type ) { case ChordConstants.SIXTH: return this.sixth(root, rootOctave); case ChordConstants.MINOR_SIXTH: return this.minorSixth(root, rootOctave); case ChordConstants.SEVENTH: return this.seventh(root, rootOctave); case ChordConstants.MINOR_SEVENTH: return this.minorSeventh(root, rootOctave); case ChordConstants.MAJOR_SEVENTH: return this.majorSeventh(root, rootOctave); case ChordConstants.NINTH: return this.ninth(root, rootOctave); case ChordConstants.MINOR_NINTH: return this.minorNinth(root, rootOctave); case ChordConstants.MAJOR_NINTH: return this.majorNinth(root, rootOctave); case ChordConstants.ELEVENTH: return this.eleventh(root, rootOctave); case ChordConstants.THIRTEENTH: return this.thirteenth(root, rootOctave); case ChordConstants.MAJOR_TRIAD: return this.majorTriad(root, rootOctave); case ChordConstants.MINOR_TRIAD: return this.minorTriad(root, rootOctave); default: return this.majorTriad(root, rootOctave); } } /** * build the chord given the parameters set in this class */ this.buildChord = function() { this._notes = []; var notations = this.notesFromChordNotation(this._notation, this._rootOctave); for (var c = 0; c < notations.length; c++) { this._notes.push(notations[c]); } } // do a build based on initial params that.buildChord(); }

Namespacing will probably come when I think of a name for this project!

Anyway, we have some code that creates chord structures!  Yay!  Now, what about sending this to the buffer for mixing.  The most basic example in audiolib.js is like so:

function audioCallback(buffer, channelCount){ // Fill the buffer with the oscillator output. osc.append(buffer, channelCount); }

This takes the Oscillator (or any audio generator for that matter) and generates some bytes to send to the audio buffer. Audiolib had some examples of mixing, but I didn't care for them so much, as they seemed a little overly complicated with whatever idea of "leads" they had - I was a bit confused. In the end, I came up with something that worked a little better for me, but is probably virtually the same concept:

function audioCallback(buffer, channelCount){ // get a list of generators from our keyboard controller object - it keeps tracks of keys pressed var gens = ctrl.pull(); var bl = buffer.length; var gl = gens.length; // loop through each sample in the buffer for (current=0; current<bl; current+= channelCount){ sample = 0; // here we're combining samples from our list of generators // and combining down into one sample for (i=0; i<gl; i++){ gens[i].generate(); sample += gens[i].getMix()*0.5; } // Here we write this sample to each channel we have (I'm just doing 2 channels) for (n=0; n<channelCount; n++){ buffer[current + n] = sample; } } }

That's pretty much it! We're constructing Oscillators from Chord notations, and then mixing the result for playback.

The last thing I wanted to try was an arpeggiator. An arpeggiator takes a chord structure, and instead of playing all the notes at the same time, only plays one note at a time in sequence.

I extended the Audiolib Oscillator again, making an Arpeggiator plugin. I needed to override the "generate" method. Every time "generate" is called, I'd step through. Once my step count reaches a threshold (the samplerate divided by some number), I'd change to the next frequency.

Here's what I got:

audioLib.generators('Arpeggiator', function (sampleRate, arpeggiatorRate, notes, octave, autoReverse){ // extend Oscillator for ( var prop in audioLib.generators.Oscillator.prototype) { this[prop] = audioLib.generators.Oscillator.prototype[prop]; } // do constructor routine for Note and Oscillator var that = this; if (autoReverse === undefined) { that.autoReverse = true; } else { that.autoReverse = autoReverse; } that.buildFrequencies(notes, octave); that.waveTable = new Float32Array(1); that.sampleRate = sampleRate; that.waveShapes = that.waveShapes.slice(0); that.arpStep = 0; that.arpIndex = 0; that.arpeggiatorRate = arpeggiatorRate; /** * override generate function */ that.generate = function(){ var self = this, f = +self.frequency, pw = self.pulseWidth, p = self.phase; f += f * self.fm; self.phase = (p + f / self.sampleRate / 2) % 1; p = (self.phase + self.phaseOffset) % 1; self._p = p self.sampleRate * self.arpeggiatorRate) { self.arpStep = 0; self.arpIndex ++; if (self.arpIndex >= self.frequencies.length) { self.arpIndex = 0; } self.frequency = self.frequencies[self.arpIndex]; } } }, { buildFrequencies: function(notes, octave) { var self = this; self.frequencies = []; if (!(notes instanceof Array)) { // work with either a chord structure... notes = ChordFactory.createNotations(notes, octave); } for (var c = 0; c 0; c--) { self.frequencies.push( Note.getFrequencyForNotation(notes[c]) ) } } self.frequency = self.frequencies[0]; } });

Lotsa code here tonight! I'd like to get started on some basic effects and envelopes next to make some non-boring tonal qualities to my playback. At that point, I hope to have a nice little useable API I can put up somewhere - especially since I don't have to put lotsa code up!