Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
81 views
in Technique[技术] by (71.8m points)

javascript - Determine the key of a song by its chords

How can I programmatically find the key of a song just by knowing the chord sequence of the song?
I asked some people how they would determine the key of a song and they all said they do it 'by ear' or by 'trial and error' and by telling if a chord resolves a song or not... For the average musician that is probably fine, but as a programmer that really isn't the answer that I was looking for.

So I started looking for music related libraries to see if anyone else has written an algorithm for that yet. But although I found a really big library called 'tonal' on GitHub: https://danigb.github.io/tonal/api/index.html I couldn't find a method that would accept an array of chords and return the key.

My language of choice will be JavaScript (NodeJs), but I'm not necessarily looking for a JavaScript answer. Pseudo code or an explanation that can be translated into code without too much trouble would be totally fine.

As some of you mentioned correctly, the key in a song can change. I'm not sure if a change in key could be detected reliably enough. So, for now let's just say, I'm looking for an algorithm that makes a good approximation on the key of a given chord sequence.

... After looking into the circle of fifths, I think I found a pattern to find all chords that belong to each key. I wrote a function getChordsFromKey(key) for that. And by checking the chords of a chord sequence against every key, I can create an array containing probabilities of how likely it is that the key matches the given chord sequence: calculateKeyProbabilities(chordSequence). And then I added another function estimateKey(chordSequence), which takes the keys with the highest probability-score and then checks if the last chord of the chord sequence is one of them. If that is the case, it returns an array containing only that chord, otherwise it returns an array of all chords with the highest probability-score. This does an OK job, but it still doesn't find the correct key for a lot of songs or returns multiple keys with equal probabililty. The main problem being chords like A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G etc. that are not in the circle of fifths. And the fact that for instance the key C contains the exact same chords as the key Am, and G the same as Em and so on...
Here is my code:

'use strict'
const normalizeMap = {
    "Cb":"B",  "Db":"C#",  "Eb":"D#", "Fb":"E",  "Gb":"F#", "Ab":"G#", "Bb":"A#",  "E#":"F",  "B#":"C",
    "Cbm":"Bm","Dbm":"C#m","Eb":"D#m","Fbm":"Em","Gb":"F#m","Ab":"G#m","Bbm":"A#m","E#m":"Fm","B#m":"Cm"
}
const circleOfFifths = {
    majors: ['C', 'G', 'D', 'A',  'E',  'B',  'F#', 'C#', 'G#','D#','A#','F'],
    minors: ['Am','Em','Bm','F#m','C#m','G#m','D#m','A#m','Fm','Cm','Gm','Dm']
}

function estimateKey(chordSequence) {
    let keyProbabilities = calculateKeyProbabilities(chordSequence)
    let maxProbability = Math.max(...Object.keys(keyProbabilities).map(k=>keyProbabilities[k]))
    let mostLikelyKeys = Object.keys(keyProbabilities).filter(k=>keyProbabilities[k]===maxProbability)

    let lastChord = chordSequence[chordSequence.length-1]

    if (mostLikelyKeys.includes(lastChord))
         mostLikelyKeys = [lastChord]
    return mostLikelyKeys
}

function calculateKeyProbabilities(chordSequence) {
    const usedChords = [ ...new Set(chordSequence) ] // filter out duplicates
    let keyProbabilities = []
    const keyList = circleOfFifths.majors.concat(circleOfFifths.minors)
    keyList.forEach(key=>{
        const chords = getChordsFromKey(key)
        let matchCount = 0
        //usedChords.forEach(usedChord=>{
        //    if (chords.includes(usedChord))
        //        matchCount++
        //})
        chords.forEach(chord=>{
            if (usedChords.includes(chord))
                matchCount++
        })
        keyProbabilities[key] = matchCount / usedChords.length
    })
    return keyProbabilities
}

function getChordsFromKey(key) {
    key = normalizeMap[key] || key
    const keyPos = circleOfFifths.majors.includes(key) ? circleOfFifths.majors.indexOf(key) : circleOfFifths.minors.indexOf(key)
    let chordPositions = [keyPos, keyPos-1, keyPos+1]
    // since it's the CIRCLE of fifths we have to remap the positions if they are outside of the array
    chordPositions = chordPositions.map(pos=>{
        if (pos > 11)
            return pos-12
        else if (pos < 0)
            return pos+12
        else
            return pos
    })
    let chords = []
    chordPositions.forEach(pos=>{
        chords.push(circleOfFifths.majors[pos])
        chords.push(circleOfFifths.minors[pos])
    })
    return chords
}

// TEST

//console.log(getChordsFromKey('C'))
const chordSequence = ['Em','G','D','C','Em','G','D','Am','Em','G','D','C','Am','Bm','C','Am','Bm','C','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Am','Am','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em','Em','C','D','Em']

const key = estimateKey(chordSequence)
console.log('Example chord sequence:',JSON.stringify(chordSequence))
console.log('Estimated key:',JSON.stringify(key)) // Output: [ 'Em' ]
question from:https://stackoverflow.com/questions/45399081/determine-the-key-of-a-song-by-its-chords

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...