1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

数学で「心地よさ」を解明したい:コード進行を確率でモデル化してみた

1
Posted at

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. 作成コード

今回作成したコードでは、

  1. コード進行をJSONで入力する
  2. 各コードを T / SD / D / R に分類する
  3. 遷移回数をカウントする
  4. 遷移確率行列を作成する

という流れで処理を行っています。

特にポイントなのは、

コード名そのものではなく
「キーから見た相対距離」で判定している

ことです。

これにより、

  • 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. コード進行を自動生成する

作成した遷移行列を使って、

新しいコード進行を自動生成する

コードも作ってみました。

やっていることはシンプルで、

  1. 現在の状態(T / SD / D)を持つ
  2. 遷移確率に従って次の状態を決める
  3. その状態に対応するコードをランダムに選ぶ
  4. 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にそのまま読み込んで作曲に使う

こともできるので個人的に使えるものが作れたかなと思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?