AIで音ゲーを作ってみる(2): 簡易譜面の自動生成
前回に続き、今回はAIを用いて「楽曲に(ある程度)合った簡易譜面データの自動生成」を試してみたい。
本題とは関係ないが、前回からの差分として
- ハイスピード設定(ノートの流れる速さを0.1〜2.0倍で変更)
- オートモード
を追加しており、現在は以下のようなゲーム画面になっている。

計画、初期実装
wavファイルの内容を解析してそれっぽい譜面を作ってもらうのがゴールだが、音声解析はまったくやったことがないので、知識ゼロの状態からのチャレンジとなる。今回は生成譜面の品質をそこまで追い求めるつもりはなく、最低限音ゲーっぽくなっていればOKとする。
まずは Plan Mode で質問してみる。
これから、リズムゲームの譜面作成をAIによって自動化できないかを検証していきます。
今想像していることを書きます。
(1)機能面として必要なこと
- 音声ファイルを読み込ませ、分析する
- BPMの判定
- 音のタイミングの判定
- それらを譜面データに変換する
(2)ゲームをおもしろくするために必要なこと
- 同じ音を同じレーンに降らせる、メロディの高低をある程度反映し、演奏感を持たせる
- その楽曲、譜面を叩いていて気持ちいいものにする有機的な判断(高音やメロディの目立つ楽曲で演奏感を重視したいのか、低音やビート感が強い楽曲でドラムを叩くような体験を重視したいのか、あるいはパートごとにそれが切り替わるのか)
といった判断をAIにさせる必要があります。 (2)は難しい内容ですが、少なくとも(1) は実施したいです。 (1) と (2) を実現するためのアプローチを教えてください。
上記の(2)を実現しようとすると「音声解析ライブラリの解析結果をLLMに渡して思考させる」といった対応が必要になる気がする。既存のリズムゲームの譜面パターンの学習、とかまで考え出すと(夢はあるが)さすがにキリがなく、少なくとも素人が今すぐ手を出すことではなさそう。
そこで、いくつかの質疑応答を経て、まずは(1)のみを対応する前提で Python の librosa というオープンソースの音声解析ライブラリを用いることにした。私は Python を まともに触ったことすらない ので、(頑張ればある程度は読めるだろうが)とりあえずAIとの対話だけで頑張ってみることにする。
初期実装では以下に限定:
- BPM検出
- オンセット検出(周波数帯域別)
- 基本的な譜面生成(ChartData JSON出力)
将来の拡張(今回は対象外):
- 音源分離(Demucs/Spleeter)
- LLMによる有機的な配置判断
- 楽曲構造分析
アーキテクチャ
flowchart TB
subgraph input [入力]
AudioFile[音声ファイル .wav]
Options[オプション: 難易度等]
end
subgraph analysis [音響分析]
BPM[BPM検出]
Onset[オンセット検出]
Band[周波数帯域フィルタ]
end
subgraph mapping [レーン配置]
LowBand[低音域 → レーン0-1]
MidBand[中音域 → レーン2-5]
HighBand[高音域 → レーン6-7]
end
subgraph output [出力]
ChartJSON[ChartData JSON]
end
AudioFile --> BPM
AudioFile --> Band
Band --> Onset
Options --> mapping
BPM --> ChartJSON
Onset --> LowBand
Onset --> MidBand
Onset --> HighBand
LowBand --> ChartJSON
MidBand --> ChartJSON
HighBand --> ChartJSON
実装内容
1. BPM検出
librosa.beat.beat_track() を使用:
import librosa
y, sr = librosa.load('song.wav')
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
# tempo: 検出されたBPM(例: 128.0)
# beat_frames: ビート位置のフレーム番号
2. 周波数帯域別オンセット検出
バンドパスフィルタで周波数帯域を分離し、各帯域でオンセット検出:
from scipy.signal import butter, sosfilt
def bandpass_filter(y, sr, low_freq, high_freq):
sos = butter(4, [low_freq, high_freq], btype='band', fs=sr, output='sos')
return sosfilt(sos, y)
# 低音域(ベース・キック): 20-250Hz
y_low = bandpass_filter(y, sr, 20, 250)
onsets_low = librosa.onset.onset_detect(y=y_low, sr=sr, units='time')
# 中音域(スネア・メロディ): 250-2000Hz
y_mid = bandpass_filter(y, sr, 250, 2000)
onsets_mid = librosa.onset.onset_detect(y=y_mid, sr=sr, units='time')
# 高音域(ハイハット・シンバル): 2000-8000Hz
y_high = bandpass_filter(y, sr, 2000, 8000)
onsets_high = librosa.onset.onset_detect(y=y_high, sr=sr, units='time')
3. レーン配置ルール
周波数帯域 → レーンの固定マッピング:
- 低音域(20-250Hz) → レーン 0, 1
- 中音域(250-2000Hz) → レーン 2, 3, 4, 5
- 高音域(2000Hz以上) → レーン 6, 7
同じ帯域内での連続ノートは、交互にレーンを切り替えて配置(同じレーンに連続しすぎないように)。
4. ノート密度調整
オンセットをそのまま全て配置すると密度が高すぎる場合があるため、間引き処理を行う:
- 最小間隔(例: 100ms)より短い間隔のノートは間引く
- 難易度オプションで間引き率を調整
5. ChartData JSON出力
既存の ChartData 型に合わせた形式で出力:
{
"metadata": {
"title": "Generated Chart",
"artist": "Unknown",
"audioFile": "assets/audio/song.wav",
"bpm": 128,
"timeSignature": [4, 4],
"offset": 0
},
"notes": [
{ "lane": 0, "timing": 500 },
{ "lane": 3, "timing": 750 },
...
]
}
ファイル構成
scripts/
└── chart-generator/
├── main.py # CLIエントリーポイント
├── analyzer.py # BPM検出、オンセット検出
├── chart_builder.py # レーン配置、ChartData生成
└── requirements.txt # 依存パッケージ
依存パッケージ
librosa>=0.10.0
numpy>=1.24.0
scipy>=1.10.0
soundfile>=0.12.0
CLI使用例
# 基本的な使い方
python scripts/chart-generator/main.py input.wav -o output.json
# タイトル・アーティスト指定
python scripts/chart-generator/main.py input.wav -o output.json \
--title "My Song" --artist "Artist Name"
# 難易度調整(ノート密度)
python scripts/chart-generator/main.py input.wav -o output.json \
--density normal # easy / normal / hard
将来の拡張ポイント
今回の実装を土台として、以下の拡張が可能:
- 音源分離の追加: Demucsを導入し、stem別のより正確なオンセット検出
- LLM統合: 分析結果をLLMに渡して配置戦略を決定
- 楽曲構造分析: イントロ/Aメロ/サビを検出し、セクション別の配置変化
- ピッチ検出: メロディの高低をレーン位置に反映
Demucs や Spleeter といった、AIで楽曲をステムデータ(パートごとの音声)に分離するライブラリを提案されたが、そこまでやるとけっこう大変そうである。まずは精度よりも、シンプルに楽曲を丸ごと読み込んで譜面生成を試してみたいところ。
stem の分離はしないと難しいですか?
良い質問です。stem分離なしでも(1)の基本機能は十分に実現可能です。整理します。
stem分離なしでできること
| 機能 | stem分離なし | 精度 |
|---|---|---|
| BPM検出 | 問題なく可能 | 高 |
| オンセット検出(全体) | 可能 | 高 |
| 周波数帯域別のオンセット | 可能(フィルタリング) | 中 |
| ビート位置の特定 | 可能 | 高 |
stem分離がないと難しいこと
- 「ドラムだけ」「ベースだけ」の正確なタイミング取得
- 「同じ楽器の音を同じレーンに」という配置
- ボーカルメロディの正確なピッチ追跡
代替アプローチ: 周波数帯域フィルタリング
stem分離の代わりに、周波数帯域で音を分類する方法があります。
低音域(20-250Hz) → ベース・キック的 → レーン 0-1
中低音域(250-500Hz) → スネア・低めのメロディ → レーン 2-3
中音域(500-2000Hz) → ボーカル・メロディ → レーン 4-5
高音域(2000Hz以上) → ハイハット・シンバル → レーン 6-7
# バンドパスフィルタで周波数帯域ごとにオンセット検出
y_low = librosa.effects.split(y, top_db=30) # 低音域
onset_low = librosa.onset.onset_detect(y=y_low, sr=sr)
この方法のメリット・デメリット
メリット:
- 追加の依存(Demucs/Spleeter)が不要
- 処理が高速
- シンプルな実装
デメリット:
- 精度はstem分離より劣る(楽器が周波数帯域で完全に分離されるわけではない)
- 「同じ楽器」の判定は近似的
初期実装はあまり難しすぎない方がいいので、今回の実装ではスコープを「BPM検出、オンセット検出、基本的な譜面生成」に限定しましょう。
了解です。スコープを絞った初期実装のプランに更新します。
プランを初期実装のスコープに絞りました。
更新後のプラン概要
実装内容:
- BPM検出 - librosaで楽曲のテンポを自動検出
- 周波数帯域別オンセット検出 - バンドパスフィルタで低音/中音/高音を分離し、各帯域の音の立ち上がりを検出
- レーン配置 - 周波数帯域に応じてレーン0-7に固定マッピング
- ChartData JSON出力 - 既存のゲームで読み込める形式で出力
ファイル構成:
scripts/
└── chart-generator/
├── main.py # CLIエントリーポイント
├── analyzer.py # 音響分析
├── chart_builder.py # 譜面生成
└── requirements.txt # 依存パッケージ
使い方(完成後):
python scripts/chart-generator/main.py input.wav -o output.json --title "曲名"
なんとなくスクリプトのイメージが固まったところで、実装を依頼。
こればかりは挙動を見てみないことには良し悪しを判断しづらいので、まずは生成したスクリプトを使って適当に SunoAI で生成した楽曲の譜面生成を試してみる。
最初はまったく音楽に合っていない譜面が出てくることも覚悟していたが、案外それなりに楽曲を叩いている気分は味わえる程度のものが出てきた。ただし、同時押しがまったく出てこない点など、いくつか改良は必要そうだ。
現状の仕組みを確認する
まずはスクリプトの仕組みについて README.md を書かせて、大まかにやっていることを把握する。
生成ロジック
1. 音声分析(analyzer.py)
処理フロー
音声ファイル読み込み (librosa) │ ↓ BPM検出 (librosa.beat.beat_track) │ ↓ 周波数帯域別オンセット検出 ├─ low (20-250Hz) → ベース・キック ├─ mid (250-2000Hz) → スネア・メロディ └─ high (2000-8000Hz) → ハイハット・シンバル
周波数帯域
| 帯域 | 周波数範囲 | 検出対象 |
|---|---|---|
| low | 20-250Hz | ベース、キック |
| mid | 250-2000Hz | スネア、メロディ |
| high | 2000-8000Hz | ハイハット、シンバル |
各帯域に対してバンドパスフィルタ(Butterworth)を適用し、個別にオンセット検出を行う。
BPM検出
- 検出範囲: 90-200 BPM
- 範囲外の場合は倍/半分に自動調整(例: 60 BPM → 120 BPM)
2. 譜面生成(chart_builder.py)
レーン配置
周波数帯域を8レーンにマッピング:
低音(キック/ベース) │ 中音(メロディ/スネア) │ 高音(ハイハット) [0] [1] │ [2] [3] [4] [5] │ [6] [7]
| 帯域 | 配置レーン |
|---|---|
| low | 0, 1(左端) |
| mid | 2, 3, 4, 5(中央) |
| high | 6, 7(右端) |
同じ帯域内の連続ノートは交互にレーンを切り替えて配置する。
難易度と密度調整
| 難易度 | 音符間隔 | BPM=120時の最小間隔 |
|---|---|---|
| easy | 4分音符 | 500ms |
| normal | 8分音符 | 250ms |
| hard | 16分音符 | 125ms |
最小間隔の計算式:
最小間隔(ms) = 60000 / BPM / (note_division / 4)
間引きロジック
- 時間順にソートし、最小間隔未満のノートを除外
- レーンに関係なく時間差でフィルタするため、同時押しは発生しにくい
librosa には音声ファイルの onset(音の立ち上がりタイミング)を検知する機能があり、この onset を用いて譜面を生成しているようだ。

