MIDIスイッチャを作る:ピアノウィジェットを作る

# 1.はじめに

自作MIDIスイッチャを作り変えるプロジェクトですが、
MIDIフィルタリング&スイッチするときに、
キーボードのスプリットとかがやりたいです。
(マスターキーボードの上半分はMIDI音源1に下半分はMIDI音源2に、とか)

で、スプリットの指定をGUIでやるにあたって、
やっぱりピアノの鍵盤が表示されて、
ここからここまでね、ってカンジで指定したい!!

そうなると、ピアノウィジェットがいるな、というわけで作ってみました。
あ、実装は相変わらずpython3 + pygtkです。



# 2.ピアノの鍵盤を描画する

とりあえず、ピアノの鍵盤を描画しないといけないわけですが、
みなさん、ピアノの鍵盤がどういう図形か説明できますか？？

まずはわかりやすいように１オクターブで考えます。

1オクターブの中に含まれるのは
ドレミファソラシドの白鍵7つと黒鍵が5つです。
合計12個の鍵盤があります
ドから順番に鍵盤番号(ノートナンバー)を0〜11までふったときに、
各鍵盤のノートナンバーに対応する鍵盤の矩形はどうなるのでしょうか?

左上を始点(begin_x,begin_y),白鍵盤の幅をwkey, 高さをhkeyとした場合に
それぞれ値をもとめてみましょう。

![piano-1oct.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/50a6b08f-02db-6288-f4db-0bb3702485c2.png)




## 2.1.ピアノの鍵盤の幅を求める

まずは、wkeyとhkey,鍵盤の幅と高さですが、これはわりと簡単。

* 白鍵の幅(wkey): 鍵盤の全体の幅÷白鍵の個数(=7)
* 白鍵の高さ(hkey): 鍵盤の全体の高さ
* 黒鍵の幅: 白鍵の幅:wkey × 任意の倍率(0.6〜0.5ぐらい)
* 黒鍵の高さ: 白鍵の高さ:hkey × 任意の倍率(0.6〜0.5ぐらい)

とします。


## 2.2.鍵盤の始点を求める

で、左上の始点を求める場合は....
begin_yは白鍵盤、黒鍵盤ともに0でいいかなー
問題はX座標だけども、うーーん、
やっぱり黒鍵と白鍵で場合分けするしかないかな...

というわけで、黒鍵か鍵か判定できるよう、
次の様な配列を用意しておきます。

```
    NOTES_WHITE = [
      0,  # ド
      2,  # レ
      4,  # ミ
      5,  # ファ
      7,  # ソ
      9,  # ラ
      11, # シ
    ]
```

もし、`ノートナンバー in NOTES_WHITE`なら白鍵、そうじゃなければ黒鍵です。



### 2.2.1 白鍵盤のときの始点

白鍵盤の場合の`begin_x`は

* 白鍵盤の幅(wkey) × 白鍵盤の中の順番(開始は0番から)

で求められます。
pythonで書くと

```
    begin_x = wkey * NOTES_WHITE.index(ノートナンバー)
```

になります(鍵盤全体を７等分したときの、白鍵盤のうちの何番目かから求める)。
図で書くとこんなイメージです：

![notes-coord-white-x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/d165c9e6-82b5-42e3-1592-c5e1aec2cc5e.png)




### 2.2.2 黒鍵盤のときの始点

次に黒鍵の場合ですが、少なくとも黒鍵の場合、ノートナンバーを-1すると、
隣に白鍵(音が低い方)があるので、それを基準に座標が求められます。

* 隣の白鍵盤の終点 =  隣の白鍵盤の始点 + wkey
* 黒鍵盤のbegin_x　= 隣の白鍵盤の終点 - 黒鍵の幅 ÷ 2

コードで書くと

```
    #
    # 隣の白鍵盤の終点までをまずオフセット
    #　
    begin_x = wkey * NOTES_WHITE.index(ノートナンバー - 1) + wkey
    #
    # 黒鍵の幅だけ戻る
    #
    begin_x -= wkey * 0.6 / 2                                   
```

です。



# 3.オクターブ増やすと？？

オクターブが増えた時はどうなるかっていうと、

* オクターブ数 = ノートナンバー ÷ 12
* そのオクターブ中のノートナンバー(0〜11) = ノートナンバー mod 12

となるので、
黒鍵か白鍵かの判定は、`ノートナンバー % 12`の値を使って判定します。
あとは`begin_x`にオフセットとして`オクターブ数×7×wkey`を足せばいいだけです。

まとめると

```
    begin_x = int(7 * wkey * (ノートナンバー / 12)) # オクターブ分X座標をオフセット
    #
    # ここから下は１オクターブしかないときとだいたい同じ
    #
    if (ノートナンバー % 12) in NOTES_WHITE:
        begin_x += wkey * NOTES_WHITE.index(ノートナンバー%12)
    else:
        begin_x += wkey * NOTES_WHITE.index(ノートナンバー%12 - 1) + wkey
        begin_x -= wkey * 0.6 / 2
```


