42

如何仅通过知道歌曲的和弦序列以编程方式找到歌曲的键?
我问一些人他们将如何确定一首歌的调,他们都说他们是“靠耳朵”或“反复试验”来做的,并通过判断和弦是否能解决一首歌......对于普通的音乐家来说可能很好,但作为一个程序员,这真的不是我想要的答案。

所以我开始寻找与音乐相关的库,看看是否有其他人为此编写了算法。但是,尽管我在 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' ]

4

8 回答 8

13

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

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

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

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

于 2017-07-30T11:03:09.797 回答
11

这就是我想出的。现代 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# 专业,但这可以通过以某种方式检查原始进程来解决。
-‎ 不会处理可能出现在流行歌曲中的和弦的“装饰”,例如。CMaj7 FMaj7 GMaj7 而不是 CF G。不知道这有多普遍,我认为不是太多。

于 2017-09-26T14:31:55.637 回答
8

给定这样的音调数组:

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;

但是,您仍然不知道它是主要还是次要,并且许多作曲家不关心上层规则(并更改了中间的键)...

于 2017-07-30T11:21:48.803 回答
7

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

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

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

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

'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

于 2018-03-09T21:15:16.583 回答
7

您也可以为每个“支持的”比例保留一个带有键的结构,并将一个带有与该比例匹配的和弦的数组作为值。

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

通过多个匹配项,您可以尝试做出有根据的猜测。例如,将其他“重量”添加到与根音匹配的任何音阶。

于 2017-07-30T11:06:14.717 回答
6

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

Chuan、Ching-Hua 和 Elaine Chew。“使用螺旋阵列 CEG 算法查找和弦音频键。” 多媒体和博览会,2005。ICME 2005。IEEE 国际会议。IEEE,2005 年。

我最近的张力模型(在此处的 .jar 文件中提供)还基于螺旋阵列输出密钥(除了张力测量值)。它可以将 musicXML 文件或文本文件作为输入,只为您的作品中的每个“时间窗口”提供一个音高名称列表。

Herremans D., Chew E.. 2016. 张力带:量化和可视化色调张力。第二届国际音乐符号和表示技术会议(TENOR)。2:8-18。

于 2017-08-08T04:05:01.467 回答
1

如果您不反对切换语言,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')

回报:<music21.key.Key of c minor>

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

于 2019-09-11T23:46:51.327 回答
0

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

https://www.mazmazika.com/chordanalyzer

于 2021-01-20T03:12:46.267 回答