Generating a musical scale programmatically
A Pen by Stéphane Busso on CodePen.
Generating a musical scale programmatically
A Pen by Stéphane Busso on CodePen.
/** | |
* MusicalScale | |
* generate a scale of music | |
* | |
* @param key {String} | |
the root of the key. flats will be converted to sharps. | |
C, C#, D, D#, E, F, F#, G, G#, A, A#, B | |
* @param mode {String} | |
desired mode. | |
ionian, dorian, phrygian, lydian, mixolydian, aeolian, locrian, | |
can also pass in: | |
major, minor, melodic, harmonic | |
* | |
* @return {Object} | |
_scale: scale info | |
key: the scale key | |
mode: the scale mode id | |
notes: an array of notes | |
step: index of note in key | |
note: the actual note | |
rel_octave: 0 || 1, in root octave or next | |
triad: major, minor, diminished, or augmented triad for this note | |
interval: I, ii, etc | |
type: min, maj, dim, aug | |
notes: array of notes in the triad | |
note: the note | |
rel_octave: 0 || 1 || 2, relative to key root octave | |
*/ | |
class MusicalScale { | |
constructor(params) { | |
this.dict = this._loadDictionary(); | |
let errors = this._errors(params); | |
if(errors) return; | |
this.updateScale = this.pubUpdateScale; | |
this._loadScale(params); | |
}; | |
pubUpdateScale(params) { | |
let errors = this._errors(params); | |
if(errors) return; | |
this._loadScale(params); | |
}; | |
_loadScale(params) { | |
// clean up the key param | |
this.key = this._paramKey(params.key); | |
// set the mode | |
this.mode = params.mode; | |
this.notes = []; | |
this._scale = this.dict.scales[this._paramMode(this.mode)]; | |
// notes to cycle through | |
let keys = this.dict.keys; | |
// starting index for key loop | |
let offset = keys.indexOf(this.key); | |
for(let s = 0; s < this._scale.steps.length; s++) { | |
let step = this._scale.steps[s]; | |
let idx = (offset + step) % keys.length; | |
// relative octave. 0 = same as root, 1 = next ocave up | |
let rel_octave = (offset + step) > keys.length - 1 ? 1 : 0; | |
// generate the relative triads | |
let triad = this._genTriad(s, idx, rel_octave, this._scale.triads[s]); | |
// define the note | |
let note = { step: s, note: keys[idx], rel_octave: rel_octave, triad: triad }; | |
// add the note | |
this.notes.push(note); | |
} | |
}; | |
// create a chord of notes based on chord type | |
_genTriad(s, offset, octave, t) { | |
// get the steps for this chord type | |
let steps = this.dict.triads[t]; | |
// instantiate the chord | |
let chord = { type: t, interval: this._intervalFromType(s, t), notes: [] }; | |
// load the notes | |
let keys = this.dict.keys; | |
for(let i = 0; i < steps.length; i++) { | |
let step = steps[i]; | |
let idx = (offset + step) % keys.length; | |
// relative octave to base | |
let rel_octave = (offset + step) > keys.length - 1 ? octave + 1 : octave; | |
// define the note | |
chord.notes.push({ note: keys[idx], rel_octave: rel_octave }); | |
} | |
return chord; | |
}; | |
// proper interval notation from the step and type | |
_intervalFromType(step, type) { | |
let steps = 'i ii iii iv v vi vii'.split(' '); | |
let s = steps[step]; | |
switch(type) { | |
case 'maj': | |
s = s.toUpperCase(); break; | |
case 'min': | |
s = s; break; | |
case 'aug': | |
s = s.toUpperCase() + '+'; break; | |
case 'dim': | |
s = s + '°'; break; | |
} | |
return s; | |
}; | |
_errors(params) { | |
if(this.dict.keys.indexOf(params.key) === -1) { | |
if(Object.keys(this.dict.flat_sharp).indexOf(params.key) === -1) { | |
return console.error(`${params.key} is an invalid key. ${this.dict.keys.join(', ')}`); | |
} | |
} else if(this.dict.modes.indexOf(params.mode) === -1) { | |
return console.error(`${params.mode} is an invalid mode. ${this.dict.modes.join(', ')}`); | |
} else { | |
return false; | |
} | |
}; | |
_loadDictionary() { | |
return { | |
keys: 'C C# D D# E F F# G G# A A# B'.split(' '), | |
scales: { | |
ion: { | |
name: 'Ionian', | |
steps: this._genSteps('W W H W W W H'), | |
dominance: [3,0,1,0,2,0,1], | |
triads: this._genTriads(0) | |
}, | |
dor: { | |
name: 'Dorian', | |
steps: this._genSteps('W H W W W H W'), | |
dominance: [3,0,1,0,2,2,1], | |
triads: this._genTriads(1) | |
}, | |
phr: { | |
name: 'Phrygian', | |
steps: this._genSteps('H W W W H W W'), | |
dominance: [3,2,1,0,2,0,1], | |
triads: this._genTriads(2) | |
}, | |
lyd: { | |
name: 'Lydian', | |
steps: this._genSteps('W W W H W W H'), | |
dominance: [3,0,1,2,2,0,1], | |
triads: this._genTriads(3) | |
}, | |
mix: { | |
name: 'Mixolydian', | |
steps: this._genSteps('W W H W W H W'), | |
dominance: [3,0,1,0,2,0,2], | |
triads: this._genTriads(4) | |
}, | |
aeo: { | |
name: 'Aeolian', | |
steps: this._genSteps('W H W W H W W'), | |
dominance: [3,0,1,0,2,0,1], | |
triads: this._genTriads(5) | |
}, | |
loc: { | |
name: 'Locrian', | |
steps: this._genSteps('H W W H W W W'), | |
dominance: [3,0,1,0,3,0,0], | |
triads: this._genTriads(6) | |
}, | |
mel: { | |
name: 'Melodic Minor', | |
steps: this._genSteps('W H W W W W H'), | |
dominance: [3,0,1,0,3,0,0], | |
triads: 'min min aug maj maj dim dim'.split(' ') | |
}, | |
har: { | |
name: 'Harmonic Minor', | |
steps: this._genSteps('W H W W H WH H'), | |
dominance: [3,0,1,0,3,0,0], | |
triads: 'min dim aug min maj maj dim'.split(' ') | |
} | |
}, | |
modes: [ | |
'ionian', 'dorian', 'phrygian', | |
'lydian', 'mixolydian', 'aeolian', | |
'locrian', 'major', 'minor', | |
'melodic', 'harmonic' | |
], | |
flat_sharp: { | |
Cb: 'B', | |
Db: 'C#', | |
Eb: 'D#', | |
Fb: 'E', | |
Gb: 'F#', | |
Ab: 'G#', | |
Bb: 'A#' | |
}, | |
triads: { | |
maj: [0,4,7], | |
min: [0,3,7], | |
dim: [0,3,6], | |
aug: [0,4,8] | |
} | |
}; | |
}; | |
_paramMode(mode) { | |
return { | |
minor: 'aeo', | |
major: 'ion', | |
ionian: 'ion', | |
dorian: 'dor', | |
phrygian: 'phr', | |
lydian: 'lyd', | |
mixolydian: 'mix', | |
aeolian: 'aeo', | |
locrian: 'loc', | |
melodic: 'mel', | |
harmonic: 'har' | |
}[mode]; | |
}; | |
_paramKey(key) { | |
if(this.dict.flat_sharp[key]) return this.dict.flat_sharp[key]; | |
return key; | |
}; | |
_genTriads(offset) { | |
// this is ionian, each mode bumps up one offset. | |
let base = 'maj min min maj maj min dim'.split(' '); | |
let triads = []; | |
for(let i = 0; i < base.length; i++) { | |
triads.push(base[(i + offset) % base.length]); | |
} | |
return triads; | |
}; | |
_genSteps(steps_str) { | |
let arr = steps_str.split(' '); | |
let steps = [0]; | |
let step = 0; | |
for(let i = 0; i < arr.length - 1; i++) { | |
let inc = 0; | |
switch(arr[i]) { | |
case 'W': | |
inc = 2; break; | |
case 'H': | |
inc = 1; break; | |
case 'WH': | |
inc = 3; break; | |
} | |
step += inc; | |
steps.push(step); | |
} | |
return steps; | |
}; | |
} | |
// let scale = new MusicalScale({ key: 'G', mode: 'melodic' }); | |
// let output = scale.notes.map((n) => { return n.triad.type + ' ' + n.triad.interval; }); | |
// console.log(output); |