LoginSignup
5
1

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

Last updated at Posted at 2023-12-02

概要

キーボードの演奏を練習するための音ゲー風アプリケーションを作りました。
画像1.png

使用した機材

シンセサイザー:Roland JUNO-DS61

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

プロジェクター:XGIMI HORIZON Pro

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

モニターアーム:エルゴトロン LX (+ VESA変換プレート)

プロジェクターを上から床面に投影するために使いました。プロジェクター側のネジ穴間隔が微妙に揃っていなかったため完全には取り付けることができず、仕方なく対角線の二つだけ付けてます。

プロジェクターにVESA変換プレートを取り付けプロジェクターとモニターアーム

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

IMG_1315.jpeg

プログラムの実装

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

MIDI入力

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

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

#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サンプル)に設定しました。

音ゲー部分の処理

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

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

draw.gif
もっと派手なエフェクトを付けたい

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

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

動画

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1