wav を入れたらカントリーブルース風の譜面が出てくる遊び道具を作った — mojo-hand

  • python
  • 音楽
  • ギター
  • 楽譜
  • LLM

日本ではあまり馴染みのない方もいらっしゃると思うので、最初に カントリーブルース と Travis picking について軽く触れさせてください。

Blind Lemon Jefferson, c.1926
Blind Lemon Jefferson (1893–1929) — Texas blues の原型と言われている人。これが唯一現存する写真とされています(Wikimedia Commons, public domain)

20 世紀初頭のアメリカ南部で育った、アコースティックギター 1 本で歌い語る音楽です。Travis picking は親指でベース、ほかの指でメロディを同時に弾く奏法で、これに ブルーススケール (1 ♭3 4 ♭5 5 ♭7) と 12-bar 進行 (I7 - IV7 - V7) が組み合わさると、いわゆる「ブルースっぽい音」になります。後のロックやフォーク、カントリーの土台にもなった音楽です。

詳しい音楽史は専門の本に譲ります。この記事の話は趣味の延長で「ブルース風の音と譜面が出てきたら楽しいかも」というレベルです。

まとめ(先に結論)

  • wav を渡すと、カントリーブルース風 Travis picking の譜面 (PDF) + ギター音色の wav を返してくれる小さなツール mojo-hand を作りました:https://github.com/logicia32/mojo-hand
  • パイプラインは「採譜 → 親指 alt bass + 指メロディに振り分け → ブルーススケールに寄せる → 楽譜と wav を出す」の素朴な構成です。--bluesness 0-100 のつまみで「ほぼ原曲」から「だいぶブルース寄り」まで連続調整できるようにしました
  • 設計のポイントは、アルゴリズム (DP) と LLM の役割分担を最初に決めたこと。LLM には「キーは何か」「フレーズの改行位置」「swing 比」みたいな音楽的な判断だけ任せて、譜面の音符そのものはアルゴリズム側で決めるようにしました。LLM 無しでも完結します(API キー未設定なら自動で algorithmic only モードに落ちます)
  • 正直、譜面の見た目が「楽譜らしく」なるまで何度も書き直しました。LilyPond の \relative で音符が空に飛んでいったり、量子化のせいで 32 分音符だらけになったり、bass と melody の押さえが物理的に届かない運指が出たり、最後の小節が紙からはみ出たり。出力を眺めては気づいて直す、を繰り返しながらひとつずつ解消していきました
  • これは仕事ではなく、ふと「童謡をブルースで弾いたら笑えるかも」と思って手を動かした遊びの実験です。クローンしてすぐ試せるように、PD(パブリックドメイン)の童謡 3 曲(ふるさと・さくらさくら・蛍の光)を repo に同梱してあります

何ができるのか

例えば「ふるさと」(高野辰之 作詞 / 岡野貞一 作曲、楽曲は保護期間経過済) の wav をコマンドラインに渡すと:

mojo cook examples/inputs/furusato.wav --bluesness 80
# -> furusato.pdf        (五線譜 + TAB の 2 段並列、コード記号 + 調号付き)
# -> furusato.blues.wav  (アコースティック・スチールの音色)
# -> furusato.events.json (中間ファイル、編集や検査用)

譜面はこんな感じになります(1 ページ目を貼ります):

furusato をブルース化した譜面

上段が treble_8 clef(ギター用の高音部記号、実音より 1 オクターブ高く書く譜表)、下段がタブ譜です。各小節の上に C7 / F7 / G7 が並びます。ブルースなので I も IV も V も全部 dominant 7th で書きました。

--bluesness の効果は段階的で:

効果
0 Travis picking フォーマットに整えるだけ
40+ メロディがブルーススケール (1 ♭3 4 ♭5 5 ♭7) に寄り始める
60+ swing 化(8 分音符が triplet feel に)
80+ ベースが 12-bar I-IV-V 進行に寄っていく
100 全部混ざる、mojo 全開

「mojo」は Lightnin' Hopkins / Muddy Waters の Got my mojo working 由来です。

全体のしくみ

5 段+オプションで LLM を 1 段、というシンプルな構成です。

