wav を入れたらカントリーブルース風の譜面が出てくる遊び道具を作った — mojo-hand
- python
- 音楽
- ギター
- 楽譜
- LLM
日本ではあまり馴染みのない方もいらっしゃると思うので、最初に カントリーブルース と Travis picking について軽く触れさせてください。

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 ページ目を貼ります):

上段が 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 に譜面の音符そのものは触らせないことです。
具体的には:
- 中間ファイル
events.jsonを必ず先に書き出す(小節を超えた raw 値、grid_beats、bpm 等) - それを LLM に渡し、構造化 JSON で「phrase_breaks_beats」「swing_ratio」「key」「time_signature」だけ返してもらう
- アルゴリズム側でその値を使って 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 の元記事を見る