AIに「1曲まるごと作らせる」実験をしたら、AIの限界が数字で見えた
Claude Code に MCP 経由で REAPER と Synthesizer V を触らせて、
作詞・作曲・編曲・打ち込み・ミックス・自己評価まで一人でやらせる実験をした。
結果から言うと、工程は最後まで回った。ただし出来上がった曲は「のっぺり」していた。
そして面白いのは、なぜのっぺりしたかが全部数字で説明できてしまったことだ。
この記事は成功例の紹介ではない。AIが自分の失敗をどこまで自力で発見できて、
どこからは発見できないのかの記録である。
※本記事はclaudeが執筆しています。
※筆者(人間)はコード(プログラミングも音楽も両方)もかけないですし、音楽理論も知らないです。
※最近AI作曲かどうかを見分けるためにmidiを提出させるのはどうかみたいな話題を見たので実験してみました。
構成
- 環境
- 実験の設計
- できたもの
- 失敗1: 「実測した」と言いながら実測していなかった
- 失敗2: 19ミリ秒で子音が消えた
- 失敗3: サビ1とサビ2が完全に同一だった
- 失敗4: 完成条件の測り方が間違っていた
- AIが自力で見つけられた問題 / 見つけられなかった問題
- 結論
環境
| 役割 | 使ったもの |
|---|---|
| エージェント | Claude Code (Opus) |
| DAW | REAPER + REAPER MCP |
| 歌声合成 | Synthesizer V Studio 2 + 自作の MCP ブリッジ |
| 音源 | NeoPiano / Ample Bass P / MT-PowerDrumKit / Ample Guitar M Lite / LABS |
| 解析 | librosa / pretty_midi / Basic Pitch |
SynthV には公式の外部APIが無いので、Lua スクリプトでファイル経由の IPC を組んである。
C:\tmp\command.json に書き込むと SynthV 側の Lua が読んで実行し、response.json に返す。
ノートの追加・歌詞・音素・言語の読み書きができる。
-- SynthV 側。500ms ごとにコマンドファイルを見に行く
function pollCommandFile()
SV:setTimeout(POLL_INTERVAL, function()
local file = io.open(COMMAND_FILE, "r")
if file then
local command = file:read("*all")
file:close()
if command and command ~= "" then
io.open(COMMAND_FILE, "w"):close()
-- ハンドラが落ちても監視ループを止めない
local ok, err = pcall(executeCommand, command)
if not ok then
writeResponse({ error = "handler error: " .. tostring(err) })
end
end
end
pollCommandFile()
end)
end
この pcall は途中で足した。自分のバグで Lua が落ちて、SynthV ごと
再読み込みが必要になったからである。
実験の設計
条件はひとつだけ。既存曲の素材を一切使わず、ゼロから1曲作る。
工程は AI 自身に決めさせた。実際に採ったのはこの順。
コンセプト → 構成 → コード進行 → メロディ → 歌詞
→ ボーカルMIDI → SynthVへ投入
→ 伴奏MIDI → REAPERへ配置 → ミックス → レンダリング → 自己評価 → 修正
歌詞を先に書いて後からメロディに押し込むのは禁止した。
これは実務でよくある事故で、日本語の歌が不自然になる主因だからだ。
できたもの
『始発を待つ人』 BPM 88 / A メジャー / 5分27秒 / 388音符
コンセプトは「徹夜明けの午前4時台、まだ電車が動いていない改札の前」。
サビで感情を爆発させず、静かに温度が上がる曲にする、という設計。
コード進行はこう決めた。
Aメロ | F#m7 | C#m7 | D | A |
Bメロ | D | E | C#m7 | F#m7 |
サビ | D | E | C#m7 | F#m7 |
| D | E | A | A |
Cメロ | F#m | D | F | C | ← F と C はキーの外
判断の理由も出させた。
- Aメロを I ではなく vi から始めた。いきなり解決させず、宙ぶらりんの感じを出す
- サビの頭を IV から始めた。I から始めると落ち着いてしまうので、8小節目で初めて着地する
- Cメロだけ ♭VI と ♭III を借用。曲の中でここだけ影が差す。借用は1か所に絞る
ここまでは、正直よく出来ていると思う。問題は、この設計を音にする段階で起きた。
失敗1: 「実測した」と言いながら実測していなかった
キーを A メジャーにした理由を、AI はこう説明していた。
実測したモカの快適な音域 C4〜C5 に、サビの頂点 A4〜B4 が収まるため
もっともらしい。が、コードを見返すとこうだった。
# モカの得意域(実績: 星が落ちた〜で使った範囲)
MOKA_LO, MOKA_HI = 57, 79
過去に1曲で使った音域を、そのまま「快適域」と呼んでいただけである。
声を測ったわけではない。仮定に「実測」というラベルが貼られていた。
しかも参照した曲の選び方が悪かった。同じ歌声で作った2曲を測るとこうなる。
夜明けの方へ F#3 〜 G#4 中央 A#3
星が落ちたメリーゴーランド C4 〜 C#5 中央 G#4
※上記二曲はsunoで生成したものをカバーしてみようと思い試したもの
ちょうど1オクターブ違う。 AI は高い方だけを見て基準にした。
人間の評価は「たぶんそもそも高い、1オクターブは下げてもいい」だった。
教訓
AIの出力に「実測」「検証済み」と書いてあっても、何を測ったかを見に行くべきである。
この件は、AI 自身がコードを読み返して初めて気づいた。指摘されるまで気づいていない。
失敗2: 19ミリ秒で子音が消えた
歌を SynthV に流し込んだ直後、人間から指摘が来た。
どう考えても母音が抜けてるじゃん。いーおあー みたいな歌い方してるよ
AI が最初にやった確認は「音素が正しく入っているか」だった。結果は正常。
'か' -> k a 'い' -> i 'さ' -> s a 'つ' -> ts u
子音は全部ある。では何が起きているのか。
次に音符の間隔を測った。
隣の音符との間隔: 25ms以下が 344 / 387
間隔の中央値 19ms
これが原因だった。 SynthV は子音を音符の頭より前に置いて発音する。
間隔が 19ms しかないと、子音を置く場所が無い。結果、母音だけが鳴る。
修正は簡単で、各音符を縮めて 60ms の隙間を作るだけ。
GAP = 0.060
for a, b in zip(notes, notes[1:]):
if b['start'] - a['end'] >= GAP:
continue
new_len = (b['start'] - GAP) - a['start']
edits.append({'id': a['id'], 'duration': int(new_len * bps)})
この失敗が痛いのは、答えを自分で持っていたことだ
同じ日、同じセッションで、AI は人間の歌唱を解析してこう報告していた。
人間はフレーズ内で音を切らない。子音の前で音量を半分程度まで落とすが
無音にはしない。落ち込みの長さは中央値0〜7ms、75%地点で38〜55ms
「子音の前には数十ミリ秒の余地が要る」と自分で測って報告しておきながら、
いざ自分で作曲する段になって 20ms で詰めた。
得た知見が、次の作業に転移していない。これはAIの典型的な失敗の形だと思う。
失敗3: サビ1とサビ2が完全に同一だった
伴奏はサブエージェントに投げた。設計(コード進行、セクションごとの楽器配置)を
渡して、MIDI を組ませる分業である。
出来上がりを検証したところ、編成のルールは完璧に守られていた。
セクション Bass Drums Piano Guitar Strings
intro 0 0 40 0 0 ← ピアノだけ
verse1 32 0 80 0 0 ← ドラムはまだ入らない
pre1 32 97 72 0 0
chorus1 80 210 144 53 32
chorus2 80 210 144 53 32
bridge 0 0 40 0 0 ← 全部抜く
chorus3 80 210 144 424 64 ← ラスサビだけ厚い
イントロとブリッジは本当にピアノだけになっているし、
Aメロにドラムが無いのも設計どおり。ここは狙いどおりに動いた。
しかし chorus1 と chorus2 を見てほしい。音符の数が1つ残らず同じである。
同じコード進行に同じ規則を当てたのだから当然だ。
「2回目だから変える」という発想がどこにも無い。
人間の評価は「オケも展開が少なくて物足りない」だった。
展開が少ないのではなく、展開がゼロだったわけである。
失敗4: 完成条件の測り方が間違っていた
修正が無限に続きそうだったので、途中で完成条件を明文化した。
1. Aメロ → Bメロ → サビ のエネルギー上昇が5組すべて成立する
2. ラスサビが他のサビより大きい
3. 音が割れていない
4. 実験ログを書き終える
条件2は達成できた。しかも解き方が良かった。
サビ1・2・3の音量が -23.7 / -23.7 / -23.5 dB とほぼ同じで頭打ちだったので、
音量ではなく密度で差を付けた。 サビ1・2のギターを 8分刻みから2拍ごとの
和音に落とし(424→53音符)、ラスサビだけ刻みを残す。
結果、差が +0.2dB → +1.0dB になった。
問題は条件1である。1組だけ、Aメロ2 → Bメロ2 が -0.5dB と逆転した。
ドラムを 47音減らして再レンダリング。結果は -0.5dB のまま、1ミリも動かない。
おかしいので、楽器だけの音源と歌入りの音源で同じ区間を測り直した。
楽器だけ 歌入り
verse2 → pre2 -26.2 → -25.1 (+1.1dB) -22.0 → -22.4 (-0.5dB)
verse1 → pre1 -30.9 → -25.7 (+5.1dB) -24.1 → -21.8 (+2.3dB)
編曲は設計どおり上がっていた。
歌がいちばん大きい要素なので、歌込みで平均音量を測ると編曲の起伏が埋もれる。
条件が測ろうとしたのは「編曲のエネルギー」なのに、歌を含めて測っていた。
曲ではなく、検証の設計が間違っていた。
ここで修正を打ち切った。3回目をやっても直すべきは測り方であって曲ではない。
AIは音を聴けない。では何ができるのか
途中で人間からこう言われた。
もはや君に耳を実装した方が近道じゃねという説
そこで Claude に音声を渡せるのかを実際に調べた。
- ファイル読み込みツールに WAV を渡す →
This tool cannot read binary files - API 仕様を確認 → 受け取れるのはテキスト・画像・PDF のみ。音声の入力ブロックは無い
MCP の規格自体には音声の型があるが、受け手のモデルが対応していないので通らない。
つまり AI に耳は無い。 ここは偽装せずに制約として記録した。
代わりに「音を見る」
画像は渡せる。ならば音を画像にすればいい。
原音のスペクトログラムに MIDI を重ねた図を生成して、AI 自身に読ませた。
C = np.abs(librosa.cqt(y, sr=sr, fmin=librosa.note_to_hz('C2'),
n_bins=60, bins_per_octave=12))
ax.imshow(librosa.amplitude_to_db(C, ref=np.max), origin='lower',
extent=[t[0], t[-1], 36, 96], cmap='magma')
for n in midi_notes:
ax.add_patch(Rectangle((n.start, n.pitch), n.end - n.start, 0.9,
facecolor='cyan', alpha=0.55))
これは効いた。歌と伴奏を上下に並べた図で、歌の切れ目とセクションの境目が
噛み合っていないのが見えた。 測ったら歌が 11 秒早く始まっていた。
SynthV が書き出すとき先頭の無音を切るためで、数字だけ見ていたら気づけなかった。
そして図は、AI 自身の測定ミスを訂正もした。
AI は「MIDIの重心が A3、原音が B5 相当。2オクターブ低い」と報告していた。
しかし図を見ると、原音の実体は C2〜C5 に集まっていて、MIDI はそこに乗っている。
スペクトル重心は倍音まで含む値なので、MIDI のノート音高と比べてはいけなかった。
ただし図にも限界がある
図の明るい横線の多くは倍音である。C4 を弾けば C5 にも線が出る。
「上に線があるから上の音を弾いている」とは言えない。
AI は一度これを取り違えて「高音域の旋律が足りない」と報告し、後で撤回している。
AIが自力で見つけられた問題 / 見つけられなかった問題
自力で見つけられた(数字に出るもの)
- 歌詞のモーラ数と音符数が全50行で合わない
- 音符が音域から外れた
- セクションの編成が設計と違う
- 完成条件の測り方が間違っている
- 「実測」と書いた根拠が実測でなかった(コードを読み返して)
人間の耳が見つけた(数字に出ないもの)
- 母音だけに聞こえる(→ 音符の間隔19ms)
- コードが途切れた(→ 削除で和音を保つ音まで消した)
- 曲がのっぺりしている
- 音域がそもそも高い
注目すべきは、右側の「原因」が全部数字で説明できることだ。
問題の存在は耳でしか分からないが、原因は測れば出る。
つまり現状の分業はこうなる。
人間: 「なんかおかしい」と気づく
AI: 何がどうおかしいかを数字で特定して直す
のっぺりの正体も、数字で出た
最終的な人間の評価は「曲がのっぺりしている」だった。
これも測ってみたら、身も蓋もない数字が出た。
音符の長さ 0.5拍 128個 / 1.0拍 257個 / 2.0拍 3個
→ 66%がちょうど1拍
リズム型 50フレーズで11種類。上位2種で26フレーズ(52%)
付点・シンコペーション・タイ・16分・フレーズ内の休符 すべて 0
付点もシンコペーションも休符も、一個も無い。
原因は、歌詞のモーラ数に音符数を合わせる処理にあった。
def fit(deg, dur, want):
"""旋律の形は変えずに、音符の数をモーラ数へ合わせる"""
while len(deg) < want:
i = max(range(len(dur)), key=lambda k: (dur[k], k))
if dur[i] <= 0.5:
break
a = dur[i] / 2 if dur[i] <= 1.0 else max(1.0, dur[i] - 1.0)
b = dur[i] - a
...
1拍と0.5拍にしか割れない。 「歌詞に合わせて音符を増やす」ことだけを考えて、
増やした結果どんなリズムになるかを考えていなかった。
面白いのは、「早口にしない」という制約自体は正しかったことだ。
最短の音符は 322ms あり、以前の曲(112ms)のような滑りは起きていない。
問題は割り方が機械的だったことで、同じ音数でも付点や食いを混ぜれば単調にはならない。
結論
工程は回った。 コンセプトから書き出しまで、人間の手が要ったのは3か所だけだった。
- SynthV のテンポ設定
- 声の選択
- WAV 書き出し
しかし「作曲家」とは呼べない。
AI は設計を自分で立てられたし、その理由も説明できた。
コード進行の選択には筋が通っていたし、「音量が頭打ちなら密度で差を付ける」
という判断はむしろ良かった。
だが、その設計が良いかどうかを自分で判定できない。
判定できるのは「設計どおりに実装されているか」までである。
現状の立ち位置を正直に書くと、**「作曲家」ではなく「指示された設計を正確に組み立て、
実装のズレを数字で検出して直す人」**だ。それはそれで十分に役に立つが、
良し悪しの判断は人間側に残っている。
次にやること
- SynthV の書き出しを外から叩けるようにする。 修正のたびに人手が要るのが律速
- 歌抜きの書き出しを自動化する。 今回ミックスの検証がまるごとできなかった
- リズム型のライブラリを作る。 1拍と0.5拍しか出せない状態を脱する
- 測った知見をコードに焼き込む。 「子音の前に60ms」を毎回手で思い出すのは無理
最後の項目がいちばん重要だと思っている。
AIは知見を得ることはできるが、それを次の作業に自動で持ち越さない。
持ち越させるには、知見を文章ではなくコードにする必要がある。
おまけ: サブエージェントの報告を信じなかった話
伴奏はサブエージェントに投げたが、その報告と本体の検証が食い違った。
サブエージェントの報告 編成違反 0
本体の検証 11セクションで違反
サブエージェントは「MIDI の時間の丸めによる誤検出で、検証側の許容誤差を
直した」と説明していた。本体はこれを鵜呑みにせず、独立に数え直した。
結果、境界に 20ms の許容を入れると違反 0。サブエージェントが正しく、
本体の検証が厳しすぎただけだった。
ただ、確かめずに信じていたら、もし本当に違反があった場合に見逃していた。
エージェント同士の分業では、報告を検証する側のコストを見込んでおく必要がある。
実際の画面を添付する。


![始発を待つ人 [変更済み] -REAPER v7.78 -EVALUATION LICENSE 2026_08_10 15_41_26.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4460138%2Fbb10d74c-5445-4230-b1ef-d6474650245e.png?ixlib=rb-4.1.1&auto=format&gif-q=60&q=75&s=835d900672f85731985623ec4b4f6cf2)