ざっくり言えば、楽曲中の一定より大きい onset を検出して
- そのタイミングをノートが落ちてくるタイミングとする
- 単に onset を取得するだけだと数が多すぎて過剰にノートが生成されてしまうため、BPMを基準としてタイミングが近すぎる onset の間引き処理を行う
- onset の周波数帯域に応じて、ノートが落ちてくるレーンを振り分ける
というロジックらしい。現状だと「同時押しになりうる配置」も間引かれてしまうため、常に単押しの譜面になってしまうようだ。
同時押しを含む譜面を生成できるようにする
譜面生成ロジックの改善を検討したいです。/scripts の README も読んでロジックを確認して欲しいんですが、現状だと同時押しが発生し得ない状態です。
同時押しを「適度」に発生させるため、取りうる方法を考えてください。単純に間引きロジックを変えただけだと、あらゆるタイミングで常に同時押しになってしまうことが想像されます。
以下は GPT-5.2 Codex の回答。
方法案(優先度高)
1) 帯域ごとの間引き → 近接クラスタで同時押し化
- 各帯域で別々に最小間隔を適用し、最後にマージする
- マージ時に
±X ms以内に集まったオンセットを「同時押し候補クラスタ」として扱う
→ 1~2レーンの複数ノートにする(上限は2〜3を推奨) - これで「異なる帯域が同時に鳴る時だけ同時押し」が起きる
(例: kick+snare, snare+hat)
利点: 同時押しが「音響的理由」に限定されるので過多になりにくい。
2) 強さベースの同時押し(ピーク強度で増やす)
- onset strength(検出強度)や RMS のピークが大きいタイミングだけ同時押し
- 例えば強度が p90 以上の時は “追加レーン” を 1 本だけ足す
→ 同じ帯域内で交互レーンとは別に、隣接レーンも使う
利点: 強い拍だけ厚くなるので、曲の抑揚と一致しやすい。
3) ビート上の限定同時押し(拍ごとの確率)
- 拍グリッドにスナップした上で
- 小節頭/拍頭にのみ同時押しを許可
- その中でも 確率 10〜20% 程度
- さらに “同時押しが連続しない” ルールを追加
- 例: 直前が同時押しなら次は必ず単押し
利点: 単調な連続同時押しを抑制できる。
補助的な抑制ルール(同時押し過多の防止)
A) 同時押しの最大数を上限で制限
- 1 タイミングに 2 ノートまで を基本
- hard のみ 3 まで許可、easy/normal は 2 まで
B) 同時押し密度のクールダウン
- 「同時押し発生後 N ms 以内は同時押し禁止」
(例: 250ms)
C) レーン偏り防止
- 同時押しは 左右対称 or 中央寄せ で出す
(視認性向上 & 連続同時押しでも押しやすい)
現状ロジックに対する最小変更の方針
- “全帯域まとめて間引き” をやめる
→ まず帯域ごとに間引く - その後、近接時刻クラスタを検出し、
“クラスタ内で1つだけ追加レーンを生成” する
→ これだけでも同時押しが「適度」に出る
具体的な調整パラメータ例(目安)
- 同時押し化ウィンドウ:
±30〜40ms - 同時押し確率:
10〜20%(beat head のみ) - 追加レーン数:
+1(2ノートまで) - クールダウン:
200〜300ms - 強度閾値:
p85〜p95
- 帯域ごとの間引き → 近接クラスタで同時押し化 をやってみようと思います。帯域は3つくらいに分割するということですか?
はい、帯域は今のまま 3 つ(low/mid/high)でOKです。
この方法は「帯域を増やす」話ではなく、**“帯域ごとに間引いてからマージし、近いタイミングをクラスタ化して同時押しにする”**という処理順の変更が主眼です。
現状の帯域分割とレーン割り当てはそのまま使えます。
必要ならあとで 4 帯域以上に細分化するのは別の改善軸です。
アプローチは悪くなさそうなので Codex に実装を試してもらい、譜面を再出力してみる。