%%{init: {'theme':'default', 'flowchart':{'nodeSpacing':50, 'rankSpacing':70, 'htmlLabels':true, 'curve':'basis'}, 'themeVariables':{'fontSize':'20px','fontFamily':'sans-serif'}}}%%
flowchart TB
  wav([🎵 wav 入力])
  transcribe["<b>transcribe</b><br/>採譜(librosa pyin)"]
  travis["<b>travis</b><br/>bass / melody 振り分け + alt bass"]
  bluesify["<b>bluesify</b><br/>scale snap / swing / 12-bar 進行"]
  quantize["<b>quantize</b><br/>8 分音符グリッドにスナップ"]
  events[("<b>events.json</b><br/>人間も LLM も読める中間ファイル")]
  llm["<b>LLM polish</b>(任意)<br/>キー / 改行 / swing 比"]
  notation["<b>notation</b><br/>自前 LilyPond → PDF"]
  synth["<b>synth</b><br/>pyfluidsynth + SF2"]
  pdf([📄 xxx.pdf<br/>五線譜 + TAB 2 段])
  wavout([🎶 xxx.blues.wav<br/>ギター音色])

  wav --> transcribe --> travis --> bluesify --> quantize --> events
  events -. "OPENROUTER_API_KEY があれば" .-> llm
  llm -. "編集判断だけを上書き" .-> events
  events --> notation --> pdf
  events --> synth --> wavout

  classDef io fill:#fff8e1,stroke:#f9a825,stroke-width:2px,color:#000;
  classDef store fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000;
  classDef opt fill:#fce4ec,stroke:#c2185b,stroke-width:2px,stroke-dasharray:6 3,color:#000;
  class wav,pdf,wavout io;
  class events store;
  class llm opt;

中央の events.json を 人間も LLM も読める中間ファイルとして置いてあるのがポイントで、ここを境に「採譜・整形」と「楽譜化・音源化」がきれいに分かれます。LLM 段は events を refine するだけで、音符そのものはアルゴリズム側に残してあります。

各段の役割は次の通り:

役割 使ったもの
採譜 wav → MIDI librosa の pyin(単声ピッチトラッキング)
Travis picking 化 bass と melody を分けて alt bass パターンを生成 numpy で素朴に
ブルース化 scale snap / swing / 12-bar 進行への寄せ bluesness パラメータで強度連動
運指 (string, fret) を移動コスト最小化で決定 動的計画法 (DP / Viterbi)
量子化 start/end を 8 分音符グリッドにスナップ 譜面の可読性のため
楽譜 LilyPond ソースを自前で生成 → PDF 化 music21 は経由していません(軽くしたかったので)
音源 MIDI → wav pyfluidsynth + FluidR3 SoundFont
(任意) LLM polish キー・改行位置・swing 比の編集判断 OpenRouter 経由で小さめのモデル

LLM をどう使ったか(一番興味があった部分)

楽譜を「機械的にいい感じ」にするのは、量子化と改行ロジックをどれだけ詰めても限界があります。例えば「自然なフレーズの切れ目はどこか」「この曲のキーは G major か A minor か」「swing っぽいか straight か」みたいな音楽的な判断は、ルールで書こうとすると脆くなりがちでした。

ここで LLM の出番にしたのですが、設計でひとつだけ決めたのは、LLM に譜面の音符そのものは触らせないことです。

具体的には:

  1. 中間ファイル events.json を必ず先に書き出す(小節を超えた raw 値、grid_beats、bpm 等)
  2. それを LLM に渡し、構造化 JSON で「phrase_breaks_beats」「swing_ratio」「key」「time_signature」だけ返してもらう
  3. アルゴリズム側でその値を使って LilyPond を組み立てる

LLM が壊れた JSON を返したり、ネットワークが落ちたり、API キー未設定だったりしても、すべて空 dict にフォールバックして algorithmic only で完結するようにしてあります。

def cook(...):
    ...
    refinement = _decide_refinement(events, bpm, use_llm)
    phrase_breaks = (
        refinement.phrase_breaks_beats
        if refinement.phrase_breaks_beats
        else default_phrase_breaks(events, every_beats=16.0)
    )
    ...

つまり LLM は「あったら助かる」存在であって、本体ではない、という関係に落ち着きました。アルゴリズムが整合性を保ち、LLM はその上で音楽的な判断だけ手伝う、という分担です。

OPENROUTER_API_KEY を環境変数に入れておけば自動で効きますし、明示的に切りたい場合は --no-llm でスキップできます。コストは 1 曲あたり数千分の 1 ドル程度(小さめのモデルを選んでいます)に収まっています。

ハマったところ(手探りした記録)

きれいにスッと出来た部分は実はあまりなくて、ほとんどがハマりとリカバリの繰り返しでした。代表的なものを正直に書いておきます。

1. basic-pitch が Python 3.12 で動かなかった

最初は Spotify の basic-pitch(多声採譜の有名どころ)で行く予定でした。が、TensorFlow が 3.12 で要求バージョン(< 2.15.1)が手に入らず、pip install で詰みました。

