Определите ключ песни по ее аккордам
Как программно найти ключ песни, просто зная последовательность аккордов песни?
Я спросил некоторых людей, как они определят тональность песни, и все они сказали, что делают это "на слух" или "методом проб и ошибок", говоря, решает ли аккорд песню или нет... Для обычного музыканта это это, вероятно, хорошо, но как программист, это действительно не тот ответ, который я искал.
Поэтому я начал искать библиотеки, связанные с музыкой, чтобы посмотреть, написал ли кто-нибудь еще алгоритм для этого. Но хотя я нашел действительно большую библиотеку под названием 'tonal' на GitHub: 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' ]
8 ответов
Один из подходов состоит в том, чтобы найти все играемые ноты, сравнить их с сигнатурой разных шкал и посмотреть, какой из них лучше всего подходит.
Обычно подпись масштаба довольно уникальна. Естественная минорная шкала будет иметь те же ноты, что и мажорная шкала (что справедливо для всех режимов), но обычно, когда мы говорим минорная шкала, мы подразумеваем гармоническую минорную шкалу, которая имеет определенную сигнатуру.
Таким образом, сравнение нот в аккордах с разными шкалами должно дать вам хорошую оценку. И вы могли бы уточнить, добавив некоторый вес к различным нотам (например, те, которые подходят больше всего, или первый и последний аккорды, тоника каждого аккорда и т. Д.)
Это, кажется, обрабатывает большинство основных случаев с некоторой точностью:
'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>
Для вашего примера это дает соль мажор, потому что в гармонической минорной шкале нет аккордов ре мажор или Bm.
Вы можете попробовать простые: C, F, G или Eb, Fm, Gm
Или некоторые с авариями: C, D7, G7 (эта даст вам 2 догадки, потому что есть реальная двусмысленность, без дополнительной информации, это может быть и то и другое)
Один с авариями, но точный: C, Dm, G, A
Аккорды в песне определенного ключа являются преимущественно членами гаммы ключа. Я полагаю, что вы могли бы получить хорошее статистическое приближение (если данных достаточно), сравнив преобладающие случайные звуки в перечисленных аккордах с сигнатурами клавиш.
Смотрите https://en.wikipedia.org/wiki/Circle_of_fifths
Конечно, песня в любой тональности может иметь / будет иметь случайные значения не в шкале тональностей, так что, скорее всего, она будет статистической. Но через несколько тактов, если вы сложите случайные данные и отфильтруете все, кроме тех, которые встречаются чаще всего, вы сможете найти соответствие ключевой подписи.
Приложение: как правильно указывает Джонас, вы можете получить подпись, но вряд ли сможете определить, является ли она мажорным или минорным ключом.
Вот что я придумал. Все еще новичок в современном JS, поэтому извиняюсь за беспорядок и плохое использование map().
Я осмотрел внутреннюю часть тональной библиотеки, в ней есть функция scale.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, но это можно исправить, проверив каким-либо образом исходную прогрессию.
- не будет иметь дело с "декорациями" для аккордов, которые вне ключа, которые могут встречаться в поп-песнях, например. CMaj7 FMaj7 GMaj7 вместо C F G. Не уверен, насколько это распространено, я думаю, не слишком много.
Учитывая массив тонов, как это:
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;
Тем не менее, вы до сих пор не знаете, является ли его основным или второстепенным, и многие компоненты не заботятся о верхних правилах (и изменили промежуточный ключ)...
Вы можете использовать спиральную матрицу, трехмерную модель тональности, созданную Элейн Чу, которая имеет алгоритм определения ключа.
Чуан, Чинг-Хуа и Элейн Чу. " Поиск полифонического аудио ключа с использованием алгоритма CEG спирального массива ". Мультимедиа и Экспо, 2005. ICME 2005. Международная конференция IEEE. IEEE, 2005.
Моя недавняя модель натяжения, которая доступна здесь в файле.jar, также выводит ключ (в дополнение к мерам натяжения) на основе спирального массива. Он может принимать файл musicXML или текстовый файл в качестве входных данных, который просто берет список имен шагов для каждого "временного окна" в вашей пьесе.
Herremans D., Chew E.. 2016. Натяжные ленты: Количественная оценка и визуализация тонального натяжения. Вторая международная конференция по технологиям музыкальной нотации и представления (TENOR). 2: 8-18.
Вы также можете сохранить структуру с ключами для каждого "поддерживаемого" масштаба, а в качестве значения - массив с аккордами, соответствующими этому масштабу.
Учитывая последовательность аккордов, вы можете начать с составления короткого списка клавиш в зависимости от вашей структуры.
С несколькими матчами вы можете попытаться сделать обоснованное предположение. Например, добавьте другой "вес" к любой шкале, соответствующей корневой ноте.
Если вы не против переключения языков, music21 (моя библиотека, отказ от ответственности) в Python сделает это:
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# major и Db major. В нем есть полный словарь названий аккордов, поэтому такие вещи, как "Dsus2", его не запутают. Единственное, что может укусить новичка, - это то, что квартиры пишутся со знаком минус, поэтому вместо "Eb / C" "E-/C".
Существует бесплатный онлайн-инструмент (MazMazika Songs Chord Analyzer), который очень быстро анализирует и определяет аккорды любой песни. Вы можете обработать песню, загрузив файл (MP3/WAV) или вставив ссылки YouTube/SoundCloud. После обработки файла вы можете воспроизвести песню, наблюдая за воспроизведением всех аккордов в режиме реального времени, а также таблицу, содержащую все аккорды, каждому аккорду назначается временная позиция и числовой идентификатор, который вы можете щелкнуть. перейти непосредственно к соответствующему аккорду и его временной позиции.