如何仅通过知道歌曲的和弦序列以编程方式找到歌曲的键?
我问一些人他们将如何确定一首歌的调,他们都说他们是“靠耳朵”或“反复试验”来做的,并通过判断和弦是否能解决一首歌......对于普通的音乐家来说可能很好,但作为一个程序员,这真的不是我想要的答案。
所以我开始寻找与音乐相关的库,看看是否有其他人为此编写了算法。但是,尽管我在 GitHub 上找到了一个名为“tonal”的非常大的库:https ://danigb.github.io/tonal/api/index.html我找不到可以接受和弦数组并返回键的方法.
我选择的语言是 JavaScript (NodeJs),但我不一定要寻找 JavaScript 的答案。伪代码或可以毫不费力地翻译成代码的解释完全没问题。
正如你们中的一些人正确提到的,一首歌的调可以改变。我不确定是否可以足够可靠地检测到密钥更改。所以,现在让我们说,我正在寻找一种算法,它可以很好地近似给定和弦序列的键。
...在查看五度圈后,我想我找到了一个模式来找到属于每个键的所有和弦。我为此写了一个函数getChordsFromKey(key)
。通过针对每个键检查和弦序列的和弦,我可以创建一个数组,其中包含该键与给定和弦序列匹配的可能性的概率calculateKeyProbabilities(chordSequence)
:然后我添加了另一个函数estimateKey(chordSequence)
,它获取概率分数最高的键,然后检查和弦序列的最后一个和弦是否是其中之一。如果是这种情况,它会返回一个仅包含该和弦的数组,否则它会返回一个包含具有最高概率分数的所有和弦的数组。这做得不错,但它仍然没有为很多歌曲找到正确的键或以相等的概率返回多个键。主要问题是像这样的和弦A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G
等不在五度圈内的。事实上,例如 keyC
包含与 key 完全相同的和弦Am
,以及G
相同的Em
等等......
这是我的代码:
'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' ]