仕方なく 単声採譜(librosa の pyin)に Day 1 暫定で切り替えました。同梱の童謡サンプルが単旋律+ベース程度の構成なので、当面はこれで十分動きます。多声に戻すのは別タスクで…と先送り中です。

2. 楽譜のオタマジャクシが空に飛んでいった

LilyPond で五線譜+タブ譜の 2 段並列を作ろうとして、\new Voice \relative c' { ... } で絶対ピッチ(c'' 等)を流したら、毎音符のオクターブが累積的に上にずれて、五線譜から数オクターブ上に飛び去る現象が起きました。

これは LilyPond の \relative モードと、私の _pitch_to_lily の絶対ピッチ生成のミスマッチです。\relative を削除して absolute mode に切り替えたら直りました。

ついでに \time 4/4 宣言が抜けていて小節線が乱れていたのと、同時刻の bass + melody が直列に並んでいたのも、出力を眺めて気づいてまとめて修正しました。

3. 8 分音符より細かい音符でいっぱいになった

量子化なしで pyin の出力をそのまま LilyPond に渡すと、32(32 分音符)や 16.(付点 16 分)が大量に発生し、譜面が読めない密度になります。

これは「各音符の duration を独立に最近接 2 のべきへ丸めると、累積誤差で小節境界がどこにも揃わなくなる」という典型的な現象です。8 分音符グリッドにスナップする量子化を 1 段追加したらかなり改善しました。Travis picking の「息遣い」のような細かい揺れは丸まりますが、譜面として読めることを優先しました。

4. 物理的に届かない運指が出ていた

bass を E/A/D 弦、melody を G/B/e 弦に分けて、それぞれの voice 内で DP(Viterbi)を回して fret 移動コストを最小化していたのですが、2 つの DP が会話していないため、同じ瞬間に「親指 2 フレット・指 14 フレット」のような手の届かない配置が出ることがありました。

ポストプロセスで、同時刻の fret 差が 5 を超えたら、片方を別の弦に置き直して span を縮める処理を入れました。出力されたタブ譜をギターを手に取って確認していて「これは指が届かない」と気づいてから対応した部分です。

5. 最後の小節が紙からはみ出る

LilyPond の auto-line-break が量子化誤差の影響でうまく動かず、最後の小節が page width の右端からはみ出して切れる現象がありました。固定間隔(16 拍ごと)に \break を生成側で押し込むことで回避しています。本来は量子化誤差を根本的に消すべき箇所で、暫定対策です。

正直な制限

まだまだ粗いところがたくさんあります:

  • 採譜は単声。多声の wav はだいたいメロディだけ拾われます
  • メロディとベースの分離は単純な pitch threshold(G3)で簡易的に判定しています
  • 拍子は 4/4 固定です。3 拍子の曲は崩れます
  • キー検出は単純な頻度ヒューリスティック(LLM polish を経由すると多少改善)
  • 量子化が 8 分グリッドなので、細かい triplet 表情は丸まります
  • 運指は「物理的に届く」レベルで、上手なギタリストの選択とは違うかもしれません

試してみたい方へ

LilyPond と FluidSynth が要ります(Debian/Ubuntu の例):

sudo apt install lilypond fluidsynth libfluidsynth3 fluid-soundfont-gm

git clone https://github.com/logicia32/mojo-hand
cd mojo-hand
python3 -m venv .venv
. .venv/bin/activate
pip install -e .

mojo cook examples/inputs/furusato.wav --out-dir out/ --bluesness 80

OPENROUTER_API_KEY を設定しておくと LLM polish が自動で効きますが、無くても algorithmic only で完結します。

Inspired by

カントリーブルースの先達たちの音と佇まいから多くをもらいました。特にこの 3 名:

  • Blind Lemon Jefferson (1893–1929) — Texas blues の原型
  • Mississippi John Hurt (1893–1966) — gentle Travis picking の代表
  • Robert Johnson (1911–1938) — crossroads と mojo の伝説

repo の README に Blind Lemon Jefferson の Wikimedia Commons 公開写真(PD-US-no-notice)を 1 枚置いてあります。ささやかな敬意の表明として、リポジトリに置かせていただきました。


完成度より「ひとまず動いた」を優先したので、改善余地は山ほどあります。issue や PR はもちろん歓迎ですが、何より「お、ふるさとをブルースで弾いたら笑える」と思って試してもらえたら嬉しいです 🎸

mojo cook your-favorite-song.wav --bluesness 100

この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る