鍵盤にプロジェクターを投影して音ゲーにする

# 概要
キーボードの演奏を練習するための音ゲー風アプリケーションを作りました。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/cb6214a2-39a5-ab72-e89c-191827b473d1.png)

# 使用した機材

### シンセサイザー：Roland JUNO-DS61
音が鳴る61鍵盤のキーボードです。USB Type-Bの出力ポートからPCにつないでMIDI入力とサウンド入力を同時に取ることができます。

### プロジェクター：XGIMI HORIZON Pro
コンパクトさと発色の良さが特徴のプロジェクターです。デフォルト設定だと遅延が大きいですが、ゲームモードにすると遅延はほとんど感じませんでした。

### モニターアーム：エルゴトロン LX （＋ VESA変換プレート）
プロジェクターを上から床面に投影するために使いました。プロジェクター側のネジ穴間隔が微妙に揃っていなかったため完全には取り付けることができず、仕方なく対角線の二つだけ付けてます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/94b5ba22-4f99-1fa9-53de-b320872ae69d.jpeg" width=50% alt="プロジェクターにVESA変換プレートを取り付け"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/94e7e6e2-6b7e-7b9b-81ef-d851396bc96d.jpeg" width=50% alt="プロジェクターとモニターアーム">

デスクトップを投影するとこんな感じです。そのままだと黒鍵に投影した光が見えなかったため上から白いテープを貼りました。

![IMG_1315.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/e36f39e6-f8ab-6099-5532-42f77b1dd47b.jpeg)



# プログラムの実装
元々作っていたMIDIプレイヤーにキーボード入力機能を追加する形で実装しました。

https://github.com/agehama/SFZ_MIDI_Player

## MIDI入力

C++でリアルタイムのMIDI入出力を扱うことができるlibremidiを使いました。

https://github.com/jcelerier/libremidi

libremidiを使うには、CMakeを使って事前にスタティックリンクライブラリをビルドする方法と、マクロ指定でヘッダオンリーライブラリとして使う方法の二つがあります。以下にヘッダオンリーの方でOpenSiv3Dから動かす例を示します。

```cpp
#include <Siv3D.hpp>

#define LIBREMIDI_HEADER_ONLY 1
#define LIBREMIDI_WINMM 1
#include <libremidi/libremidi.hpp>

void Main()
{
	HashSet<uint8> pressedNotes;
	const auto callback = [&](const libremidi::message& message)
	{
		switch (static_cast<libremidi::message_type>(message.get_message_type()))
		{
		case libremidi::message_type::NOTE_OFF:
			pressedNotes.erase(static_cast<uint8>(message.bytes[1]));
			break;
		case libremidi::message_type::NOTE_ON:
			pressedNotes.emplace(static_cast<uint8>(message.bytes[1]));
			break;
		default:
			break;
		}
	};

	libremidi::observer observer;
	auto inputs = observer.get_input_ports();
	if (inputs.size() == 0)
	{
		Logger << U"No MIDI input devices found\n";
		return;
	}

	libremidi::midi_in midiin{ {.on_message = callback}, libremidi::midi_in_configuration_for(observer) };
	midiin.open_port(inputs[0]);
	if (!midiin.is_port_connected())
	{
		Logger << U"Failed to connect to MIDI input port\n";
		return;
	}

	while (System::Update())
	{
		ClearPrint();

		Print << U"押されているキー：";
		for (auto note : pressedNotes)
		{
			Print << note;
		}
	}

	midiin.close_port();
}
```
使い方は`libremidi::midi_in`クラスにMIDIメッセージを受け取るコールバック関数を登録して、`open_port`を呼び出して入力待ちに入ります。受け取ったメッセージには入力されたMIDIイベントが入っており、NOTE_ON / NOTE_OFF イベントの場合は `message.bytes[1]` にノート番号、`message.bytes[2]` にベロシティの値が入っています。またコールバック関数は別スレッドで走るため、実際に使うときは排他制御も多分必要です。

## サウンド入力
OpenSiv3Dサンプル集にある「マイク入力のリアルタイム書き込み」のコードをそのまま使いました。再生遅延の大きさは違和感なく聞こえる0.05秒（2200サンプル）に設定しました。

https://zenn.dev/link/comments/8af1a8f60deaa1

## 音ゲー部分の処理

音ゲーっぽくしたいので押した鍵盤が光る処理を入れました。また、キーの入力開始タイミングで4段階の判定を付けて、キーの上部に表示しています。

| 誤差(±秒) | 判定 |
|:-:|:-:|
| 0.1  | Perfect  |
| 0.2  | Good  |
| 0.4  | Bad |
| それ以上  | Miss  |

![draw.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/b4a8d2c8-361d-c6e6-cb2c-e15cf6e53382.gif)
もっと派手なエフェクトを付けたい

譜面が複数トラックに分かれていた場合は、そのうちの1つを演奏用として、他のトラックは自動演奏扱いにしています。

再利用性は低いですが、今回実装したコードはここに上げました。

https://github.com/agehama/SFZ_MIDI_Player/tree/dev/20231123_midi_input

# 動画

https://twitter.com/agehama_/status/1730926447064494205

楽譜は読めないけど好きな曲を弾いた気分になれるので楽しいです。手元を見ずに弾けるくらいになったらプロジェクターを外して画面見ながらでも良いかも。