# 4.じゃぁ座標からノートナンバーを求めるには？？

じゃぁ逆にマウスで座標をクリックした所から
ノートナンバーを求めるにはどうしたらいいでしょうか？？
マウスがポイントした点を(x,y)として場合分けしてみます。

## 4.1.鍵盤が白鍵盤しかないと仮定してみると

まずは、鍵盤の下の方をたたいたと仮定、すなわち、
ピアノの鍵盤が白鍵盤しかないと仮定して、
仮のノートナンバーを求めます。

yの値は無視してxに注目すると

* x ÷ wkey = (マウスがポイントした点までの)白鍵盤の数
* octave = マウスがポイントした点までの白鍵盤の数 ÷ 7 
* (マウスがポイントした点の)オクターブ中のノートナンバー = NOTES_WHITE[白鍵盤の数 mod 7]
* ノートナンバー(仮) = octave × 12 + オクターブ中のノートナンバー

となって、鍵盤がすべて白鍵盤と仮定したときのノートナンバー(仮)が出ます。

![detect-key-white.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/a72d7881-1051-585a-0a97-64bca8a59207.png)



## 4.2.黒鍵押してるかどうかをを判定

次に、黒鍵がクリックされたか、白鍵盤がクリックされたか判定します。

すくなくとも黒鍵がクリックされた場合、
鍵盤の上の方がクリックされている必要があるので、
マウスの座標 y が 黒鍵の長さより短い場合は黒鍵をクリックしている可能性があります。
そうでない場合はノートナンバー(仮)がノートナンバーになります。

もし、鍵盤の上の方をクリックしている場合ノートナンバー(仮)に応じて処理が変わります

### 4.2.1.ドとファの場合

ノートナンバー(仮)より高い音側に黒鍵が存在する可能性があるので、
x がその範囲にあるかどうかを調べます。
高い音側が押されていたら、ノートナンバー(仮)を+1したものがノートナンバーになります。
そうでない場合はノートナンバー(仮)がノートナンバーになります。

![detect-key-black-DoFa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/f2bd9a6d-3d3b-cdaf-da65-f0181f8527d4.png)


### 4.2.2.ミとシの場合

ドとファの場合と逆に
ノートナンバー(仮)より低い音側に黒鍵が存在する可能性があるので、
同じく調べます。
低いい音側が押されていたら、ノートナンバー(仮)を-1したものがノートナンバーになります。
そうでない場合はノートナンバー(仮)がノートナンバーになります。

![detect-key-black-MiShi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/d3e44080-2984-bd2a-aad5-19e9b8b3243d.png)


### 4.2.3.それ以外の場合

高い音側と低い音側両方に黒鍵がある可能性があるので、
両方を調べます。ノートナンバーを決定します。

![detect-key-black-others.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/384620f4-b731-6650-2e3e-069fe7cc158b.png)


# 5.で、実装してみると

そんなわけで色々盛り込んで結局実装すると[こんなカンジ](https://github.com/KiyohitoSUZUKI/gmidisw/blob/master/midisw/gtk3/piano.py)になります。
諸般の事情でクッソ汚いソースですが,少しづつ理解を深めながら作っていったので、
最初の方に書いたコードとあとの方に書いたコードで
わかりやすさにかなりの差があります。

縦方向に配置できたりとか、
クリックした場所にマーカーつけたりとか、
ドラッグして範囲を求めるウィジェットも作ってみました。
実行例はこんなカンジです：
![piano-widget-execed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111803/25c6b5b5-2377-c064-fdb1-afd0d6996be8.png)



# 6.感想

鍵盤を書くロジックは比較的簡単に求められたのですが、
マウスの座標からノートナンバーを計算するのはちょっと考えました。

最初に考えた時は

* x ÷　鍵盤全体の幅 = 正規化した位置
* ノートナンバー = 正規化した位置 × 全体の鍵盤の個数

で求められるかなーと思ったら、微妙にずれていて、
やっぱドミシの隣は黒鍵が少ないの考慮しないとダメかー、となりました。

オブジェクト指向らしく、ボタンウィジェット使って計算をサボろうかなとも考えましたが、
押した場所に赤色のマーカーで色付けしようと思った場合に、色々と面倒くさい[^1]のでやめました。

[^1]: Gtk3だと色変えるのにわざわざCSSで指定しなきゃいけない(!!)わけです。Gtk2の頃はあんなに簡単だったのに.....。でも、そもそもウィジェットの色を変えるのは**テーマの一貫性がなくなる可能性がある**からやめといてね、という思惑もありそう。

ちなみに一ヶ月後にソース見たら、今回のロジック思い出せない自信はあります(笑)。
まぁ、そのためのカプセル化だからナ



よく考えたら、鍵盤を描いたり、クリックして音を出すって
大昔からある実装なわけで、
フリーのシーケンスソフトとかを参照すれば、
もっとスムーズに出来たかもしれません。

まぁ、車輪の再発明も脳のトレーニングということでひとつ....