Déterminer la clé d’une chanson par ses accords

Comment puis-je trouver par programme la clé d’une chanson simplement en connaissant la séquence d’accord de la chanson?
J’ai demandé à des gens comment ils allaient déterminer la clé d’une chanson et ils ont tous dit qu’ils le faisaient “à l’oreille” ou par “essais et erreurs” et en disant si un accord résout une chanson ou non … C’est probablement bien, mais en tant que programmeur, ce n’est pas vraiment la réponse que je cherchais.

J’ai donc commencé à chercher des bibliothèques liées à la musique pour voir si quelqu’un d’autre avait déjà écrit un algorithme pour cela. Mais bien que j’ai trouvé une très grande bibliothèque appelée “tonal” sur GitHub: https://danigb.github.io/tonal/api/index.html, je n’ai pas trouvé de méthode qui accepterait un tableau d’accords et renverrait la clé .

Ma langue de choix sera JavaScript (NodeJs), mais je ne cherche pas nécessairement une réponse JavaScript. Un pseudo-code ou une explication pouvant être traduite en code sans trop de problèmes serait parfaitement acceptable.

Comme certains d’entre vous l’ont mentionné correctement, la clé d’une chanson peut changer. Je ne suis pas sûr qu’un changement de clé puisse être détecté de manière suffisamment fiable. Donc, pour l’instant, disons simplement que je recherche un algorithme qui fait une bonne approximation de la clé d’une séquence d’accords donnée.

… Après avoir regardé dans le cercle des quintes, je pense avoir trouvé un modèle pour trouver tous les accords appartenant à chaque touche. J’ai écrit une fonction getChordsFromKey(key) pour cela. Et en vérifiant les accords d’une séquence d’accords contre chaque clé, je peux créer un tableau contenant les probabilités de la probabilité que la clé corresponde à la séquence d’accords donnée: calculateKeyProbabilities(chordSequence) . Et puis j’ai ajouté une autre fonction estimateKey(chordSequence) , qui prend les clés avec le score de probabilité le plus élevé et vérifie ensuite si le dernier accord de la séquence d’accord est l’une d’entre elles. Si tel est le cas, il retourne un tableau ne contenant que cet accord, sinon il renvoie un tableau de tous les accords ayant le plus grand score de probabilité. Cela fait un travail correct, mais il ne trouve toujours pas la bonne clé pour un grand nombre de chansons ou renvoie plusieurs clés avec une probabilité égale. Le problème principal A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G accords comme A5, Asus2, A+, A°, A7sus4, Am7b5, Aadd9, Adim, C/G etc., qui ne sont pas dans le cercle des quintes. Et le fait que, par exemple, la clé C contient exactement les mêmes accords que la clé Am , et G la même chose que Em et ainsi de suite …
Voici mon code:

 'use ssortingct' 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 { 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.ssortingngify(chordSequence)) console.log('Estimated key:',JSON.ssortingngify(key)) // Output: [ 'Em' ] 

Une approche serait de trouver toutes les notes jouées et de les comparer à la signature des différentes échelles et de voir quelle est la meilleure correspondance.

Normalement, une signature à l’échelle est unique. Une échelle mineure naturelle aura les mêmes notes qu’une échelle majeure (c’est le cas pour tous les modes), mais généralement, lorsque nous parlons d’échelle mineure, nous entendons l’échelle mineure harmonique, qui a une signature spécifique.

Donc, comparer les notes contenues dans les accords avec vos différentes échelles devrait vous donner une bonne estimation. Et vous pouvez affiner en ajoutant un peu de poids à différentes notes (par exemple celles qui apparaissent le plus souvent, ou les premier et dernier accords, la tonique de chaque accord, etc.)

