根据和弦确定歌曲的调性

Determine the key of a song by its chords

如何仅通过知道歌曲的和弦顺序以编程方式找到歌曲的调?
我问过一些人他们如何确定歌曲的基调,他们都说他们这样做 'by ear' 或 'trial and error' 并通过判断和弦是否解决了歌曲......对于普通音乐家这可能很好,但作为一名程序员,这确实不是我一直在寻找的答案。

所以我开始寻找与音乐相关的库,看看是否还有其他人为此编写了算法。但是,尽管我在 GitHub 上找到了一个名为 'tonal' 的非常大的库:https://danigb.github.io/tonal/api/index.html 我找不到可以接受和弦数组和 return 键的方法。

我选择的语言是 JavaScript (NodeJs),但我不一定要寻找 JavaScript 答案。伪代码或可以轻松翻译成代码的解释是完全可以的。

正如你们中的一些人正确提到的那样,歌曲中的调可以改变。我不确定是否可以足够可靠地检测到密钥的更改。所以,现在我们只是说,我正在寻找一种算法,它可以很好地近似给定和弦序列的键。

... 在研究了五度圈之后,我想我找到了一种模式来找到属于每个键的所有和弦。我为此写了一个函数getChordsFromKey(key)。通过针对每个键检查和弦序列的和弦,我可以创建一个数组,其中包含该键与给定和弦序列匹配的可能性的概率:calculateKeyProbabilities(chordSequence)。然后我添加了另一个函数 estimateKey(chordSequence),它采用概率得分最高的键,然后检查和弦序列的最后一个和弦是否是其中之一。如果是这种情况,它 return 是一个仅包含该和弦的数组,否则它 return 是具有最高概率分数的所有和弦的数组。 这做的不错,但它仍然没有找到很多歌曲的正确键或 returns 多个相同概率的键。主要问题是 A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G 等不在五度圈内的和弦。事实上,例如键 C 包含与键 Am 完全相同的和弦,而 GEm 相同,依此类推...
这是我的代码:

'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' ]

特定调的歌曲中的和弦主要 调音阶的成员。我想您可以通过将列出的和弦中的主要临时记号与琴键的调号进行比较,在统计上(如果有足够的数据)得到一个很好的近似值。

https://en.wikipedia.org/wiki/Circle_of_fifths

当然,任何调 can/will 中的歌曲都有不在调音阶中的临时记号,因此它可能是统计近似值。但是在几个小节上,如果您将临时记号加起来并过滤掉除最常出现的以外的所有临时记号,您也许能够匹配调号。

附录:正如 Jonas w 正确指出的那样,您可能能够获得签名,但您可能无法确定它是主键还是次要键。

您也可以为每个 "supported" 音阶保留一个结构,其中包含与该音阶匹配的和弦的数组作为值。

给定一个和弦进行,然后您可以根据您的结构制作一个候选键列表。

对于多个匹配项,您可以尝试做出有根据的猜测。例如,将其他 "weight" 添加到与根音匹配的任何音阶。

给定一组这样的音调:

var tones = ["G","Fis","D"];

我们可以先生成一组独特的音调:

tones = [...new Set(tones)];

然后我们可以检查 # 和 bs 的出现:

var sharps = ["C","G","D","A","E","H","Fis"][["Fis","Cis","Gis","Dis","Ais","Eis"].filter(tone=>tones.includes(tone)).length];

然后对 bs 做同样的事情并得到结果:

var key = sharps === "C" ? bs:sharps;

然而,你仍然不知道它是 major 还是 minor,并且许多作曲家不关心上层规则(并更改中间的关键)...

您可以使用螺旋阵列,这是 Elaine Chew 创建的调性 3D 模型,它具有关键检测算法。

川、Ching-Hua 和 Elaine Chew。 “Polyphonic audio key finding using the spiral array CEG algorithm。” Multimedia and Expo, 2005. ICME 2005. IEEE 国际会议。 IEEE, 2005.

我最近的张力模型,在.jar file here中可用,也输出基于螺旋阵列的键(除了张力测量)。它可以采用 musicXML 文件或文本文件作为输入,只采用您作品中每个 'time window' 的音高名称列表。

Herremans D., Chew E.. 2016。Tension ribbons: Quantifying and visualising tonal tension。第二届国际音乐符号和表示技术会议 (TENOR)。 2:8-18.

这是我想出的。对于现代 JS 来说仍然是新事物,因此对于 map() 的混乱和错误使用深表歉意。

我查看了音调库的内部结构,它有一个函数 scales.detect(),但它并不好,因为它需要每个音符都存在。相反,我以它为灵感,将进程扁平化为一个简单的音符列表,并在所有转调中将其作为所有可能音阶的子集进行检查。

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' ] ]

一些缺点:
- 不一定会给出您想要的等音符。在 p2 中,更正确的答案是 C# major,但这可以通过以某种方式检查原始进程来解决。
- 不会处理 'decorations' 到调外的和弦,这可能出现在流行歌曲中,例如。 CMaj7 FMaj7 GMaj7 而不是 C F G。不确定这有多常见,我认为不是太多。

一种方法是找到所有正在演奏的音符,并与不同音阶的签名进行比较,看看哪个是最佳匹配。

通常,音阶签名非常独特。自然小调音阶将具有与大调音阶相同的音符(这对所有模式都是如此),但通常当我们说小调音阶时,我们指的是和声小调音阶,它具有特定的特征。

因此,比较和弦中的音符与您的不同音阶应该会给您一个很好的估计。你可以通过为不同的音符添加一些权重来改进(例如出现最多的音符,或者第一个和最后一个和弦,每个和弦的主音等)

