Determine the key of a song by its chords
Here's what I came up with. Still new with modern JS so apologies for messiness and bad use of map().
I looked around the internals of the tonal library, it has a function scales.detect(), but it was no good since it required every note present. Instead I used it as inspiration and flattened the progression into a simple note list and checked this in all transpositions as a subset of all the possible scales.
const _ = require('lodash');
const chord = require('tonal-chord');
const note = require('tonal-note');
const pcset = require('tonal-pcset');
const dictionary = require('tonal-dictionary');
const SCALES = require('tonal-scale/scales.json');
const dict = dictionary.dictionary(SCALES, function (str) { return str.split(' '); });
//dict is a dictionary of scales defined as intervals
//notes is a string of tonal notes eg 'c d eb'
//onlyMajorMinor if true restricts to the most common scales as the tonal dict has many rare ones
function keyDetect(dict, notes, onlyMajorMinor) {
//create an array of pairs of chromas (see tonal docs) and scale names
var chromaArray = dict.keys(false).map(function(e) { return [pcset.chroma(dict.get(e)), e]; });
//filter only Major/Minor if requested
if (onlyMajorMinor) { chromaArray = chromaArray.filter(function (e) { return e[1] === 'major' || e[1] === 'harmonic minor'; }); }
//sets is an array of pitch classes transposed into every possibility with equivalent intervals
var sets = pcset.modes(notes, false);
//this block, for each scale, checks if any of 'sets' is a subset of any scale
return chromaArray.reduce(function(acc, keyChroma) {
sets.map(function(set, i) {
if (pcset.isSubset(keyChroma[0], set)) {
//the midi bit is a bit of a hack, i couldnt find how to turn an int from 0-11 into the repective note name. so i used the midi number where 60 is middle c
//since the index corresponds to the transposition from 0-11 where c=0, it gives the tonic note of the key
acc.push(note.pc(note.fromMidi(60+i)) + ' ' + keyChroma[1]);
}
});
return acc;
}, []);
}
const p1 = [ chord.get('m','Bb'), chord.get('m', 'C'), chord.get('M', 'Eb') ];
const p2 = [ chord.get('M','F#'), chord.get('dim', 'B#'), chord.get('M', 'G#') ];
const p3 = [ chord.get('M','C'), chord.get('M','F') ];
const progressions = [ p1, p2, p3 ];
//turn the progression into a flat string of notes seperated by spaces
const notes = progressions.map(function(e) { return _.chain(e).flatten().uniq().value(); });
const possibleKeys = notes.map(function(e) { return keyDetect(dict, e, true); });
console.log(possibleKeys);
//[ [ 'Ab major' ], [ 'Db major' ], [ 'C major', 'F major' ] ]
Some drawbacks:
- doesn't give the enharmonic note you want necessarily. In p2, the more correct response is C# major, but this could be fixed by checking somehow with the original progression.
- won't deal with 'decorations' to chords that are out of the key, which might occur in pop songs, eg. CMaj7 FMaj7 GMaj7 instead of C F G. Not sure how common this is, not too much I think.
The chords in a song of a particular key are predominantly members of the key's scale. I imagine you could get a good approximation statistically (if there is enough data) by comparing the predominant accidentals in the chords listed to the key signatures of the keys.
See https://en.wikipedia.org/wiki/Circle_of_fifths
Of course, a song in any key can/will have accidentals not in the keys scale, so it would likely be a statistical approximation. But over several bars, if you add up the accidentals and filter out all but the ones that occur most often, you may be able to match to a key signature.
Addendum: as Jonas w correctly points out, you may be able to get the signature, but you won't likely be able to determine if it is a major or minor key.