web-dev-qa-db-ja.com

和音で曲のキーを決定する

曲のコードシーケンスを知るだけで、プログラムで曲のキーを見つけるにはどうすればよいですか?
私は何人かの人々に彼らが曲のキーをどのように決定するかを尋ねました、そして彼ら全員は彼らがそれを「耳によって」または「試行錯誤」によってそしてコードが曲を解決するかどうかを告げることによってそれを行うと言いました...おそらく大丈夫な平均的なミュージシャンのためですが、プログラマーとしては、私が探していた答えではありません。

それで、私は音楽関連のライブラリーを探し始め、他の誰かがそのためのアルゴリズムをまだ書いていないかどうかを確認しました。しかし、GitHubで 'tonal'という非常に大きなライブラリを見つけましたが、 https://danigb.github.io/tonal/api/index.html を受け入れるメソッドを見つけることができませんでしたコードの配列とキーを返します。

私が選択する言語はJavaScript(NodeJ)ですが、必ずしもJavaScriptの回答を探しているわけではありません。あまり問題なくコードに変換できる疑似コードまたは説明は、まったく問題ありません。

一部の人が正しく述べたように、曲のキーは変わる可能性があります。キーの変更を十分に確実に検出できるかどうかはわかりません。したがって、今のところは、特定のコードシーケンスのキーを適切に近似するアルゴリズムを探しています。

... 5分の1の円を調べた後、各キーに属するすべてのコードを見つけるパターンを見つけたと思います。そのための関数getChordsFromKey(key)を書きました。そして、すべてのキーに対してコードシーケンスのコードをチェックすることで、キーが特定のコードシーケンスと一致する可能性が高い確率を含む配列を作成できます:calculateKeyProbabilities(chordSequence)。次に、別の関数estimateKey(chordSequence)を追加しました。この関数は、確率スコアが最も高いキーを取得し、コードシーケンスの最後のコードがその1つであるかどうかを確認します。その場合、そのコードのみを含む配列を返します。それ以外の場合は、確率スコアが最も高いすべてのコードの配列を返します。これは問題ありませんが、それでも多くの曲の正しいキーが見つからないか、確率が等しい複数のキーが返されます。主な問題は、5度圏にない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' ]
40
Forivin

1つのアプローチは、演奏されているすべてのノートを見つけ、さまざまなスケールのシグネチャと比較して、どれが最適な一致であるかを確認することです。

通常、スケールの署名はかなりユニークです。自然なマイナースケールは、メジャースケールと同じノートを持ちます(すべてのモードに当てはまります)が、一般にマイナースケールとは、特定のシグネチャを持つ調和的なマイナースケールを意味します。

したがって、和音に含まれている音符をさまざまな音階と比較すると、適切な見積もりが得られます。そして、異なるノートに重みを加えることで洗練させることができます(たとえば、最も多く現れる音、または最初と最後のコード、各コードの強壮など)。

これはほとんどの基本的なケースをある程度の精度で処理するようです:

'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

6

特定のキーの歌のコードは、主にキーのスケールのメンバーです。リストされたコードの主な臨時記号を鍵の調号と比較することにより、統計的に(十分なデータがある場合)良い近似が得られると思います。

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

もちろん、どのキーの曲でも、キースケールではない臨時記号が存在する可能性があります。そのため、統計的な概算になります。しかし、いくつかの小節にわたって、臨時記号を合計して、最も頻繁に発生するもの以外をすべて除外すると、調号に一致する可能性があります。

補遺:Jonas wが正しく指摘しているように、署名を取得できる可能性がありますが、それがメジャーキーかマイナーキーかを判別できない可能性があります。

13
RichGoldMD

これが私が思いついたものです。現代の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#メジャーですが、元の進行状況を何らかの方法でチェックすることでこれを修正できます。
-ポップソングなどで発生する可能性のある、キーから外れたコードへの「装飾」は扱いません。 C F Gの代わりにCMaj7 FMaj7 GMaj7。これがどれほど一般的であるかはわかりませんが、多すぎるとは思いません。

11
Lucas

次のようなトーンの配列があるとします。

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;

ただし、そのmajorまたはminorであるかどうかはまだわかりません。多くの作曲家は、上位のルールを気にしません(そしてその間のキーを変更しました)...

8
Jonas Wilms

「サポートされている」すべてのスケールのキーを持つ構造を保持することもできます。値として、そのスケールに一致するコードの配列が含まれます。

コード進行が与えられたら、構造に基づいてキーの候補リストを作成することから始めます。

複数の一致を使用すると、知識に基づいた推測を試みることができます。たとえば、ルートノートに一致するスケールに他の「ウェイト」を追加します。

7

キー検出アルゴリズムを備えたElaine Chewが作成した色調の3Dモデルである螺旋配列を使用できます。

Chuan、Ching-Hua、Elaine Chew。 「 スパイラルアレイCEGアルゴリズムを使用したポリフォニックオーディオキーの検索 。」 Multimedia and Expo、2005。ICME2005。IEEEInternational Conference on。 IEEE、2005年

.jarファイル(ここ )にある最近の張力モデルも、スパイラル配列に基づいて(張力測定に加えて)キーを出力します。楽曲の「タイムウィンドウ」ごとにピッチ名のリストを取得するだけのmusicXMLファイルまたはテキストファイルを入力として使用できます。

Herremans D.、Chew E .. 2016。 Tension ribbons:Quantizing and Visualizing Tonal Tension 。楽譜の表記と表現のための技術に関する第2回国際会議(TENOR)。 2:8-18

6
dorien

言語の切り替えに反対していない場合は、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」のようなものはそれを混乱させません。新規参入者を噛む可能性がある唯一のことは、フラットがマイナス記号で書かれているため、「Eb/C」ではなく「E-/C」であることです。