For*_*vin 40 javascript node.js music-notation
如何通过了解歌曲的和弦顺序以编程方式找到歌曲的键?
我问过一些人他们如何确定一首歌的关键,他们都说他们是"通过耳朵"或"反复试验"并通过判断一个和弦是否能解决一首歌......对于普通音乐家而言可能很好,但作为一个程序员,真的不是我想要的答案.
所以我开始寻找音乐相关的库,看看是否还有其他人为此编写过算法.但是我在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是不在五分之一圈内.事实上,例如,键C包含与键完全相同的和弦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' ]Run Code Online (Sandbox Code Playgroud)
Ric*_*dMD 13
在特定密钥的一首歌的和弦是主要的关键是一个规模的成员.我想通过将所列出的和弦中的主要意外事件与键的关键签名进行比较,你可以在统计上得到一个很好的近似值(如果有足够的数据).
请参阅https://en.wikipedia.org/wiki/Circle_of_fifths
当然,任何键中的一首歌都可能/将会出现意外而不是按键音阶,所以很可能是统计近似值.但是在几个条形图中,如果你将偶然事件加起来并过滤除了最常出现的事故之外的所有事件,你可以匹配一个关键签名.
附录:正如Jonas w正确指出的那样,你可以获得签名,但是你不太可能确定它是主键还是次键.
Luc*_*cas 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' ] ]
Run Code Online (Sandbox Code Playgroud)
一些缺点:
- 没有给你想要的和谐音符.在p2中,更正确的响应是C#major,但这可以通过以某种方式检查原始进展来修复.
- 不会将"装饰"处理到关键之外的和弦,这可能出现在流行歌曲中,例如.CMaj7 FMaj7 GMaj7而不是CF G.不确定这是多么常见,我认为不是太多.
给出一系列这样的音调:
var tones = ["G","Fis","D"];
Run Code Online (Sandbox Code Playgroud)
我们首先可以生成一组独特的音调:
tones = [...new Set(tones)];
Run Code Online (Sandbox Code Playgroud)
然后我们可以检查#和bs的外观:
var sharps = ["C","G","D","A","E","H","Fis"][["Fis","Cis","Gis","Dis","Ais","Eis"].filter(tone=>tones.includes(tone)).length];
Run Code Online (Sandbox Code Playgroud)
然后用bs做同样的事情并获得结果:
var key = sharps === "C" ? bs:sharps;
Run Code Online (Sandbox Code Playgroud)
但是,你仍然不知道它的主要或次要,以及许多组成人员不关心上层规则(并改变了中间的关键)......
对于每个"支持"的刻度,您也可以使用键保持结构,其值为与该刻度匹配的和弦的数组.
根据和弦进程,您可以根据您的结构制作一个键列表.
通过多个匹配,您可以尝试进行有根据的猜测.例如,将其他"权重"添加到与根音符匹配的任何比例.
您可以使用螺旋阵列,这是由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.
一种方法是找到所有正在演奏的音符,并将其与不同音阶的签名进行比较,以找出最匹配的音符。
通常,比例尺签名非常独特。自然的小音阶将具有与大音阶相同的音符(对所有模式都是如此),但是通常当我们说小音阶时,我们指的是谐波小音阶,它具有特定的特征。
因此,将和弦中的音符与不同音阶进行比较,可以得出一个不错的估计。您可以通过在不同的音符上增加一些权重来进行优化(例如,最重的音符,或第一个和最后一个和弦,每个和弦的音调等)。
这似乎可以准确地处理大多数基本情况:
'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)
})Run Code Online (Sandbox Code Playgroud)
<input type="text" id="chodseq" />
<button id="showguess">
Click to guess the key
</button>Run Code Online (Sandbox Code Playgroud)
在您的示例中,它给出G大调,这是因为在次和音阶上没有D大调或Bm和弦。
您可以尝试简单的方法:C,F,G或Eb,Fm,Gm
或一些意外事故:C,D7,G7(这将给您2个猜测,因为存在真正的歧义,如果不提供更多信息,则可能两者都有)
发生事故但准确的一个:C,Dm,G,A