ピアノアプリを二段鍵盤に対応させた話 — 同時押下の参照カウントとスクロール永続化
| 開発記録 | ピアノ
タグ: #Flutter #ピアノ #個人開発 #状態管理
「鍵盤を 2 段にしてほしい」。以前からいただいていた要望に、ピアノアプリ v5.5.0 で応えました。高さの等分割、同じ鍵盤を上下から同時に押せる問題の参照カウント解決、段ごとのスクロール位置永続化、切り替え時の音鳴りっぱなし対策。実装で踏んだ穴まで含めて記録します。
「鍵盤を2段にしてほしい」という要望
「鍵盤を上下 2 段にしてほしい」。ピアノアプリに以前からいただいていた要望のひとつです。
実際のピアノは 88 鍵が一列に並んでいます。でもスマホやタブレットの横幅では、一度に表示できる鍵盤数に限界があります。上下 2 段にすれば、画面に映る鍵盤が倍になります。両手で離れた音域を弾けるようになります。
先日リリースした v5.5.0 で、この二段鍵盤対応を入れました。実装で考えたことを記録しておきます。
レイアウト: 高さの等分割と divider の扱い
まず単純な問題です。鍵盤エリアの高さをどう分けるか。間に 4px の区切り線(divider)を入れて、残りを上下で等分する方式にしました。
final perRowHeight =
((effectiveHeight - _kDualRowDividerHeight) / 2)
.clamp(0.0, double.infinity);
最初の実装は、Column に 2 段目を足しただけでした。当然のように画面からはみ出しました(固定の鍵盤高さを前提にしたコードが残っていました)。LayoutBuilder で親の実制約を取って、動的に分割する形に直しています。
地味ですが、divider は各段のタッチ判定(Listener)の外側に置きます。そうすれば区切り線をタップしても、鍵盤押下と誤判定されません。
同じ鍵盤を上下から同時に押せる問題
上下の段には、それぞれ 88 鍵の全音域を割り当てました。つまり同じ「ド」が上の段にも下の段にもあります。
すると、こんな操作が起こり得ます。上段のドを押したまま、下段の同じドを押し、片方だけ離す。素朴に「離したら音を止める」と実装すると、まだ押されているのに音が消えます。
そこで鍵盤ごとに参照カウントを持たせました。
final Map<int, int> _keyPressCounts = {};
// 0 → 1 の遷移でのみ playKey
// 1 → 0 の遷移でのみ stopKey
カウントが 0 から 1 になった瞬間だけ発音する。1 から 0 に戻った瞬間だけ停止する。オーディオ層には一切手を入れていません。UI 層の押下管理だけで解決できました。
段ごとに独立したスクロール位置と、その永続化
上下の段は、それぞれ独立にスクロールできます。上段は中央のド付近、下段は低音域。そういう使い方を想定しているからです。
となると、アプリを再起動したときに両方の位置を覚えていてほしい。ただ、ドラッグ中に毎フレーム DB に書き込むと I/O が無駄に走ります。なので 500ms のデバウンスを入れました。最後の操作から 500ms 経ったら書き込む方式です。
ただしデバウンスだけだと、「操作直後にアプリを閉じた」ケースで保存が間に合いません。バックグラウンド遷移時と dispose 時に、未書き込み分を強制フラッシュしています。
リリース直後には「段を切り替えると下段のスクロール位置が上段の値で上書きされる」というバグも踏みました。上下の保存キーを分離して修正済みです(v5.5.0+57)。直したつもりで、すぐ次の穴に落ちていました。
段切り替え時の「鳴りっぱなし」対策
鍵盤を押したまま段数を切り替えると、押下中のポインタ情報が宙に浮きます。そして音が鳴りっぱなしになります。
これは切り替えイベントで全鍵盤を強制解放して対処しました。UI 側のリスナーと状態管理側の両方に解放処理を入れる、二重防御です。片方だけだと、Widget の破棄タイミングによってすり抜けるケースがありました。
まとめ
二段鍵盤は「鍵盤をもう一段描くだけ」に見えます。実際は違いました。同時押下の参照カウント、段ごとのスクロール永続化、切り替え時のクリーンアップ。状態管理の設計を問われる機能でした。
久しぶりに歯ごたえのあるアップデートでした。ぜひ 2 段モードで、連弾風の演奏を試してみてください。