Cela semble gérer la plupart des cas de base avec une certaine précision:

 'use ssortingct' 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: /^[AG]$|[AG](?=[#b])/ }, minor: { intervals: [3, 7], reg: /^[AG][#b]?[m]/ }, dom7: { intervals: [4, 7, 10], reg: /^[AG][#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(/^[AG][#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) }) 
   

Les accords d’une chanson d’une touche particulière sont principalement des membres de la gamme de la clé. J’imagine que vous pouvez obtenir une bonne approximation statistique (s’il y a suffisamment de données) en comparant les altérations prédominantes dans les accords énumérés aux signatures clés des clés.

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

Bien sûr, une chanson dans n’importe quelle touche peut / va avoir des altérations qui ne sont pas dans l’échelle des touches, ce serait donc une approximation statistique. Mais sur plusieurs mesures, si vous additionnez les altérations et filtrez toutes les données, sauf celles qui surviennent le plus souvent, vous pourrez peut-être faire correspondre une signature de clé.

Addendum: comme Jonas le souligne correctement, vous pourrez peut-être obtenir la signature, mais vous ne pourrez probablement pas déterminer s’il s’agit d’une clé majeure ou mineure.

Voici ce que j’ai imaginé. Encore une nouveauté avec le JS moderne, donc excuses pour le désordre et la mauvaise utilisation de map ().

J’ai regardé autour de l’intérieur de la bibliothèque tonale, elle a une fonction scales.detect (), mais ce n’était pas bon car elle nécessitait toutes les notes présentes. Au lieu de cela, je l’ai utilisé comme source d’inspiration et aplati la progression dans une simple liste de notes et j’ai vérifié cela dans toutes les transpositions en tant que sous-ensemble de toutes les échelles possibles.

 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 ssortingng of tonal notes eg 'cd eb' //onlyMajorMinor if true ressortingcts 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 ssortingng 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' ] ] 

Quelques inconvénients:
– Ne donne pas la note enharmonique que tu veux nécessairement. Dans p2, la réponse la plus correcte est C # major, mais cela pourrait être corrigé en vérifiant la progression initiale.
– ne traitera pas les «décorations» des accords qui sont hors de la clé, ce qui pourrait se produire dans les chansons pop, par exemple. CMaj7 FMaj7 GMaj7 au lieu de CF G. Je ne sais pas si c’est courant, pas trop, je pense.

Étant donné un tableau de tons comme celui-ci:

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

Nous pouvons tout d’abord générer un ensemble unique de tonalités:

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

Ensuite, nous pourrions vérifier l’apparence de # et bs:

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

Ensuite, faites la même chose avec bs et obtenez le résultat avec:

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

Cependant, vous ne savez toujours pas si sa majeure ou mineure , et beaucoup de composants ne se soucient pas des règles supérieures (et ont changé la clé entre les deux) …

Vous pourriez aussi garder une structure avec des clés pour chaque échelle “supscope”, avec comme valeur un tableau avec des accords correspondant à cette échelle.

Étant donné la progression des accords, vous pouvez commencer par créer une liste de touches basée sur votre structure.

Avec plusieurs correspondances, vous pouvez essayer de faire une estimation éclairée. Par exemple, ajoutez un autre “poids” à toute échelle correspondant à la note racine.

Vous pouvez utiliser le tableau en spirale, un modèle 3D pour la tonalité créé par Elaine Chew, qui dispose d’un algorithme de détection de clé.

Chuan, Ching-Hua et Elaine Chew. ” Recherche de clé audio polyphonique à l’aide de l’algorithme CEG à masortingce en spirale .” Multimedia and Expo, 2005. ICME 2005. IEEE International Conference on. IEEE, 2005.

Mon modèle de tension récent, disponible dans un fichier .jar, affiche également la clé (en plus des mesures de tension) basée sur la masortingce en spirale. Il peut soit prendre un fichier musicXML ou un fichier texte en entrée qui prend simplement une liste de noms de pitch pour chaque “fenêtre temporelle” de votre morceau.

Herremans D., Chew E .. 2016. Rubans de tension: quantification et visualisation de la tension tonale . Deuxième conférence internationale sur les technologies pour la notation et la représentation musicales (TENOR). 2: 8-18.