音楽の三大要素、メロディ・ハーモニー・リズムの内、ハーモニーの根幹を担っているのが和音です。
そのため、計算機を使った楽曲分析でも和音分析は非常に重要な処理です。
ただ、楽譜データ*1は簡単ですが、オーディオファイルのような楽曲データからの和音の推定はなかなか難しい処理です。
実際、音楽情報検索・音楽信号分析の分野でも長く研究されている分野であり、論文は見つかりますがエンジニア向けの簡単な資料・参考文献はそう多くないと思います。
そこでこの記事では、和音推定のベーシックな方法であるテンプレートマッチング法をPython実装とともに紹介します。
設計方針
和音推定アルゴリズムの処理の流れは、以下のようになります。
- 和音テンプレートを作る
- クロマグラム (各音名の強度)を算出
- 和音テンプレートと、クロマグラムのテンプレートマッチング
- マッチング結果(マッチングスコア)から、和音を推定
単にテンプレートマッチングだけでは和音は定まらず、連続量であるマッチングスコアから和音を推定する処理が必要となります。
1. 和音テンプレートの作成
クロマグラムとの内積を取る前提で、和音テンプレートの作成します。
和音テンプレート(行列)は、推定したい和音候補(e.g. [C, C#, ..., Cm, C#m, ...])に対して、和音の構成要素となる音名に重みを設定することで作成します。
シンプルな設定だと、図のようにバイナリな行列が使われます。さらに、和音の非構成音名には負の値を入れたり、各音名で値を変えたりする重み付けもよく利用されます。
なお、今回は三和音ですが、もちろんジャジーな響きの四和音や五和音用のテンプレートも作ることはできます*2。
template_major = np.array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
template_minor = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
templates = np.array( [np.roll(template_major, k) for k in range(0, 12)] \
+ [np.roll(template_minor, k) for k in range(0, 12)] )
2. クロマグラム (各音名の強度)を算出
定Q変換などを使って、クロマグラム(PCP: ピッチクラスプロファイル)を算出します。
詳細は、以下の記事をご参照ください。
www.wizard-notes.com
LibROSAを使うと、以下のように数行でクロマグラムを算出できます。
import librosa
audiofilepath = ...
n_bins = 84
hop_length = 512
y, sr = librosa.load(audiofilepath, sr=16000, mono=True)
pitch = np.abs(librosa.cqt(y=y, sr=sr, hop_length=hop_length, n_bins=n_bins))
chroma = librosa.feature.chroma_cqt(C=pitch)
3. 和音テンプレートと、クロマグラムのテンプレートマッチング
先ほど作った和音テンプレート templates
と、クロマグラム chroma
の内積を取ることで、
各和音らしさを示すマッチングスコアを計算します。
chord_matching_score = np.dot(templates, chroma)
4. マッチング結果(マッチングスコア)から、和音を推定
先ほどの和音マッチングスコア chord_matching_score
から、和音を推定します。
もっとも単純な方法だと、最も和音マッチングスコアが高い和音を和音推定値とする手法が考えられます。
chord_binary = np.zeros(chord_matching_score.shape)
for k in range(0, n_t):
idx = np.argmax(chord_matching_score[:,k])
chord_binary[idx,k] = 1.0
実装と楽曲への適用
以下のサイトの音源を使わせて頂きました。
パッヘルベル:カノン ニ長調: クラシック名曲サウンドライブラリー
正解データとしては、上記音源と(ほぼ)同じタイミングで和音構成音のサイン波を鳴らした音源を使っています。
クロマグラム
和音テンプレートマッチングスコア
和音推定結果
楽曲は、J.S.バッハ:メヌエット ト長調の前半部を使っています。
以下のサイトの音源を使わせて頂きました。
J.S.バッハ:メヌエット ト長調 BWV.Anh.114、ト短調 BWV.Anh.115: クラシック名曲サウンドライブラリー
正解データとしては、上記音源と(ほぼ)同じタイミングで和音構成音のサイン波を鳴らした音源を使っています。
クロマグラム
和音テンプレートマッチングスコア
和音推定結果
チューニングすべき箇所
- クロマグラム
- 調波打楽器音分離で得た調波成分からクロマグラムを抽出&
- 時間方向スムージング
- スパース化
- テンプレート
- 重みの設定方法
- {0, 1} or {-1, 0, 1}, or 実数値
- rootに対しての役割
- 和音の組み合わせ
- セブンスやテンションの扱い
- 組み合わせを増やすと推定誤りは増える
- コード推定
- コード進行に基づく制約
- あり得ないと考えられるコード進行になっていたら再推定する
- 調性に基づく制約
- 時間方向の連続性に基づく制約
- 和音推定値が非連続な場合は推定誤りとし、再推定する
- 出現頻度に基づく制約
- (その楽曲・音楽ジャンル・調性で)出現頻度の低い和音は推定誤りとし、再推定する
重みの設定は、音楽ジャンル・使用楽器によってチューニングが顕著に変わると思います。
それも踏まえて、楽曲の調性と音楽ジャンルに応じてチューニングするのがよいかと思います。
また、通常コードチェンジは頻繫に行われるものではないため、拍の頭など特定のタイミングでの推定値のみ採用するのも有効だと思います。
まとめ
和音推定のベーシックな方法であるテンプレートマッチング法を実装しました。
今回試した2つの楽曲では、どちらもなんとなく推定はできていますが、推定誤りが目立つ結果となりました。
特に2曲目はピアノの独奏であり、どうやら旋律に結果が引っ張られてしまっているようです。
なるべく滑らかで正しい和音推定値を得るために、事前学習をしたり、コード進行に仮定を設けた手法が提案されています。
今後、その手法を紹介をできたらいいなと思います
参考文献
FUJISHIMA, Takuya. Real-time chord recognition of musical sound: A system using common lisp music. Proc. ICMC, Oct. 1999, 1999, 464-467.
付録
和音テンプレート
ディミニッシュ(減三和音)の推定
減三和音のテンプレートマッチングもやってみたので、その結果を掲載します。
正解用データのほうは、ドミナント7のところ(D7)でディミニッシュも検出されています。
import numpy as np
import librosa
import librosa.display
import matplotlib.pyplot as plt
def chord_estimation_with_template_matching(
pitch,
pitch_ans=None,
smoother_coef=0.9,
dim_on=False,
debug_on=True,
):
note_labels = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
chord_labels = [note for note in note_labels] + [note+"m" for note in note_labels]
chord_labels += [note+"dim" for note in note_labels] if dim_on else []
template_major = np.array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
template_minor = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
template_dim = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
templates = np.array( [np.roll(template_major, k) for k in range(0, 12)] \
+ [np.roll(template_minor, k) for k in range(0, 12)] \
+ [np.roll(template_dim, k) for k in range(0, 12) if dim_on] )
n_chords = templates.shape[0]
for k in range(0, n_chords):
templates[k,:] /= np.sum(np.abs(templates[k,:]))
chroma_org = librosa.feature.chroma_cqt(C=pitch)
chroma = chroma_org.copy()
n_note, n_t = chroma.shape
for k in range(1, n_t):
chroma[:,k] = (1.0 - smoother_coef) * chroma[:,k] \
+ smoother_coef * chroma[:,k-1]
for k in range(1, n_t):
chroma[:,k] /= np.max(chroma[:,k]+1e-9)
for k in range(1, n_t):
ch = chroma[:,k]
thrd = np.sort(ch)[5]
ch[ch<thrd] = 0.0
chroma[:,k] = ch
chord = np.dot(templates, chroma)
chord_binary = np.zeros(chord.shape)
for k in range(0, n_t):
idx = np.argmax(chord[:,k])
chord_binary[idx,k] = 1.0
chord_ans = np.zeros(chord_binary.shape)
if pitch_ans is not None:
chroma_ans = librosa.feature.chroma_cqt(C=pitch_ans)
chord_ans = np.dot(templates, chroma_ans)
chord_ans /= np.max(chord_ans)
chord_ans[chord_ans < 0.8] = 0.0
for k in range(0, n_t):
if np.sum(chord_ans[:,k]) >= 3.0:
chord_ans[:,k] = 0.0
for k in range(1, n_t-1):
for n in range(0, n_chords):
if chord_ans[n,k+1] == 0.0 and chord_ans[n,k-1] == 0.0:
chord_ans[n,k] = 0.0
for n in range(0, n_chords):
if np.sum(chord_ans[n,:]) < 20:
chord_ans[n,:] = 0.0
chord_ans[chord_ans>0.5] = 1.0
if debug_on:
plt.clf()
plt.imshow(templates, aspect="auto", cmap="jet")
plt.xticks(np.arange(0, len(note_labels)), note_labels)
plt.yticks(np.arange(0, len(chord_labels)), chord_labels)
plt.colorbar()
plt.title("Chord templates")
plt.tight_layout()
plt.show()
plt.clf()
plt.subplot(2,1,1)
librosa.display.specshow(chroma_org, y_axis='chroma', x_axis='time', cmap="jet")
plt.colorbar()
plt.title("Chromagram")
plt.subplot(2,1,2)
librosa.display.specshow(chroma, y_axis='chroma', x_axis='time', cmap="jet")
plt.colorbar()
plt.title("Sparselize & smoothed chromagram")
plt.tight_layout()
plt.show()
plt.clf()
plt.imshow(chord, aspect="auto", cmap="jet")
plt.yticks(np.arange(0, len(chord_labels)), chord_labels)
plt.colorbar()
plt.title("Chord template matching")
plt.tight_layout()
plt.show()
plt.clf()
plt.subplot(2,1,1)
plt.imshow(chord_binary, aspect="auto", cmap="jet")
plt.yticks(np.arange(0, len(chord_labels)), chord_labels)
plt.colorbar()
plt.title("Chord estimation")
plt.subplot(2,1,2)
plt.imshow(chord_ans, aspect="auto", cmap="jet")
plt.yticks(np.arange(0, len(chord_labels)), chord_labels)
plt.colorbar()
plt.title("Correct chords")
plt.tight_layout()
plt.show()
return chord_binary, chord_labels
filepath = "audio/Canon.wav"
ansfilepath = "audio/Canon_chord.wav"
hop_length = 512
n_bins = 84
y, sr = librosa.load(filepath, sr=16000, mono=True)
y_harm, y_perc = librosa.effects.hpss(y)
y = y_harm
pitch = np.abs(librosa.cqt(y=y, sr=sr, hop_length=hop_length, n_bins=n_bins))
y, sr = librosa.load(ansfilepath, sr=16000, mono=True)
pitch_ans = np.abs(librosa.cqt(y=y, sr=sr, hop_length=hop_length, n_bins=n_bins))
chord_estimation_with_template_matching(pitch, pitch_ans=pitch_ans)