javascript - progresiones - Determina la clave de una canción por sus acordes.
progresiones de acordes pdf (6)
¿Cómo puedo encontrar la clave de una canción mediante programación simplemente al conocer la secuencia de acordes de la canción?
Le pregunté a algunas personas cómo determinarían la clave de una canción y todos dijeron que lo hacían ''de oído'' o ''de prueba y error'' y al decir si un acorde resuelve una canción o no ... Para el músico promedio que Probablemente esté bien, pero como programador realmente no es la respuesta que estaba buscando.
Así que empecé a buscar bibliotecas relacionadas con la música para ver si alguien más ha escrito un algoritmo para eso todavía. Pero a pesar de que encontré una biblioteca realmente grande llamada ''tonal'' en GitHub: https://danigb.github.io/tonal/api/index.html no pude encontrar un método que aceptara un conjunto de acordes y devolviera la clave .
Mi idioma de elección será JavaScript (NodeJs), pero no necesariamente estoy buscando una respuesta de JavaScript. El pseudo código o una explicación que se pueda traducir en código sin demasiados problemas sería totalmente correcto.
Como algunos de ustedes mencionaron correctamente, la clave de una canción puede cambiar. No estoy seguro de si un cambio en la clave podría detectarse con la suficiente fiabilidad. Entonces, por ahora solo digamos, estoy buscando un algoritmo que haga una buena aproximación a la clave de una secuencia de acordes dada.
... Después de mirar el círculo de quintas, creo que encontré un patrón para encontrar todos los acordes que pertenecen a cada tecla. Escribí una función getChordsFromKey(key)
para eso. Y al comparar los acordes de una secuencia de acordes con cada clave, puedo crear una matriz que contenga probabilidades de que la clave coincida con la secuencia de acordes dada: calculateKeyProbabilities(chordSequence)
secuencia de acordes calculateKeyProbabilities(chordSequence)
. Y luego agregué otra función estimateKey(chordSequence)
, que toma las teclas con el puntaje de probabilidad más alto y luego verifica si el último acorde de la secuencia de acordes es una de ellas. Si ese es el caso, devuelve una matriz que contiene solo ese acorde, de lo contrario, devuelve una matriz de todos los acordes con el puntaje de probabilidad más alto. Esto hace un buen trabajo, pero aún no encuentra la clave correcta para muchas canciones o devuelve múltiples claves con la misma probabilidad. El problema principal son los acordes como A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G
etc. que no están en el círculo de quintas. Y el hecho de que, por ejemplo, la tecla C
contiene exactamente los mismos acordes que la tecla Am
, y G
lo mismo que Em
etc.
Aquí está mi código:
''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'' ]
Dada una serie de tonos como este:
var tones = ["G","Fis","D"];
En primer lugar podemos generar un conjunto único de tonos:
tones = [...new Set(tones)];
Entonces podríamos comprobar la aparición de # y bs:
var sharps = ["C","G","D","A","E","H","Fis"][["Fis","Cis","Gis","Dis","Ais","Eis"].filter(tone=>tones.includes(tone)).length];
Luego haz lo mismo con bs y obtén el resultado con:
var key = sharps === "C" ? bs:sharps;
Sin embargo, todavía no sabes si es mayor o menor , y muchos de los compositores no se preocupan por las reglas superiores (y cambiaron la clave entre ellas) ...
Esto es lo que se me ocurrió. Todavía nuevo con JS moderno, así que disculpas por el desorden y el mal uso de map ().
Miré alrededor de la parte interna de la biblioteca tonal, tiene una función escalas.detect (), pero no fue buena ya que requería cada nota presente. En lugar de eso, lo usé como inspiración, aplasté la progresión en una lista de notas simple y verifiqué esto en todas las transposiciones como un subconjunto de todas las escalas posibles.
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'' ] ]
Algunos inconvenientes:
- No da necesariamente la nota enarmónica que quieres. En p2, la respuesta más correcta es C # mayor, pero esto podría solucionarse comprobando de alguna manera con la progresión original.
- no tratará con ''decoraciones'' para acordes que están fuera de la clave, lo que podría ocurrir en canciones pop, por ejemplo. CMaj7 FMaj7 GMaj7 en lugar de CF G. No estoy seguro de cuán común es esto, no demasiado, creo.
Los acordes en una canción de una clave en particular son predominantemente miembros de la escala de la clave. Imagino que podría obtener una buena aproximación estadística (si hay datos suficientes) al comparar los accidentes predominantes en los acordes enumerados con las firmas clave de las teclas.
Ver https://en.wikipedia.org/wiki/Circle_of_fifths
Por supuesto, una canción en cualquier tecla puede / tendrá accidentes no en la escala de teclas, por lo que probablemente sería una aproximación estadística. Pero a lo largo de varios compases, si agrega los accidentes y filtra todos menos los que ocurren con mayor frecuencia, es posible que pueda coincidir con una firma de clave.
Anexo: como señala correctamente Jonas w, es posible que pueda obtener la firma, pero es probable que no pueda determinar si se trata de una clave mayor o menor.
Puede usar la matriz en espiral, un modelo 3D para la tonalidad creada por Elaine Chew, que tiene un algoritmo de detección clave.
Chuan, Ching-Hua y Elaine Chew. " Búsqueda de claves de audio polifónicas mediante el algoritmo CEG de matriz en espiral ". Multimedia y Expo, 2005. ICME 2005. IEEE International Conference on. IEEE, 2005.
Mi modelo de tensión reciente, que está disponible en un archivo .jar aquí , también genera la clave (además de las medidas de tensión) basada en la matriz en espiral. Puede tomar un archivo musicXML o un archivo de texto como entrada que solo toma una lista de nombres de tono para cada "ventana de tiempo" en su pieza.
Herremans D., Chew E .. 2016. Cintas de tensión: cuantificación y visualización de la tensión tonal . Segunda Conferencia Internacional sobre Tecnologías para la Notación y Representación de la Música (TENOR). 2: 8-18.
También puede mantener una estructura con claves para cada escala "compatible", con un valor como una matriz con acordes que coincidan con esa escala.
Dada una progresión de acordes, puede comenzar haciendo una lista corta de claves basadas en su estructura.
Con múltiples coincidencias puedes intentar hacer una suposición educada. Por ejemplo, agregue otro "peso" a cualquier escala que coincida con la nota raíz.
Un enfoque sería encontrar todas las notas que se tocan, y compararlas con la firma de diferentes escalas y ver cuál es la mejor coincidencia.
Normalmente una firma de escala es bastante única. Una escala menor natural tendrá las mismas notas que una escala mayor (esto es cierto para todos los modos), pero generalmente cuando decimos escala menor nos referimos a la escala menor armónica, que tiene una firma específica.
Entonces, comparar qué notas están en los acordes con tus diferentes escalas debería darte una buena estimación. Y podría refinar agregando algo de peso a diferentes notas (por ejemplo, las que más suben, o los acordes primero y último, el tónico de cada acorde, etc.)
Esto parece manejar la mayoría de los casos básicos con cierta precisión:
''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>
Para su ejemplo, da G mayor, eso es porque con una escala menor armónica, no hay acordes de D mayor o Bm.
Puedes probar los más fáciles: C, F, G o Eb, Fm, Gm
O algunos con accidentes: C, D7, G7 (este le dará 2 conjeturas, porque hay una ambigüedad real, sin dar más información, podrían ser ambas cosas)
Una con accidentes pero precisa: C, Dm, G, A