楽譜から手のサイズに合わせたピアノの指使いを物理シミュレーションで自動判定
目次
原題は I Taught a Browser to Play Piano — Here’s How It Figures Out Which Finger Goes Where で、タイトルだけ見るとブラウザでピアノを鳴らすおもちゃの話に見える。
中身は違う。
MusicXMLの楽譜を読み込み、手のサイズと指の動かしやすさを物理モデルで評価して最適な指使いを返す、という結構真面目な道具の作り話だった。
ピアノ楽譜には指使い(fingering)が振られていないことが多い。
振られていても、小柄な子どもの手と大人の手で同じ配置が最適になるわけがなく、「教則本の推奨指使いが自分の手には無理」はピアノ学習者が日常的に突き当たる問題だ。
教師がついていればその場で書き換えてくれるが、独学者は詰まる。
デモは audiotool.monkeymore.app/en/piano-finger からブラウザで直接触れる。
MusicXMLファイルをドロップすると数秒で指番号入りのアニメーションが始まる。
サーバーには何も送っていない。
何を解いているのか
ピアノの指使いは「各音符に1〜5の指番号を割り当てる組合せ最適化」として定式化できる。
評価軸は大きく3つある。
- 手の物理的制約: 隣接する指との最大スパン、親指で黒鍵を押すときのコスト、指のくぐり・またぎの可否
- 手のサイズ: 子どもの手と大人の手ではそもそも物理的に届く範囲が違う
- 指ごとの強さ: 薬指と小指は独立に動かしづらく、強拍で使うと崩れやすい
素朴にやるなら動的計画法で前から順に最小コストを積めば良さそうに見える。
実際には「今の音符に親指を当てるか」で数小節先の選択肢が崩れるので、局所最適を積んでも大域最適にはならない。
具体例を挙げると、Cメジャースケール上行で「ドレミ」まで1-2-3と振ったあと、「ファソラシド」で1-2-3-4-5と素直に並べると小指で高音を押さえた時点で手が詰まる。
正解は「ドレミ」まで1-2-3、「ファ」で親指をくぐらせて1に戻す。
局所的には「ミ」の次を親指にするのは手先の位置が遠くて不利に見えるが、数音先まで見渡すと親指くぐりのほうが低コストになる。
動的計画法で状態に「現在の指」だけを持たせると、こういう「数音先まで見ないと得にならない行動」を正しく評価できない。
状態に「手全体の位置」を持たせればDPでも扱えるが、状態空間が爆発する。
9音先読みのバックトラッキングは、このトレードオフの現実解として選ばれている。
物理シミュレーションで指使いを決めるアルゴリズム
このツールは「深さ制限バックトラッキング探索+9音スライディングウィンドウ」を採用している。
先9音ぶんを全列挙して一番コストが低い割り当てを採用し、ウィンドウを1音ぶんスライドさせる。
flowchart TD
A[MusicXML入力] --> B[INote列に正規化<br/>ピッチ・時間・cm座標]
B --> C[スライディングウィンドウ<br/>先9音ぶんを切り出し]
C --> D[指配置を全列挙<br/>5^9 候補]
D --> E[不可能な配置を事前除外<br/>プルーニング]
E --> F[コスト関数で評価<br/>平均指速度+補正項]
F --> G[最良の1音ぶんを確定]
G --> H{末尾まで?}
H -- No --> C
H -- Yes --> I[指番号列を出力]
コアの擬似コードはこうなる(元コードをJavaScriptのまま簡略化)。
const backtrack = (level) => {
if (level === depth) {
const velocity = aveVelocity(candidate, nseq);
if (velocity < minvel) {
bestFingering = [...candidate];
minvel = velocity;
}
return;
}
for (const finger of fingers) {
if (level > 0 && skip(candidate[level - 1], finger, ...)) continue;
candidate[level] = finger;
backtrack(level + 1);
}
};
プルーニング(skip 関数)で落とす典型例は以下のような配置。
- 同じ指で連続した異なる音符を弾く
- 不正な指交差(右手で3の左に4を置くなど)
- 物理的に届かないストレッチ(手のサイズを超える)
- 上行フレーズの途中で親指を黒鍵に置く(指くぐりが難しくなる)
プルーニングが効けば5^9 ≒ 195万通りの全列挙がミリ秒で終わる。
ウィンドウが9音という数字は絶妙で、5^7≒7.8万、5^9≒195万、5^11≒4880万と指数で伸びるので、「先読みを10音以上に伸ばしても実用的な恩恵が薄れる」位置にちょうど収まっている。
和音や短いフレーズの局所的な動きをカバーするには9音が下限の実用ラインという設計判断だ。
コスト関数の実態
記事のキーワードは「平均指速度」だが、実装を見るともう少し細かい。
核にあるのは各音の移動速度で、そこに指の強さと黒鍵の快適度を補正項として乗せる。
function aveVelocity(fingering, notes) {
let vmean = 0.0;
for (let i = 1; i < depth; i++) {
const dx = Math.abs(notes[i].x - fingerPos);
const dt = Math.abs(notes[i].time - notes[i - 1].time) + 0.1;
let v = dx / dt;
const weight = this.weights[fingering[i]] ?? 1.0;
if (notes[i].isBlack) {
v /= weight * this.bfactor[fingering[i]];
} else {
v /= weight;
}
vmean += v;
}
return vmean / (depth - 1);
}
補正テーブルの具体値は次の通り。
| 指 | 名前 | weights(強さ) | bfactor(黒鍵快適度) |
|---|---|---|---|
| 1 | 親指 | 1.1 | 0.3 |
| 2 | 人差指 | 1.0 | 1.0 |
| 3 | 中指 | 1.1 | 1.1 |
| 4 | 薬指 | 0.9 | 0.8 |
| 5 | 小指 | 0.8 | 0.7 |
v /= weight なので、値が大きい指ほど速度コストが下がる(=使いやすい)。
親指と中指が 1.1 で一番使いやすく、小指が 0.8 で一番使いにくい評価になっている。
経験的にも親指と中指は独立に速く動くので妥当な数字だ。
面白いのは bfactor のほうで、親指で黒鍵を押すときだけ値が 0.3 まで極端に下がる。
親指は他の指より短く、黒鍵は白鍵の奥にあるため、親指で黒鍵を押そうとすると手全体を前に出す必要があり姿勢が崩れる。
0.3 はこの不自然さを数値化したもので、実質的に「親指での黒鍵押しは3倍以上のコスト」という意味になる。
コスト関数の設計で注目したいのは、これらのパラメータが学習されたものではなく手作業でチューニングされた定数だという点だ。
ピアノ教師の経験則を数値化したルックアップテーブルで、RLのように訓練データから係数を推定するわけではない。
エンドユーザーが手のサイズを選ぶだけで動くシンプルさは、この学習フリーの設計が支えている。
同時発音(和音)はタイムスタンプが重なるので dt がゼロになる。
これを避けるために +0.1 の下駄を履かせ、さらに和音は50msの人為オフセットを足して時間軸上に並べ直している。
和音の指配置は、分解された単音列として評価される。
手のサイズという軸
既存の指使い自動判定ツール、特に PianoPlayer(Pythonで書かれた先行実装、今回のツールはこれのJavaScriptポート)でも手のサイズを考慮する概念はある。
このツールは Hand クラスにXXS〜XXLのプリセットを持たせ、スケール係数で次のパラメータを一斉に調整する。
this.frest = [null, -7.0, -2.8, 0.0, 2.8, 5.6]; // 指1〜5のリラックス時cm座標
this.weights = [null, 1.1, 1.0, 1.1, 0.9, 0.8];
this.bfactor = [null, 0.3, 1.0, 1.1, 0.8, 0.7];
this.hf = Hand.size_factor(size); // XXS〜XXLに応じた倍率
this.max_span_cm = 21.0 * this.hf; // 親指〜小指の最大スパン
this.max_follow_lag_cm = 2.5 * this.hf; // 目標位置からの許容ズレ
this.min_finger_gap_cm = 0.15 * this.hf; // 隣接指の最小間隔
frest はリラックス時の各指先のcm座標で、中指を原点(0.0)にして親指が左に7cm、小指が右に5.6cm離れている。
これが「何もしていない状態の手の形」の数値モデルになる。
Mサイズ基準で max_span_cm = 21.0 は親指と小指を最大限に開いたときの距離で、ピアノでオクターブ(約16.5cm)を余裕で押さえられるレンジだ。
手のサイズを変えると hf が変わり、上記パラメータが一斉にスケールする。
XXSなら hf ≒ 0.33 で最大スパン7cm(幼児の手)、XXLなら hf ≒ 1.2 で最大スパン25cm(ラフマニノフが片手で13度≒26cm届いたと言われるレンジに近い)。
XXSにすれば物理的に届かない配置はプルーニングで全部落ちる。
学習者にとって何を意味するかというと、自分の手のスパンを測ってXXS/S/Mのどれに当てはまるかを選ぶだけで、教則本とは違う現実的な配置が出てくる可能性があるということだ。
左手は座標をミラーリングすれば同じアルゴリズムがそのまま使えるので、右手用のロジックを書けば自動的に左手にも対応する。
学術的な系譜の中での位置づけ
ピアノ運指生成は音楽情報処理の中でも長い歴史がある研究テーマで、主な流派は3つ。
1. ルールベース+動的計画法(1990年代〜)
原点は Parncutt 1997(“An ergonomic model of keyboard fingering for melodic fragments”)で、12個のルール(指ペア間の最大スパン、指くぐり条件、黒鍵の扱いなど)をコストに写像して動的計画法で最小化する。
右手の単旋律限定、短いフレーズ対象という制約があるが、後続研究のベースラインになっている。
CouperinやBachの運指表記に見られる歴史的な慣習を数値化した枠組みだ。
2. メタヒューリスティクス(2010年代)
Herremans 2015 のタブーサーチや変数近傍探索は、ポリフォニー(多声部)対応を目指した流派。
DPでは状態空間が爆発する多声部楽譜に対して、局所探索で近似解を出す。
DPとは別方向で計算可能性を広げた系譜。
3. 強化学習(2020年代)
Ramoneda 2021(“Piano Fingering with Reinforcement Learning”)以降、運指をマルコフ決定過程として定式化してRLで解く研究が増えている。
状態は「現在の手の位置」、行動は「次の音にどの指を当てるか」、報酬は「指の移動量の逆数」など。
2023年のモデルベースRL論文 はQ-tableをハッシュテーブルに置き換え、prioritized sweepingを使っている。
最近はロボットハンドで実際にピアノを弾かせる研究(RP1Mデータセットなど)まで射程に入っている。
今回のツールの立ち位置
PianoPlayer(Python、2010年代後半から開発)をベースにJavaScriptへ移植したもので、系譜としては1の延長線上にある。
ただし動的計画法ではなく深さ制限バックトラッキングを採用することで、状態爆発を避けつつ局所最適のトラップも回避する折衷案になっている。
9音先読みという決め打ちは、Parncuttのmelodic fragments仮定とも、Herremansのpolyphonic全曲探索とも違う、実用向けの中間解だ。
学習を使わないシンプルさゆえにMusicXMLを食わせた瞬間に結果が出るし、ブラウザ完結も成立する。
RLベースは事前学習モデルが必要で、ブラウザにロードするにはモデルサイズが大きい。
古典的手法がWebツールに適した理由がここにある。
他楽器への応用可能性
同じ「音符列に物理的制約を考慮した指配置を割り当てる」枠組みは、ピアノ固有のものではない。
ギター
ギター運指の自動生成も古くから研究されていて、動的計画法が主流。
guitar_dp のような実装例もある。
状態は「弦番号+フレット番号+指番号+手の位置(人差指の位置)」のタプルで、ピアノより次元が高い(弦×フレットの2次元+指4本)。
コスト関数の設計はピアノと似ていて、移動距離+指の独立性+フレットジャンプのペナルティ。
Path Difference Learning のように実際のタブ譜から勾配降下でコスト関数の係数を学習する手法もある。
他の鍵盤楽器
オルガンは足ペダルが加わるので、両手+両足の4次元運指問題。
アコーディオンは左手のベースボタンと右手の鍵盤で物理モデルが違う。
いずれもピアノの枠組みを拡張すれば扱えるが、実装例は少ない。
弦楽器(ヴァイオリン等) ヴァイオリン運指の研究もあるが、弓使い(ボーイング)との連動が必要で、ピアノより複雑な多目的最適化になる。
つまり、このツールのアルゴリズム骨格(生体力学コスト+プルーニング+窓限定探索)は楽器依存ではなく、物理制約を差し替えれば他楽器に流用可能な汎用フレームワークだ。
ピアノ運指の解法として洗練されているというより、もっと広い「演奏動作の最適化」という枠組みの一サンプルと見たほうがいい。
ブラウザで完結する設計の意味
著者は「ブラウザ完結」のメリットを3つ挙げている。
- プライバシー: ファイルは絶対にサーバーに送信されない
- レイテンシーなし: ネットワーク往復がなく、即座にアニメーションが動く
- ゼロセットアップ: インストール不要、URLを開くだけ
ピアノ楽譜は商用教則本のスキャン・自分で打ち込んだ未発表曲・編曲作品など、著作権上グレーなデータを扱いたい場面が多い。
サーバーアップロード型のツールだと、法律以前にそもそも使いたくない。
このツールがブラウザ完結なのは、使いやすさのためというより対象ユーザーの性質に合わせた必然に近い。
技術構成は素直だ。
MusicXMLパーサー(musicxml_io.js)、React+Canvas 2D(DPR対応の高精度描画)、Web Audio APIの正弦波合成、requestAnimationFrame でのアニメーションループ。
PianoPlayerエンジンをJavaScriptにポートしたものをコアに据えている。
WebAssemblyもWebWorkerも使っていない。
つまり、JavaScriptの素の速度で9音ぶんの組合せ探索が1秒以内に終わるというベンチマーク的な結論でもある。
最適化アルゴリズムの重さに対してJavaScriptのコストモデルが妥当という確認が取れているのは、似たツールを作ろうとする人にとって地味に重要な情報だ。
誰に効くか、どこを見てから試すと良いか
このツールは次の層に効く。
- 独学者: 教則本の指使いが合わなくて詰まっている人
- ピアノ講師: 教え子の手のサイズに合わせた配置を素早く提示したい
- 編曲者: 自分で編曲したスコアに指使いを振るのが面倒な人
- 譜読みの段階で運指を決めたい人: 初見練習の前に指配置を可視化したい
試すときに見るべきポイントは3つある。
- 手のスパン選択が結果に変える幅: 同じ楽譜をXXSとXLで走らせて差分を眺める
- 和音の扱い: 同時発音は50msタイムオフセットで処理されるので、和音で違和感があれば設定を疑う
- 左手モード: 座標ミラーリングで実装されているので、左右非対称な配置が必要な曲では出力を比較する
一方で限界もある。
バックトラッキングはウィンドウ内の局所最適しか見ていないので、数十小節にまたがるフレーズ全体を見渡した「この曲ならこの指配置でまとめる」という大域判断はしない。
人間の上級ピアニストが譜読み段階でやっているフレージング込みの運指決定とは、違う種類の道具として捉えるのが正確だろう。
実際にバッハのインヴェンション1番(BWV 772)のMusicXMLをドロップして動かしてみた。
再生すると、押さえている指が鍵盤上で濃い色に変わり、その指先に押さえている音名(A4やC5など)が表示される。
指番号(1〜5)がそのまま出るわけではなく、どの指が動いているかを目で追って読み取る形式だった。
情報は出ているのに一目では頭に入らず、画面の雰囲気は昔ゲーセンにあったKEYBOARDMANIAを彷彿とさせた。
ここで思い出すのは、昔ゲーセンでギタフリやドラマニをやり込んでも実ギターや実ドラムが弾けるようにはならなかった経験だ。
エレクトーン経験者がKEYBOARDMANIAで手こずるという話もあって、音ゲーで譜面を処理する能力と実楽器の運指は別物らしい。
このツールの画面も近い位置にあって、アニメーションを目で追うことと、自分の指を動かして弾けるようになることは別の能力になる。
示されるのは「この譜面をこの手で弾くならこの運指が無理がない」という解析結果で、運指そのものを身体に叩き込むのは別途やるしかない。
ツールとして触って楽しいタイプではない。
ただ、9音ウィンドウのバックトラッキングがラップトップで引っかかりなく回っている確認が取れたのは、このアルゴリズムを自分で実装しようとする人には有益な情報だと思う。