1. はじめに
この記事は、こちらのLT会
ゆるっと数学LT会
に登壇した際の発表を記事としてまとめたものです。
今年から新しい趣味としてDTMを始めようと思い、それと同時にLT登壇やアウトプットも継続していきたいと考えていました。
その結果、
「音楽 × 技術」
をテーマに、今年1年は音楽に絡めたLTをしていこうと決めました。
今回はその第一弾として、
「音楽 × 数学」
をテーマにしたプロダクトを作った記録です。
2. コード理論について
まずはプロダクトに触れる前に、軽くコード理論について説明します。
2.1 コードとは
音楽における「コード(Chord)」とは、
複数の音を同時に鳴らして作られる和音
のことです。
たとえば、
- ド・ミ・ソ → C(Cメジャー)
- ラ・ド・ミ → Am(Aマイナー)
のように、いくつかの音を重ねることで、それぞれ異なる響きや雰囲気を作ることができます。
2.2 コード進行とは
コードを時間順に並べたものを
コード進行(Chord Progression) と呼びます。
例:
C → G → Am → F
この並び方によって、
- 明るい
- 切ない
- 緊張感がある
- 安定している
など、曲の印象が大きく変化します。
つまり、
コード進行は「音楽の流れ」や「感情の動き」を作る重要な要素
と言えます。
2.3 ダイアトニックコード
多くの楽曲では、その曲のキー(調)に対応した
使われやすい基本コードのセット
があります。
これを
ダイアトニックコード
と呼びます。
たとえば Key=C(ハ長調)では、
C, Dm, Em, F, G, Am, Bdim
が代表的です。
これらのコードは自然につながりやすく、多くのポップスやJ-POPでも頻繁に使われます。
2.4 ファンクション(機能)
コードには単なる名前だけでなく、
音楽の中での役割
があります。
大きく分けると以下の3種類です。
| ファンクション | 役割 |
|---|---|
| T(トニック) | 安定・落ち着く |
| SD(サブドミナント) | 展開・動き出す |
| D(ドミナント) | 緊張・解決したくなる |
たとえば Key=C では、
- C, Am → T
- Dm, F → SD
- G, Bdim → D
として扱えます。
3. 今回やりたいこと
今回やりたいのは、
「どのコードから、次にどのコードへ進みやすいか」
を確率としてモデル化し、
最終的に
コード進行を自動生成すること
です。
つまり、
音楽の“文法”をプログラムで再現する
ことを目指します。
今回はこの「役割(ファンクション)」を
状態(State)として扱います。
- T:安定
- SD:展開
- D:緊張
- R:終了(休符)
こうすることで、音楽を
状態遷移
として捉えられるようになります。
4. 使用技術
今回のコード進行生成では、
マルコフ連鎖(Markov Chain)
という考え方を利用しています。
難しく聞こえますが、本質はとてもシンプルです。
一言で言うと、
「次の状態が、今の状態だけで決まる確率モデル」
です。
たとえば、
- 今日が晴れなら、明日も晴れやすい
- 今日が雨なら、明日も雨になりやすい
というように、
「未来は現在に依存する」
という考え方です。
これを数学的に扱うのがマルコフ連鎖です。
4.1 身近な例:天気
たとえば天気を
- 晴れ
- 曇り
- 雨
の3つの状態で考えるとします。
すると、
晴れ → 晴れ(70%)
晴れ → 雨(30%)
のように、
「今が晴れなら、次にどうなりやすいか」
を確率で表すことができます。
これがマルコフ連鎖です。
4.2 コード進行に置き換える
音楽でも同じことが起こります。
たとえば、
C → G → Am → F
のようなコード進行には、
「このコードの次には、これが来やすい」
という傾向があります。
つまり、コード進行も
状態が時間とともに変化するもの
として見ることができます。
4.3 今回の状態(State)
今回のモデルでは、
コードそのものではなく
ファンクション(役割)
を状態として扱います。
| 状態 | 意味 |
|---|---|
| T | トニック(安定) |
| SD | サブドミナント(展開) |
| D | ドミナント(緊張) |
| R | 休符・終了 |
つまり、
T → SD → D → T
のような流れをモデル化します。
4.4 遷移確率
たとえば、
T → T : 0.2
T → SD : 0.4
T → D : 0.3
T → R : 0.1
のように、
「Tの次に何が来やすいか」
を確率として表します。
これをすべての状態について集計したものが
遷移行列(Transition Matrix)
です。
4.5 遷移行列
行列で表すと次のようになります。
A =
\begin{pmatrix}
P(T→T) & P(T→SD) & P(T→D) & P(T→R) \\
P(SD→T) & P(SD→SD) & P(SD→D) & P(SD→R) \\
P(D→T) & P(D→SD) & P(D→D) & P(D→R) \\
P(R→T) & P(R→SD) & P(R→D) & P(R→R)
\end{pmatrix}
この行列を使うことで、
「次にどのコードへ進みやすいか」
を数学的に扱えるようになります。
4.6 なぜマルコフ連鎖を使うのか
理由はシンプルで、
コード進行には「よくある流れ」が存在する
からです。
たとえば、
T → SD → D → T
は非常によく使われます。
一方で、
D → D → D → D
のような進行はあまり自然ではありません。
この「自然さ」を
確率として表現できるのがマルコフ連鎖です。
4.7 LLMとの共通点
この考え方は
大規模言語モデル(LLM)
にも少し似ています。
LLMは文章を生成するとき、
「次に来る単語の確率」
を計算しています。
今回の研究では、
「次に来るコードの確率」
を計算しています。
つまり、
音楽版の“次の単語予測”
のようなものと言えます。
5. 作成コード
今回作成したコードでは、
- コード進行をJSONで入力する
- 各コードを T / SD / D / R に分類する
- 遷移回数をカウントする
- 遷移確率行列を作成する
という流れで処理を行っています。
特にポイントなのは、
コード名そのものではなく
「キーから見た相対距離」で判定している
ことです。
これにより、
- Key=C の G → D
- Key=G の G → T
のように、
移調しても同じロジックで扱えるようにしています。
5.1 ソースコード
import json
NOTE_TO_SEMITONE = {
"C": 0, "C#": 1, "Db": 1,
"D": 2, "D#": 3, "Eb": 3,
"E": 4, "F": 5, "F#": 6, "Gb": 6,
"G": 7, "G#": 8, "Ab": 8,
"A": 9, "A#": 10, "Bb": 10,
"B": 11
}
FUNCTION_MAP = {
0: "T",
1: "D",
2: "SD",
3: "T",
4: "T",
5: "SD",
6: "D",
7: "D",
8: "T",
9: "T",
10: "SD",
11: "D",
}
FUNCTION_IDX = {
"T": 0,
"SD": 1,
"D": 2,
"R": 3
}
def load_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def root_of(chord):
chord = chord.strip()
if chord.lower() in ["rest", "r", "-"]:
return "R"
if len(chord) >= 2 and chord[1] in ["#", "b"]:
return chord[:2]
return chord[0]
def chord_to_function(chord, key):
root = root_of(chord)
if root == "R":
return "R"
root_val = NOTE_TO_SEMITONE.get(root)
key_val = NOTE_TO_SEMITONE.get(key)
if root_val is None or key_val is None:
return "T"
interval = (root_val - key_val) % 12
return FUNCTION_MAP.get(interval, "T")
def decoder(chords, key):
return [chord_to_function(c, key) for c in chords]
def count_transition(functions, table):
functions = functions + ["R"]
for a, b in zip(functions, functions[1:]):
i = FUNCTION_IDX[a]
j = FUNCTION_IDX[b]
table[i][j] += 1
def normalize(table):
for i in range(4):
total = sum(table[i])
if total == 0:
continue
for j in range(4):
table[i][j] /= total
def process(json_data):
transition_table = [[0] * 4 for _ in range(4)]
if "songs" in json_data:
for song in json_data["songs"]:
key = song["key"]
chords = song["chords"]
functions = decoder(chords, key)
count_transition(functions, transition_table)
else:
key = json_data["key"]
chords = json_data["chords"]
functions = decoder(chords, key)
count_transition(functions, transition_table)
normalize(transition_table)
return transition_table
if __name__ == "__main__":
data = load_json("input.json")
result = process(data)
print("Transition Matrix (T, SD, D, R):")
for row in result:
print(row)
5.2 入力するJSON
入力するJSONは次のような形にしています。
コード進行をフレーズごとに区切って、複数曲まとめて入れられるようにしています。
{
"songs": [
{
"key": "C",
"chords": ["C", "G", "Am", "F"]
},
{
"key": "C",
"chords": ["F", "G", "C"]
}
]
}
6. 実データを入れてみる
今回はスピッツの実際の楽曲として、スピッツのチェリーを入れてみることにしました。(コード進行はこちらのサイトを参考にしてます:https://chordify.net/chords/supittsu-songs/cheri-chords)
実際にコード進行をJSONに入力し、遷移確率を計算すると次のような行列になりました。
A =
\begin{pmatrix}
0.23076923076923078 & 0.19230769230769232 & 0.38461538461538464 & 0.19230769230769232 \\
0.6666666666666666 & 0.0 & 0.25 & 0.08333333333333333 \\
0.46153846153846156 & 0.3076923076923077 & 0.0 & 0.23076923076923078 \\
0 & 0 & 0 & 0
\end{pmatrix}
これを見ると、
- T → D への遷移がかなり多い
- D → T に戻る流れも強い
- SD → T への解決も目立つ
など、
「この曲らしさ」
が確率として可視化されていることが分かります。
つまり、
なんとなく気持ち良い進行
を、数値として取り出せているわけです。
7. コード進行を自動生成する
作成した遷移行列を使って、
新しいコード進行を自動生成する
コードも作ってみました。
やっていることはシンプルで、
- 現在の状態(T / SD / D)を持つ
- 遷移確率に従って次の状態を決める
- その状態に対応するコードをランダムに選ぶ
- R(終了)になるまで繰り返す
という流れです。
7.1 自動生成コード
import random
import numpy as np
import wave
# =========================
# 状態
# =========================
states = ["T", "SD", "D", "R"]
transition_matrix = [
# ここにできた行列をコピペ
]
function_to_chord = {
"T": ["C", "Am", "Em"],
"SD": ["Dm", "F"],
"D": ["G", "Bdim"]
}
# =========================
# コード→音(MIDI番号)
# =========================
chord_notes = {
"C": [60, 64, 67],
"Am": [69, 72, 76],
"Em": [64, 67, 71],
"Dm": [62, 65, 69],
"F": [65, 69, 72],
"G": [67, 71, 74],
"Bdim": [71, 74, 77]
}
# =========================
# マルコフ生成
# =========================
def next_state(idx):
return random.choices(range(4), weights=transition_matrix[idx])[0]
def generate_progression(start="T", max_len=16):
progression = []
current = states.index(start)
for _ in range(max_len):
state = states[current]
if state == "R":
break
chord = random.choice(function_to_chord[state])
progression.append(chord)
current = next_state(current)
return progression
# =========================
# WAV生成
# =========================
def midi_to_freq(midi_note):
return 440.0 * (2 ** ((midi_note - 69) / 12))
def generate_wav(progression, filename="output.wav"):
fs = 44100
duration = 1.2 # 秒/コード
audio = np.array([], dtype=np.float32)
for chord in progression:
t = np.linspace(0, duration, int(fs * duration), False)
notes = chord_notes[chord]
wave_data = sum(np.sin(2*np.pi*midi_to_freq(n)*t) for n in notes)
wave_data /= len(notes)
audio = np.concatenate((audio, wave_data))
audio *= 32767 / np.max(np.abs(audio))
audio = audio.astype(np.int16)
with wave.open(filename, "w") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(fs)
f.writeframes(audio.tobytes())
# =========================
# MIDI生成(外部ライブラリ不要)
# =========================
def var_len(value):
buffer = value & 0x7F
bytes_ = []
while True:
value >>= 7
if value:
buffer <<= 8
buffer |= ((value & 0x7F) | 0x80)
else:
break
while True:
bytes_.append(buffer & 0xFF)
if buffer & 0x80:
buffer >>= 8
else:
break
return bytes_
def generate_midi(progression, filename="output.mid"):
header = bytearray([
0x4D, 0x54, 0x68, 0x64,
0x00, 0x00, 0x00, 0x06,
0x00, 0x00,
0x00, 0x01,
0x01, 0xE0
])
track = bytearray()
# tempo 120
track += bytes([0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20])
duration = 480
for chord in progression:
notes = chord_notes[chord]
# note on
for note in notes:
track += bytes([0x00, 0x90, note, 80])
# note off
delta = var_len(duration)
for i, note in enumerate(notes):
if i == 0:
track += bytes(delta)
else:
track += bytes([0x00])
track += bytes([0x80, note, 80])
track += bytes([0x00, 0xFF, 0x2F, 0x00])
track_header = bytearray([
0x4D, 0x54, 0x72, 0x6B
]) + len(track).to_bytes(4, "big")
with open(filename, "wb") as f:
f.write(header + track_header + track)
# =========================
# 実行
# =========================
if __name__ == "__main__":
prog = generate_progression()
print("Generated progression:")
print(prog)
generate_wav(prog, "generated.wav")
generate_midi(prog, "generated.mid")
print("WAV & MIDI files generated!")
7.2 実際に生成してみた
実際に生成してみると、
C → G → Am → F → G → C → Am → Dm
のような、
それっぽいコード進行
が得られます。
かなり機械的ではありますが、味のあるコードが作成できたと思います。
また、このコードはそのまま
- WAVファイル
- MIDIファイル
として出力できるようにもしてあります。
そのため、
DAWにそのまま読み込んで作曲に使う
こともできるので個人的に使えるものが作れたかなと思います。