見た目だけは beatmania の高難易度譜面のような雰囲気になったが、鳴っている音に対して過剰に同時押しが出すぎている。「low / mid / high 帯域がそれぞれ鳴っていれば同時押し判定」は条件として緩すぎるようだ(楽曲中でどれか1つの帯域しか出ていないことはあまりないだろう)。
改善案を検討させる。
同時押しが過剰に出すぎている問題を調整したいです。
- 静かなパートや音数が少ないパートでは単押しを保つ
- 派手な音や、大きな音をヒットするときに同時押しを出す
というメリハリをつけたいんですが、改善案ありますか? 音量など、別の判定軸を加えるのもありですか?
改善案あります。別の判定軸(音量・強度)はアリで、むしろメリハリを付けるには有効です。実装コストと効果のバランスで3案出します。
低コストで効く案
-
帯域ごとの“密度”で同時押し許可
一定時間窓(例: 1秒)内のオンセット数が閾値以上なら同時押し可、未満は単押し固定。
→ 静かなパートは自然に単押し化。 -
小節/拍頭だけ同時押し許可
BPMグリッドにスナップ後、拍頭/小節頭でのみ同時押しを許可+確率(10〜20%)。
→ 常時同時押しにならない。
音量・強度を使う案(おすすめ)
-
オンセット強度(onset strength)を使う
各オンセットに“強さ”を持たせ、強いピークだけ同時押し許可。- 強度が
p85以上 → 同時押し可 - それ以外 → 単押し
→ 派手な音だけ厚くなるので意図に近い。
- 強度が
-
RMSエネルギー(音量)を使う
オンセット近傍のRMSが高いときだけ同時押し許可。
→ “大きい音” を反映しやすい。
仕上げの抑制ルール(併用推奨)
- クールダウン: 同時押し発生後
200〜300msは同時押し禁止 - 最大数: easy=1, normal=2, hard=2〜3
- 確率: 強度条件を満たしても
10〜30%で間引く
質問
- 実装に進めるなら、どの軸を使うのがいいですか?
- 密度ベース
- onset強度
- RMS音量
- 併用(強度 + クールダウン)
方針が決まれば、analyzer.py に強度を出す処理を追加して chart_builder.py で同時押し判定に使う形にできます。
2 と 3 の併用はどうですか?
オンセット強度 + RMS音量の併用で、同時押しの許可条件を追加しました。
静かなパートは単押しになり、強いピークや大きい音量のときだけ同時押しが出るようにしています。
結果はこんな感じ。
楽しい譜面かどうかはともかく、わりとそれっぽくなっている気はする。NORMAL 難易度としてはちょっと難しすぎる気はするが、あとはパラメータの微調整である程度改善しそうだ。今回はクオリティを追い込むつもりはないので、基本的なロジックはこれで完成とする。
クォンタイズを行う
微妙に同時押しになっていない箇所がいくつかあるなど譜面のズレが気になったので、取得した BPM を用いてノートのクォンタイズを実施してみることにした。あらかじめ気になった懸念点を伝えながら指示していく。
音声データは0:00:00時点が一拍目の開始とは限らないですよね。
- BPM を取得する
- 取得した BPM グリッドと楽曲上の拍位置が合うように調整
- その BPM グリッドを基準に、1/16 でノート位置を補正する
という処理が必要だと想像しているのですが、どう思いますか?
その理解で合っています。BPMだけでは“位相(1拍目の位置)”が決まらないので、
グリッドに沿ってクォンタイズするなら 拍の開始位置(オフセット) を推定する工程が必要です。
おすすめの流れ
- BPM取得(今のまま)
- 拍位置の推定
librosa.beat.beat_trackで beat frames を取得- 最初の拍を「グリッドの起点(0拍目)」にする
- グリッド生成
- 取得した BPM と拍起点から、4分/8分/16分のグリッドを展開
- クォンタイズ
- 1/16グリッドに**±許容幅内だけ**吸着
特に「0:00:00が1拍目と限らない」点は重要で、
グリッドの位相合わせがないと “ズレたままの正確なクォンタイズ” になります。
もし進めるなら、
- beat positions を使って 位相を推定
- その位相基準で 許容幅付きクォンタイズ
を入れると自然に仕上がります。
必要なら、その実装を analyzer.py / chart_builder.py に組み込む案も出します。
たとえば「ボーカルソロから始まる曲」の場合、先頭数十秒でキックが鳴らないといったケースが普通にあると思います。このとき、beat frames も数十秒地点になるのですか? その場合、冒頭のボーカルソロ部分にクォンタイズは適用されるのですか?
結論から言うと:
beat_trackの結果は「拍が検出できるところ」から始まるので、
先頭がボーカルソロだけだと 最初の beat frame が数十秒後になることは普通にあります。
その場合どうなるか?
- そのまま使うと、冒頭は“位相が分からない”状態になります。
つまり、厳密な意味でのグリッド基準クォンタイズは 適用できない(またはズレたまま適用してしまう)ので、
冒頭はクォンタイズを弱く/しないのが自然です。
対処パターン(おすすめ順)
-
最初の beat から前後にグリッドを“外挿”する
- 最初の beat 時刻を起点に、BPM間隔で 過去方向にグリッドを展開
- これで冒頭にも位相を与えられる
- ただし BPMが正しいことが前提(誤差があるとズレる)
-
冒頭はクォンタイズを弱く/無効にする
- 例:最初の beat より前は “許容幅を狭く” する or “クォンタイズしない”
- ボーカルの自然な揺れを残せるメリットもある
-
beat_track 以外の“位相推定”も使う
- onset strengthの周期性や、セクション全体のスペクトル変化から位相推定
- 実装は少し重いが、冒頭の位相推定が改善される
要するに、**「beat が数十秒後に出る → 冒頭は位相不明」**が起きます。
この場合は
- 外挿してクォンタイズ
- or 冒頭はクォンタイズしない/弱める
のどちらかが実用的です。
もし実装するなら、
- 最初の beat 時刻を起点に過去へグリッド展開
- 前半はクォンタイズ許容幅を小さく
という“保険付き”のやり方が安定します。
前半はクォンタイズ許容幅を小さく、とかやり始めると複雑化しそうですね。ビート検知ができない冒頭区間についてはクォンタイズしない方針でいいと思います。
- beat_track が検知できたタイミングから、それ以降のノート位置がグリッドに吸着するようになる
であってますよね?
はい、その理解で合っています。
beat_trackが返す 最初の beat 時刻を起点にグリッドを作る- その時刻以降だけノートをグリッドに吸着
- それより前のオンセットは クォンタイズしない
この方針なら複雑化せず、冒頭のボーカルソロも自然に残せます。
実装まで進めますか?
まあよさそうだろう、というところで実装を開始。
完璧とはいえないが、ある程度違和感が解消されている。
おわりに
今回自動生成した譜面はゲームとしてハイスコアを狙いたくなるようなクオリティには達していないが、手動で改良する前提であれば「楽曲を増やしたらすぐになんとなくそれっぽく叩ける譜面を自動生成できる」だけでも開発スピードアップにある程度貢献できる気がする。
作業を終えてからいまさら知ったのだが、(AIが今ほど高性能ではない時代の記事だが)本格的なゲーム開発現場においてもそういった 活用事例 があったらしい。先に調べておけばよかった。
ここまででゲームシステムがある程度整ったので、次回は ダサすぎる view の改善 に着手していきたい。画面レイアウトのアイデア出しにAIを活用していく予定だ。