88鍵のキーボードで「奏でる」プログラミング

この記事は[ピクシブ株式会社 AdventCalendar 2017](https://qiita.com/advent-calendar/2017/pixiv)、18日目の記事です。

こんにちは、@fudafoota です。主に、pixivFANBOXというサービスの設計開発を担当しています。開発では主にPHP、JavaScript、TypeScriptを書いています。

今回の記事では、プログラミングに欠かせないキーボードについて話したいと思います。

# キーボードとは
現在コンピュータの入力機器としてのキーボードとは、タイプライターに搭載されていたハンマーの位置に活字を打ち付けるためのスイッチがならんだ盤状の部品に由来します。[^typewriter]

また、同じくスイッチが並んだ構造をする機械として、ピアノやオルガン、電子ピアノなどの鍵盤楽器も同様にキーボードの名前をもちます。

文字入力機器としてのキーボード(タイプライター)は18世紀前後の発明ですが、同じく**鍵を並べた楽器としてのキーボードははるかに長い歴史を持った機械**で、紀元前のギリシアでは初期のパイプオルガンが発明されていたと言われています。

現在ではPCの入力機器としてのキーボードと、楽器としてのキーボードはともに電子信号をコンピュータに入力することができます。

下では文字入力のキーボードと、電子ピアノを比較しています。

|                   | 一般的な鍵の数              | PCとの主要なインターフェイス  |
|-------------------|---------------------------|--------------------------|
| キーボード(入力機器) | 100程度 (ANSI 101/JIS 109) | USB HID                  |
| 電子ピアノ         | 最大88 (A0-C8)             | MIDI                     |

これらのことを踏まえると、電子ピアノでも、カーソルやテンキーなど一部のキーを省けば十分に文字入力用途に使えるように思えてきませんか。

もし電子ピアノで文字入力ができれば、日々のコーディングを無機質なタイプ音からメロディアスなピアノ演奏に変えることができるかもしれません。

それを実現する方法について、これから考えてみましょう。

## 仮想キーボードアプリ
今回はピアノのキー入力を入力機器に変換させる仮想キーボードアプリケーションを作成しました。

動作は下のようになります。

* PCに電子ピアノをUSB接続する(USB-MIDI機器としてコンピュータに認識される)
* 仮想キーボードアプリケーションがMIDI入力を読み、キーボード入力に変換
* 仮想キーボードアプリケーションが別のアプリケーションに対してキー入力を流す

### OS Xアプリケーションにキーイベントを送信する

キーボードの入力は、OSごとに異なる**仮想キーコード**として表現されます。これは非常に低レイヤーのAPIで、物理キーと1対1に対応する符号です。

プラットフォームごとのキーコードは下のMDNのドキュメントが詳しいです。

https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values

今回扱うMac OS X環境であれば仮想キーコードの対応マップはヘッダファイル

```
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
```

で定義されています。

注意点は、このキーコードは[物理的なキーボードのレイアウトをそのまま符号にしている](https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes)点です。基本的にUS配列(ANSI)のキーボードがベースになっています。JISなど別の配列では物理的なレイアウトに従ってそれぞれのキーをマッピングして考える必要があります。

このキーコードをアプリケーションに送信できれば、仮想的にキーボード入力をエミュレーションすることができるはずです。

キーコードの送信処理はOS Xのグラフィック処理に用いられるQuartzの機能として実現でき、Pythonでは[ラッパーのライブラリ](https://pypi.python.org/pypi/pyobjc-framework-Quartz)を使うことで比較的容易に実現できます。

次のメソッドはキーコードを送信する処理を実現しています。

```python
def send_key(self, key_code, is_press):
    event = Quartz.CGEventCreateKeyboardEvent(None, key_code, is_press)
    Quartz.CGEventSetFlags(event, Quartz.kCGEventFlagMaskShift if self.shift else 0)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
```

`Quartz.CGEventPost`は現在のアクティブなアプリケーションに対してイベントを送信することができるメソッドで、ここでは上で作られたキー入力イベントをアプリケーションに送信しています。

また、`shift`キーや`command`キーなど(装飾キー)を押しながらの入力はイベントに対してビットフラグを付与することで実現されており、さしあたり今回は`shift`のみを入力できるようにしました。

### キーマッピング
OS Xが規定するキーコードでは主要なキーは文字入力に使える主要なキーは0から60くらいの間に概ね収まっているので、これを電子ピアノの各キーにマッピングしてやりましょう。

MIDIでは各キーはそれぞれノート番号という連番が振られていて、1バイト、つまり128つの音(C-1〜G9)として符号化されます。この音域は一般的なグランドピアノの音域(A0〜C8)の音域をカバーしています。

そこで、今回のキーマッピングはMIDIノートを2オクターブ(24半音)分下げたものをMacの仮想キーコード入力として見なすようにマッピングしてやることにします。

上のルールで変換した場合、JISキーボードで送信されるキーと鍵盤のマッピング(一部抜粋)は下のようになります。

![keymap.png](https://qiita-image-store.s3.amazonaws.com/0/85777/ac238c99-58de-90ac-5efb-7787f26394aa.png)

しかし、これだけでは文字入力に問題があります。

現在のOSのキー設定だと仮想キーコード入力からはJISキーの物理配列に従ったキーが入力されますが、JIS配列キーボードはUS配列にないキーがあり、それらは仮想キーコードの定義だと

* `kVK_JIS_Yen` (0x5D = 93)
* `kVK_JIS_Underscore` (0x5E = 94)
* `kVK_JIS_KeypadComma` (0x5F = 95)
* `kVK_JIS_Eisu` (0x66 = 102)
* `kVK_JIS_Kana` (0x68 = 104)

と、いずれも鍵盤の外になってしまいます。特に`¥`キー、`_`キーはプログラミングでは必須のキーになっているため、`kVK_JIS_Yen`, `kVK_JIS_Underscore`, `kVK_JIS_KeypadComma`の3つは例外的に上図の左にできたA0, A#0, B0の3つにマッピングしました。[^kana]

次に、シフトキーを押しながらの入力をエミュレーションすることを考えてみましょう。プログラミングにおいては頻出のキーなので、簡単に切り替えができる入力が求められます。

ここでは、ピアノのサスティン・ペダル[^pedal]の入力をシフトキーとして認識させました。ペダルを踏んでいる間のキー入力はシフトキーが押された状態で認識されます。

# 電子ピアノでプログラミングしてみよう

さぁ、ここまで揃えば基本的な文字入力機能は電子ピアノで表現できるので、プログラミングに使ってみましょう。

また、ピアノの演奏でキーを入力するため、**キーストロークを楽譜として表現する**ことができます。

下の楽譜は今回のターミナル上でVimを起動し、PHPのコードを書いて保存し、実行するまでの手続きを今回のキー入力で表したものです。

![say_hello_to_php.png](https://qiita-image-store.s3.amazonaws.com/0/85777/ab1f173a-1a68-432d-aee2-a7ddf5379ae5.png)

初めの2小節で`vim hello.php`と入力してインサートモードに入ります。3〜7小節めでは激しくコーディングし、`:wq`で保存した後は曲の冒頭の旋律(`hello.php`)を繰り返し、`return`を押下することで

```
Hello, PHP!
```

と出力する、単純なものです。随所で繰り返される`php`(B3 E1 B3)の音型が印象的ですね。

早速演奏してみた、とできればよかったのですが、音の跳躍が激しく数回の練習ではこの曲を弾きこなすことはできませんでした。

そこで、仮想MIDIポートに対してMIDIシーケンサーから流すことで自動演奏を試み、右下のターミナルに入力されていく様子を動画にしました。[^video]

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これからPHPに入門する人のためのピアノ練習曲<a href="https://t.co/u8Ajb5IUMs">https://t.co/u8Ajb5IUMs</a></p>&mdash; #foota.fuda (@fudafoota) <a href="https://twitter.com/fudafoota/status/942023461299351552?ref_src=twsrc%5Etfw">2017年12月16日</a></blockquote>

期待通り演奏とともにプログラムが生成され、実行されました。

# まとめ
今回は電子楽器の電子ピアノでPCにキーボードイベントを送信する実験をしてみました。

作成したキーボードにはまず、下のような欠点があります。

* 送信されるコードがOS・キーレイアウトに完全に依存する
* キー入力にあたって身体を大きく動かす必要がある
* キーとの対応を覚えるのが人間には難しい
* ペダルを踏むタイミングが非常にシビア
* タイピング時に前衛的な楽曲ができてしまい作業能率が下がるおそれがある

キー配置を覚える作業は、慣れの問題といって問題ないかとは思うのですが、OS依存、レイアウト依存は汎用キーボードとして用いる上でかなり厳しいところがあるかと思います。

次はプラットフォーム依存を解消するため、USB HIDデバイスをエミュレーションして特定のキー配列のキーボードとしてコンピュータに認識されるキーボードに挑戦してみたいと思います。

----
[ピクシブ株式会社](https://www.pixiv.co.jp/)では、キーボードにこだわりのあるエンジニア[^keyboard]、楽器好きな仲間も募集中です。

明日は @matsurai25 によるLottieの話です。どんな素晴らしい動画作品が飛び出すのでしょう。乞うご期待です。

## おまけ
今回の仮想キーボードアプリケーションのコードを下に掲載します。動作環境は下の通りです。

* OS: Mac OS X (10.11.6)
* キーボード設定: JIS
* Python 3.6.3
    * pygame==1.9.3
    * pyobjc-framework-Quartz==4.0.1

`control` + `C` で終了するまでMIDI入力をモニタしてアクティブウィンドウにキーイベントを送信します。

お手持ちのピアノでコンピュータを操作する体験ができます。

```python
import Quartz
import pygame.midi
import asyncio

class MIDIListener():
    def __init__(self, channel = 1):
        pygame.init()
        pygame.midi.init()

        self.channel_index = channel - 1
        self.shift = False

        # はじめに認識されたMIDI INPUTデバイスを使用
        input_id = pygame.midi.get_default_input_id()

        if input_id < 0:
            print('No MIDI input selected :(')
            exit()

        self.midi_input = pygame.midi.Input(input_id)

    def __call__(self):
        'MIDIイベントをもとにキーイベントを発行'

        if self.midi_input.poll():
            events = self.midi_input.read(10) # 10イベント分読み込む
            for event in events:
                if event[0][0] == 0xb0 + self.channel_index and event[0][1] == 64:
                    # サスティンペダル
                    self.shift = event[0][2] > 63
                elif event[0][0] == 0x90 + self.channel_index and event[0][2] > 0:
                    note_num = event[0][1] - 24 # 2オクターブ分下にシフト
                    if note_num < 0:
                        note_num += 12 * 8 # JISキーボードの特殊キーをA0, A#0, B0に割り当てる
                    self.send_key(note_num, True)
                    self.send_key(note_num, False)

    def send_key(self, key_code, down):
        event = Quartz.CGEventCreateKeyboardEvent(None, key_code, down)
        Quartz.CGEventSetFlags(event, Quartz.kCGEventFlagMaskShift if self.shift else 0)
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)

@asyncio.coroutine
def periodic_call(interval, func):
    '特定の間隔おきにfuncを無限に実行する'

    while True:
        func()
        yield from asyncio.sleep(interval)

if __name__ == '__main__':
    midi_listener = MIDIListener(1)

    # 1msごとにMIDI入力をモニタリング
    task = asyncio.Task(periodic_call(0.01, midi_listener))
    loop = asyncio.get_event_loop()

    loop.run_until_complete(task)
```

[^typewriter]: ちなみに、[タイプライターが楽器として用いられる楽曲](https://www.youtube.com/watch?v=nW8dGwa2zRw)もあります
[^kana]: 入力言語切り替えは`cmd` + `space`で可能なので、かな/英数キーはいったん考えなくてよいと判断しました
[^pedal]: 踏んでいる間、ピアノの音が切れずにのびるペダル。ダンパーペダルともいいます。グランドピアノだと一番右側にあります
[^video]: 制御用のGUIアプリケーションが特定のアプリケーションに対してキーイベントを送信するように組んで撮影した後にコマンドラインツールに書き換えられることに気付いて書き直したため、動画にはGUI画面があります
[^keyboard]: Majestouch、REALFORCE、Happy Hacking Keyboard、Kinesis、ErgoDoxなど様々なキーボードのユーザがいます