这似乎可以准确地处理大多数基本情况:

'use strict'
const allnotes = [
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"
]

// you define the scales you want to validate for, with name and intervals
const scales = [{
  name: 'major',
  int: [2, 4, 5, 7, 9, 11]
}, {
  name: 'minor',
  int: [2, 3, 5, 7, 8, 11]
}];

// you define which chord you accept. This is easily extensible,
// only limitation is you need to have a unique regexp, so
// there's not confusion.

const chordsDef = {
  major: {
    intervals: [4, 7],
    reg: /^[A-G]$|[A-G](?=[#b])/
  },
  minor: {
    intervals: [3, 7],
    reg: /^[A-G][#b]?[m]/
  },
  dom7: {
    intervals: [4, 7, 10],
    reg: /^[A-G][#b]?[7]/
  }
}

var notesArray = [];

// just a helper function to handle looping all notes array
function convertIndex(index) {
  return index < 12 ? index : index - 12;
}


// here you find the type of chord from your 
// chord string, based on each regexp signature
function getNotesFromChords(chordString) {

  var curChord, noteIndex;
  for (let chord in chordsDef) {
    if (chordsDef[chord].reg.test(chordString)) {
      var chordType = chordsDef[chord];
      break;
    }
  }

  noteIndex = allnotes.indexOf(chordString.match(/^[A-G][#b]?/)[0]);
  addNotesFromChord(notesArray, noteIndex, chordType)

}

// then you add the notes from the chord to your array
// this is based on the interval signature of each chord.
// By adding definitions to chordsDef, you can handle as
// many chords as you want, as long as they have a unique regexp signature
function addNotesFromChord(arr, noteIndex, chordType) {

  if (notesArray.indexOf(allnotes[convertIndex(noteIndex)]) == -1) {
    notesArray.push(allnotes[convertIndex(noteIndex)])
  }
  chordType.intervals.forEach(function(int) {

    if (notesArray.indexOf(allnotes[noteIndex + int]) == -1) {
      notesArray.push(allnotes[convertIndex(noteIndex + int)])
    }

  });

}

// once your array is populated you check each scale
// and match the notes in your array to each,
// giving scores depending on the number of matches.
// This one doesn't penalize for notes in the array that are
// not in the scale, this could maybe improve a bit.
// Also there's no weight, no a note appearing only once
// will have the same weight as a note that is recurrent. 
// This could easily be tweaked to get more accuracy.
function compareScalesAndNotes(notesArray) {
  var bestGuess = [{
    score: 0
  }];
  allnotes.forEach(function(note, i) {
    scales.forEach(function(scale) {
      var score = 0;
      score += notesArray.indexOf(note) != -1 ? 1 : 0;
      scale.int.forEach(function(noteInt) {
        // console.log(allnotes[convertIndex(noteInt + i)], scale)

        score += notesArray.indexOf(allnotes[convertIndex(noteInt + i)]) != -1 ? 1 : 0;

      });

      // you always keep the highest score (or scores)
      if (bestGuess[0].score < score) {

        bestGuess = [{
          score: score,
          key: note,
          type: scale.name
        }];
      } else if (bestGuess[0].score == score) {
        bestGuess.push({
          score: score,
          key: note,
          type: scale.name
        })
      }



    })
  })
  return bestGuess;

}


document.getElementById('showguess').addEventListener('click', function(e) {
  notesArray = [];
  var chords = document.getElementById('chodseq').value.replace(/ /g,'').replace(/["']/g,'').split(',');
  chords.forEach(function(chord) {
    getNotesFromChords(chord)
  });
  var guesses = compareScalesAndNotes(notesArray);
  var alertText = "Probable key is:";
  guesses.forEach(function(guess, i) {
    alertText += (i > 0 ? " or " : " ") + guess.key + ' ' + guess.type;
  });
  
  alert(alertText)
  
})
<input type="text" id="chodseq" />

<button id="showguess">
Click to guess the key
</button>

对于你的例子,它给出了 G 大调,那是因为在和声小调音阶中,没有 D 大调或 Bm 和弦。

您可以尝试简单的:C、F、G 或 Eb、Fm、Gm

或者有意外的:C、D7、G7(这个会给你2个猜测,因为真的有歧义,不给出更多信息,可能两者都是)

有意外但准确的:C、Dm、G、A

如果您不反对切换语言,Python 中的 music21(我的图书馆,免责声明)会这样做:

from music21 import stream, harmony

chordSymbols = ['Cm', 'Dsus2', 'E-/C', 'G7', 'Fm', 'Cm']
s = stream.Stream()
for cs in chordSymbols:
    s.append(harmony.ChordSymbol(cs))
s.analyze('key')

Returns: <music21.key.Key of c minor>

系统会知道 C# 专业和 Db 专业之间的区别。它有完整的和弦名称词汇表,所以 "Dsus2" 之类的东西不会混淆它。唯一可能会咬新人的是公寓是用减号写的,所以 "E-/C" 而不是 "Eb/C"

有一个在线免费工具(MazMazika Songs Chord Analyzer),可以非常快速地分析和检测任何歌曲的和弦。您可以通过文件上传 (MP3/WAV) 或粘贴 YouTube / SoundCloud 链接来处理歌曲。处理文件后,您可以播放歌曲,同时实时看到所有和弦一起播放,以及包含所有和弦的 table,每个和弦都分配有时间位置和数字 ID,您可以单击它直接转到相应的和弦及其时间位置。

https://www.mazmazika.com/chordanalyzer