Last active
July 31, 2022 11:05
-
-
Save trueroad/9c5317af5f212b2de7c7012e76b9e66b to your computer and use it in GitHub Desktop.
WinRT MIDI Transfer with C++/WinRT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// WinRT MIDI Transfer with C++/WinRT | |
// https://gist.github.com/trueroad/9c5317af5f212b2de7c7012e76b9e66b | |
// | |
// Copyright (C) 2022 Masamichi Hosoda. | |
// All rights reserved. | |
// | |
// Redistribution and use in source and binary forms, with or without | |
// modification, are permitted provided that the following conditions | |
// are met: | |
// | |
// * Redistributions of source code must retain the above copyright notice, | |
// this list of conditions and the following disclaimer. | |
// | |
// * Redistributions in binary form must reproduce the above copyright notice, | |
// this list of conditions and the following disclaimer in the documentation | |
// and/or other materials provided with the distribution. | |
// | |
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
// ARE DISCLAIMED. | |
// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |
// SUCH DAMAGE. | |
// | |
// C++/WinRT で MIDI メッセージを転送する試み | |
// | |
// 普通のコンソールアプリで UWP MIDI の API を叩いて MIDI を扱う。 | |
// 従来は WinMM や MME などと呼ばれる API で MIDI を取り扱っていたが、 | |
// これはかなり古くから存在する API であり、 | |
// 一部を除きマルチクライアントに対応していない | |
// (カスタムドライバでなければ同じポートを複数アプリで開くことができない) | |
// Bluetooth MIDI (BLE MIDI) に対応していない、などの問題がある。 | |
// 一方、新しく実装された UWP MIDI の API は | |
// (UWP MIDI で使っている限り)マルチクライアント対応だし、 | |
// Bluetooth MIDI (BLE MIDI) にも対応している。 | |
// UWP MIDI API は UWP でなければ使えないと思っていたのだが、 | |
// 新しく導入された C++/WinRT を使えば、従来のデスクトップアプリでも | |
// コンソールアプリでも、普通の C++ プログラムから使うことができる | |
// ことがわかった。 | |
// そこで、実験的に MIDI IN から入ってきた MIDI メッセージを | |
// MIDI OUT へ転送しメッセージの内容を表示するコンソールアプリを作ってみる。 | |
// その際、従来のコンソールアプリとの違いを明らかにするため、 | |
// Visual Studio で C++/WinRT 向けのプロジェクトを作るのではなく、 | |
// わざと従来のコンソールアプリ向けのプロジェクトを作り、 | |
// 明示的に設定変更することで C++/WinRT を使えるようにする。 | |
// コンパイラ環境 | |
// | |
// C++/WinRT は Visual Studio 2017 の途中のバージョン以降もしくは | |
// Visual Studio 2019 以降で使うことができるとされている。 | |
// そこで、今回は Visual Studio 2019 を使用することにする。 | |
// Visual Studio 2019 の機能としては「C++ によるデスクトップ開発」 | |
// 「ユニバーサル Windows プラットフォーム開発」を | |
// インストールしておく必要がある。また、今回は使わないが拡張機能で | |
// 「C++/WinRT templates and visualizer for VS2019」 | |
// をインストールしておけば C++/WinRT 向けプロジェクトが作れるようになるので | |
// 通常の C++/WinRT 開発をするんは便利だろう。 | |
// プロジェクト設定 | |
// | |
// 今回は Visual Studio 2019 Community を使う。 | |
// 「新しいプロジェクトの作成」を選び、選択肢の中から | |
// | |
// コンソールアプリ | |
// Windows ターミナルでコードを実行します。規定では"Hello World"を出力します。 | |
// [C++] [Windows] [コンソール] | |
// | |
// となっているものを選ぶ。(わざと UWP や C++/WinRT を選ばない) | |
// プロジェクト名は winrt_midi_transfer にしておく。 | |
// | |
// プロジェクトが開かれたら、メニューから | |
// 「プロジェクト」→「winrt_midi_transfer のプロパティ」 | |
// を選んで「winrt_midi_transfer プロパティページ」ダイアログを出す。 | |
// | |
// 左上の「構成」で「すべての構成」を選択、 | |
// その右にある「プラットフォーム」で「すべてのプラットフォーム」を選択する。 | |
// | |
// 左側「構成プロパティ」のツリーで | |
// 「C/C++」→「言語」をクリックする。 | |
// 右側で | |
// 「準拠モード」を「はい (/permissive-)」から「いいえ (/permissive)」へ変更、 | |
// 「C++ 言語標準」を「規定(ISO C++14 標準)」から | |
// 「ISO C++17 標準 (/std:c++17)」へ変更する。 | |
// | |
// 左側「構成プロパティ」のツリーで | |
// 「C/C++」→「コマンドライン」をクリックする。 | |
// 右側で | |
// 「追加のオプション」に「/await /bigobj」を入力する。 | |
// | |
// 左側「構成プロパティ」のツリーで | |
// 「リンカー」→「入力」をクリックする。 | |
// 右側で | |
// 「追加の依存ファイル」のプルダウンから「<編集...>」を選び、 | |
// 「追加の依存ファイル」ダイアログを出す。 | |
// 一番上の入力欄に「windowsapp.lib」を入力して OK ボタンを押す。 | |
// | |
// 「winrt_midi_transfer プロパティページ」ダイアログに戻るので、 | |
// OK ボタンを押して、設定完了。 | |
// ビルド | |
// | |
// Visual Studio 2019 のツールバーで | |
// 「Debug」「x64」または「Release」「x64」を選んで | |
// ビルドやデバッグをすればよい。 | |
// 参考 | |
// 似たような機能を python winrt で実現したもの | |
// https://gist.github.com/trueroad/6beaf87280afb2b5e33b4838d73be6ed | |
#include <chrono> | |
#include <future> | |
#include <mutex> | |
#include <iomanip> | |
#include <iostream> | |
#include <regex> | |
#include <sstream> | |
#include <string> | |
#include <string_view> | |
#include <vector> | |
#include <windows.h> | |
#include <winrt/base.h> | |
#include <winrt/Windows.Foundation.h> | |
#include <winrt/Windows.Foundation.Collections.h> | |
#include <winrt/Windows.Devices.Enumeration.h> | |
#include <winrt/Windows.Devices.Midi.h> | |
#include <winrt/Windows.Storage.Streams.h> | |
using namespace std::chrono_literals; | |
using namespace winrt; | |
using namespace Windows::Foundation; | |
using namespace Windows::Devices::Enumeration; | |
using namespace Windows::Devices::Midi; | |
using namespace Windows::Storage::Streams; | |
// WinRT MIDI Transfer class | |
class winrt_midi_transfer | |
{ | |
public: | |
// ポートの名前などをまとめた構造体 | |
struct port | |
{ | |
// ポートの名前 | |
std::wstring name; | |
// ポートの ID | |
std::wstring id; | |
// ID の 16 進数 8 桁部分 | |
std::wstring hex_id; | |
// ポートの表示名(ID の 16 進数 8 桁部分を含む) | |
std::wstring display_name; | |
}; | |
winrt_midi_transfer() | |
{ | |
// 念のため mutex をかけておく | |
std::lock_guard<std::mutex> lock(mtx_ctor_); | |
if (!event_) | |
{ | |
// 終了待ちイベントが無いので作成する | |
event_.attach(::CreateEvent(nullptr, true, false, nullptr)); | |
winrt::check_bool(static_cast<bool>(event_)); | |
// Ctrl+C などのハンドラを登録する | |
winrt::check_bool(::SetConsoleCtrlHandler(HandlerRoutine, true)); | |
} | |
} | |
// ポートのリストを取得する | |
// | |
// 引数:デバイスを選択するための文字列 | |
// MidiInPort::GetDeviceSelector() または | |
// MidiOutPort::GetDeviceSelector() を指定する。 | |
// 返り値:ポートのリスト | |
// | |
// IAsyncOperation などは C++/WinRT の型でなければ使用できない | |
// (コンパイルエラーになる)ので、 | |
// コルーチンにするために std::future を使っている。 | |
// マイクロソフトのドキュメントでは C++/WinRT の型以外を返す際には | |
// std::future より ppltasks.h の concurrency::task を勧めているが、 | |
// 使い方がよくわからないし Visual Studio 以外の環境でも | |
// 使えるようになるか疑問なので C++ 標準の std::future の方を使う。 | |
// ただし、現状 GCC-11 では C++20 を有効にしていても | |
// std::future ではコルーチン化できないようである。 | |
// | |
// 時間がかかっても構わないのでコルーチンではなく普通の関数にしてしまう | |
// (FindAllAsync() に .get() を付ける)ことや、 | |
// 返り値を返さずにメンバ変数に格納するだけにして IAsyncAction 型にする、 | |
// 返り値を std::vector ではなく C++/WinRT の型に変えて IAsyncOperation | |
// を使うようにする、などの方法も考えられる。 | |
std::future<std::vector<port>> list_ports(winrt::hstring device_selector) | |
{ | |
const auto devs = | |
co_await DeviceInformation::FindAllAsync(device_selector); | |
std::vector<port> retval; | |
for (const auto& d : devs) | |
{ | |
port p; | |
// d.Name() などは C++/WinRT の文字列型 winrt::hstring である。 | |
// これは operator std::wstring_view() があり | |
// std::wstring には std::wstring_view() を引数に取る | |
// 代入演算子があるのでそのまま代入できる。 | |
p.name = d.Name(); | |
p.id = d.Id(); | |
p.display_name = d.Name(); | |
// ID の 16 進数 8 桁部分を抽出する | |
std::wsmatch m; | |
if (std::regex_search(p.id, m, hex_id_pattern_)) | |
{ | |
// ID の 16 進数 8 桁部分を保存する | |
p.hex_id = m[1]; | |
// 表示名にも付与する | |
// 名前がまったく同じでも ID で区別できるようになる | |
std::wostringstream ss; | |
ss << p.name | |
<< L" [ " | |
<< p.hex_id | |
<< L" ]"; | |
p.display_name = ss.str(); | |
} | |
// ID に 16 進数 8 桁部分がなければ何もしない | |
// (p.hex_id は空文字列、表示名は名前と同じ) | |
// `Microsoft GS Wavetable Synth` が該当 | |
retval.push_back(p); | |
} | |
co_return retval; | |
} | |
// Windows 10 では MIDI OUT の名前がおかしいので修正する | |
// | |
// なぜか Windows 10 では一部の MIDI OUT ポートの名前が | |
// `MIDI` になってしまい名前では判別がつかなくなってしまう。 | |
// https://forum.juce.com/t/winrt-midi-output-wrong-device-names/43301 | |
// (上記ページによると `MIDI-2` などになる場合もあるようだが | |
// 手元では複数ポートがあってもすべて `MIDI` になり、 | |
// `MIDI-2` などは見たことが無い) | |
// これは MIDI IN/OUT で ID の 16 進数 8 桁部分が同じものがある場合に | |
// 発生する現象のようであり、恐らく Windows 10 のバグだが放置されている。 | |
// (手元では USB MIDI の M4U eX の 8 ポートはすべて発生、 | |
// BLE MIDI の MD-BT01 は IN/OUT で 16 進数 8 桁が異なるため発生しない) | |
// | |
// 上記ページにあるとおり、MIDIberry というアプリは自前で何とかしており、 | |
// IN/OUT で ID が類似している場合に IN の名前を使っているらしい。 | |
// そこで、同様の方法で MIDI OUT の名前を修正する。 | |
void fix_display_name() | |
{ | |
for (auto& outp : out_ports_) | |
{ | |
for (const auto& inp : in_ports_) | |
{ | |
if (outp.hex_id == inp.hex_id && | |
std::wstring_view{ outp.name }.substr(0, 4) == L"MIDI") | |
{ | |
// hex_id (ID の 16 進数 8 桁部分)が一致、かつ | |
// 名前の最初の 4 文字 | |
// (std::wstring_view にすることで | |
// 部分文字列を新しく作らないようにしている) | |
// が MIDI であれば、事象発生とみなし、 | |
// IN の表示名を OUT の表示名にコピーする。 | |
outp.display_name = inp.display_name; | |
break; | |
} | |
} | |
} | |
} | |
// MIDI IN ポートを選択する | |
// | |
// 返り値:選択された MIDI IN ポート | |
// 選択されなかったら nullptr になる | |
IAsyncOperation<MidiInPort> select_midi_in_port() | |
{ | |
in_ports_ = co_await list_ports(MidiInPort::GetDeviceSelector()); | |
// MIDI IN ポートの選択肢を表示 | |
std::wcout | |
<< std::endl | |
<< L"MIDI IN ports" | |
<< std::endl << std::endl; | |
for (unsigned int i = 0; i < in_ports_.size(); i++) | |
{ | |
std::wcout | |
<< i | |
<< L": " | |
<< in_ports_[i].display_name | |
<< std::endl; | |
} | |
// 選択肢を入力させる | |
std::wcout | |
<< std::endl | |
<< L"Select number > " | |
<< std::flush; | |
unsigned int choice; | |
std::wcin >> choice; | |
// 選択範囲内か否か判定 | |
if (choice < in_ports_.size()) | |
{ | |
// 選択されたポートの情報を表示 | |
std::wcout | |
<< std::endl | |
<< L"Selected: " | |
<< choice | |
<< L", " | |
<< in_ports_[choice].name | |
<< L", " | |
<< in_ports_[choice].id | |
<< L", " | |
<< in_ports_[choice].hex_id | |
<< L", " | |
<< in_ports_[choice].display_name | |
<< std::endl; | |
// ポートの作成を要求 | |
auto async = MidiInPort::FromIdAsync(in_ports_[choice].id); | |
// 5 秒待って完了していたら結果を受け取って返す | |
if (async.wait_for(5s) == AsyncStatus::Completed) | |
co_return async.GetResults(); | |
// 5 秒では完了しなかったのでタイムアウト | |
// ペアリング済みの BLE MIDI は圏外でもリストに登場するが | |
// 接続できないとずっと処理中になるのでタイムアウトを設定 | |
std::wcout | |
<< L"Timeout" | |
<< std::endl; | |
co_return nullptr; | |
} | |
// 選択範囲外 | |
std::wcout | |
<< L"Error: " | |
<< choice | |
<< std::endl; | |
co_return nullptr; | |
} | |
// MIDI OUT ポートを選択する | |
// | |
// 返り値:選択された MIDI IN ポート | |
// 選択されなかったら nullptr になる | |
// | |
// 返り値は MidiOutPort ではなく IMidiOutPort である。 | |
// I がつく型は I が付かない型と MidiSynthesizer 型を | |
// 統一的に扱える型のようである。 | |
// ただ、GetDeviceSelector() や FromIdAsync() は | |
// I が付かない方にのみ存在し、しかも I が付かない FromIdAsync() は | |
// なぜか I が付く方を返すという WinRT 仕様になっている。 | |
// 本コルーチンも I が付く方を返すようにしている。 | |
IAsyncOperation<IMidiOutPort> select_midi_out_port() | |
{ | |
out_ports_ = co_await list_ports(MidiOutPort::GetDeviceSelector()); | |
fix_display_name(); | |
// MIDI OUT ポートの選択肢を表示 | |
std::wcout | |
<< std::endl | |
<< L"MIDI OUT ports" | |
<< std::endl << std::endl; | |
for (unsigned int i = 0; i < out_ports_.size(); i++) | |
{ | |
std::wcout | |
<< i | |
<< L": " | |
<< out_ports_[i].display_name | |
<< std::endl; | |
} | |
// 選択肢を入力させる | |
std::wcout | |
<< std::endl | |
<< L"Select number > " | |
<< std::flush; | |
unsigned int choice; | |
std::wcin >> choice; | |
// 選択範囲内か否か判定 | |
if (choice < out_ports_.size()) | |
{ | |
// 選択されたポートの情報を表示 | |
std::wcout | |
<< std::endl | |
<< L"Selected: " | |
<< choice | |
<< L", " | |
<< out_ports_[choice].name | |
<< L", " | |
<< out_ports_[choice].id | |
<< L", " | |
<< out_ports_[choice].hex_id | |
<< L", " | |
<< out_ports_[choice].display_name | |
<< std::endl; | |
// ポートの作成を要求 | |
auto async = MidiOutPort::FromIdAsync(out_ports_[choice].id); | |
// 5 秒待って完了していたら結果を受け取って返す | |
if (async.wait_for(5s) == AsyncStatus::Completed) | |
co_return async.GetResults(); | |
// 5 秒では完了しなかったのでタイムアウト | |
// ペアリング済みの BLE MIDI は圏外でもリストに登場するが | |
// 接続できないとずっと処理中になるのでタイムアウトを設定 | |
std::wcout | |
<< L"Timeout" | |
<< std::endl; | |
co_return nullptr; | |
} | |
// 選択範囲外 | |
std::wcout | |
<< L"Error: " | |
<< choice | |
<< std::endl; | |
co_return nullptr; | |
} | |
// MIDI IN ポートと MIDI OUT ポートを選んで転送 | |
IAsyncAction transfer() | |
{ | |
in_port_ = co_await select_midi_in_port(); | |
out_port_ = co_await select_midi_out_port(); | |
if (!in_port_) | |
{ | |
std::wcout | |
<< L"Error: MIDI IN is not opened" | |
<< std::endl; | |
co_return; | |
} | |
std::wcout | |
<< L"Callback starting..." | |
<< std::endl; | |
// MIDI メッセージ受信時のイベントハンドラを設定し、 | |
// 元のイベントハンドラのトークンを保存する。 | |
// this ポインタを使っているのでインスタンス破棄中などの | |
// 微妙なタイミングでイベント発生するとマズいことになる。 | |
// get_strong() で強参照を得るなどした方がよさそうではあるが、 | |
// 本クラスは C++/WinRT のクラスから派生していないので使えない。 | |
auto before_token = in_port_.MessageReceived({ this, | |
&winrt_midi_transfer::midi_in_callback }); | |
std::wcout | |
<< L"Started" | |
<< std::endl; | |
// _event がシグナル状態になるまでコルーチンを中断 | |
co_await resume_on_signal(event_.get()); | |
// イベントハンドラを元に戻す。 | |
// これによって this ポインタを使った自前ハンドラが呼ばれなくなり、 | |
// この後の this ポインタ参照を防ぐ。 | |
in_port_.MessageReceived(before_token); | |
} | |
private: | |
std::wstring note_off(const MidiNoteOffMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto note{ message.Note() }; | |
const auto velocity{ message.Velocity() }; | |
std::wostringstream buff; | |
buff << L"note_off: channel " | |
<< channel | |
<< L", note " | |
<< note | |
<< L", velocity " | |
<< velocity; | |
return buff.str(); | |
} | |
std::wstring note_on(const MidiNoteOnMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto note{ message.Note() }; | |
const auto velocity{ message.Velocity() }; | |
std::wostringstream buff; | |
buff << L"note_on: channel " | |
<< channel | |
<< L", note " | |
<< note | |
<< L", velocity " | |
<< velocity; | |
return buff.str(); | |
} | |
std::wstring polytouch(const MidiPolyphonicKeyPressureMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto note{ message.Note() }; | |
const auto pressure{ message.Pressure() }; | |
std::wostringstream buff; | |
buff << L"polytouchf: channel " | |
<< channel | |
<< L", note " | |
<< note | |
<< L", pressure " | |
<< pressure; | |
return buff.str(); | |
} | |
std::wstring control_change(const MidiControlChangeMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto controller{ message.Controller() }; | |
const auto control_value{ message.ControlValue() }; | |
std::wostringstream buff; | |
buff << L"control_change: channel " | |
<< channel | |
<< L", controller " | |
<< controller | |
<< L", control_value " | |
<< control_value; | |
return buff.str(); | |
} | |
std::wstring program_change(const MidiProgramChangeMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto program{ message.Program() }; | |
std::wostringstream buff; | |
buff << L"program_change: channel " | |
<< channel | |
<< L", program " | |
<< program; | |
return buff.str(); | |
} | |
std::wstring aftertouch(const MidiChannelPressureMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto pressure{ message.Pressure() }; | |
std::wostringstream buff; | |
buff << L"aftertouch: channel " | |
<< channel | |
<< L", pressure " | |
<< pressure; | |
return buff.str(); | |
} | |
std::wstring pitchwheel(const MidiPitchBendChangeMessage& message) | |
{ | |
const auto channel{ message.Channel() }; | |
const auto bend{ message.Bend() }; | |
std::wostringstream buff; | |
buff << L"pitchwheel: channel " | |
<< channel | |
<< L", bend " | |
<< bend; | |
return buff.str(); | |
} | |
std::wstring sysex(const MidiSystemExclusiveMessage& message) | |
{ | |
const auto raw_data{ message.RawData() }; | |
return L"sysex"; | |
} | |
std::wstring quarter_frame(const MidiTimeCodeMessage& message) | |
{ | |
const auto frame_type{ message.FrameType() }; | |
const auto raw_data{ message.RawData() }; | |
std::wostringstream buff; | |
buff << L"quarter_frame: frame_type " | |
<< frame_type; | |
return buff.str(); | |
} | |
std::wstring songpos(const MidiSongPositionPointerMessage& message) | |
{ | |
const auto beats{ message.Beats() }; | |
const auto raw_data{ message.RawData() }; | |
std::wostringstream buff; | |
buff << L"songpos: beats " | |
<< beats; | |
return buff.str(); | |
} | |
std::wstring song_select(const MidiSongSelectMessage& message) | |
{ | |
const auto song{ message.Song() }; | |
const auto raw_data{ message.RawData() }; | |
std::wostringstream buff; | |
buff << L"song_select: song " | |
<< song; | |
return buff.str(); | |
} | |
std::wstring tune_request(const MidiTuneRequestMessage& message) | |
{ | |
return L"tune_request"; | |
} | |
std::wstring sysex_eox(const MidiSystemExclusiveMessage& message) | |
{ | |
return L"sysex_eox"; | |
} | |
std::wstring clock(const MidiTimingClockMessage& message) | |
{ | |
return L"clock"; | |
} | |
std::wstring start(const MidiStartMessage& message) | |
{ | |
return L"clock"; | |
} | |
std::wstring continue_message(const MidiContinueMessage& message) | |
{ | |
return L"continue"; | |
} | |
std::wstring stop(const MidiStopMessage& message) | |
{ | |
return L"stop"; | |
} | |
std::wstring active_sensing(const MidiActiveSensingMessage& message) | |
{ | |
return L"active_sensing"; | |
} | |
std::wstring system_reset(const MidiSystemResetMessage& message) | |
{ | |
return L"system_reset"; | |
} | |
// MIDI メッセージを文字列へ変換する | |
// | |
// 引数:MIDI メッセージ | |
// 返り値:MIDI メッセージを文字列化したもの | |
// | |
// コルーチンではないので引数の破棄を考慮する必要が無く、 | |
// const 参照で構わない。 | |
std::wstring message_to_str(const IMidiMessage& message) | |
{ | |
std::wostringstream buff; | |
const auto t{ message.Type() }; | |
switch (t) | |
{ | |
case MidiMessageType::NoteOff: | |
// C++/WinRT では IMidiMessage 型から MidiNoteOffMessage 型 | |
// への変換のように、派生型へ変換するには | |
// try_as() または as() を使う。 | |
buff << note_off(message.try_as<MidiNoteOffMessage>()); | |
break; | |
case MidiMessageType::NoteOn: | |
buff << note_on(message.try_as<MidiNoteOnMessage>()); | |
break; | |
case MidiMessageType::PolyphonicKeyPressure: | |
buff << polytouch( | |
message.try_as<MidiPolyphonicKeyPressureMessage>()); | |
break; | |
case MidiMessageType::ControlChange: | |
buff << control_change( | |
message.try_as<MidiControlChangeMessage>()); | |
break; | |
case MidiMessageType::ProgramChange: | |
buff << program_change( | |
message.try_as<MidiProgramChangeMessage>()); | |
break; | |
case MidiMessageType::ChannelPressure: | |
buff << aftertouch( | |
message.try_as<MidiChannelPressureMessage>()); | |
break; | |
case MidiMessageType::PitchBendChange: | |
buff << pitchwheel( | |
message.try_as<MidiPitchBendChangeMessage>()); | |
break; | |
case MidiMessageType::SystemExclusive: | |
buff << sysex( | |
message.try_as<MidiSystemExclusiveMessage>()); | |
break; | |
case MidiMessageType::MidiTimeCode: | |
buff << quarter_frame( | |
message.try_as<MidiTimeCodeMessage>()); | |
break; | |
case MidiMessageType::SongPositionPointer: | |
buff << songpos( | |
message.try_as<MidiSongPositionPointerMessage>()); | |
break; | |
case MidiMessageType::SongSelect: | |
buff << song_select( | |
message.try_as<MidiSongSelectMessage>()); | |
break; | |
case MidiMessageType::TuneRequest: | |
buff << tune_request( | |
message.try_as<MidiTuneRequestMessage>()); | |
break; | |
case MidiMessageType::EndSystemExclusive: | |
buff << sysex_eox( | |
message.try_as<MidiSystemExclusiveMessage>()); | |
break; | |
case MidiMessageType::TimingClock: | |
buff << clock( | |
message.try_as<MidiTimingClockMessage>()); | |
break; | |
case MidiMessageType::Start: | |
buff << start( | |
message.try_as<MidiStartMessage>()); | |
break; | |
case MidiMessageType::Continue: | |
buff << continue_message( | |
message.try_as<MidiContinueMessage>()); | |
break; | |
case MidiMessageType::Stop: | |
buff << stop( | |
message.try_as<MidiStopMessage>()); | |
break; | |
case MidiMessageType::ActiveSensing: | |
buff << active_sensing( | |
message.try_as<MidiActiveSensingMessage>()); | |
break; | |
case MidiMessageType::SystemReset: | |
buff << system_reset( | |
message.try_as<MidiSystemResetMessage>()); | |
break; | |
default: | |
buff << L"unknown (" | |
<< static_cast<int>(t) | |
<< L")"; | |
} | |
const auto raw_data = message.RawData(); | |
const std::basic_string_view<uint8_t> raw_data_sv | |
{ raw_data.data(), raw_data.Length() }; | |
buff << L"," << std::hex; | |
for (auto u : raw_data_sv) | |
{ | |
buff << L" " | |
<< std::setw(2) << std::setfill(L'0') | |
<< u; | |
} | |
return buff.str(); | |
} | |
// MIDI メッセージを表示する | |
// Fire and forget として終了まで待つ必要も返り値もなく、 | |
// バックグラウンドで表示処理を行う | |
// | |
// 引数:表示する MIDI メッセージ | |
// C++/WinRT の型を値渡ししているので中身がコピーされており、 | |
// 呼び出し側で破棄されても問題ないと考えられる | |
winrt::fire_and_forget show_midi_message(IMidiMessage message) | |
{ | |
// コルーチンを中断して戻り、バックグラウンドで再開する | |
co_await winrt::resume_background(); | |
// メッセージ受信時の時刻情報を取り出す | |
const auto timestamp = message.Timestamp(); | |
const std::chrono::nanoseconds duration = timestamp; | |
{ | |
// スレッド間でメッセージ表示に割り込まれると見にくくなるので | |
// 1 行の表示が終わるまで mutex でロックする。 | |
std::lock_guard<std::mutex> lock(mtx_show_message_); | |
// 時刻情報とメッセージを文字列化したものを表示 | |
std::wcout | |
<< duration.count() | |
<< L", " | |
<< message_to_str(message) | |
<< std::endl; | |
} | |
} | |
// MIDI IN イベントで呼ばれるコールバック | |
// MIDI メッセージ受信時のイベントハンドラ | |
// | |
// 引数:イベントが発生した MIDI IN ポートと MIDI メッセージ情報 | |
void midi_in_callback(const MidiInPort& sender, | |
const MidiMessageReceivedEventArgs& e) | |
{ | |
// MIDI メッセージを取り出す | |
const auto message = e.Message(); | |
// MIDI OUT ポートが有効ならそのまま送信 | |
// メッセージに時刻情報がついているが WinRT 仕様で送信時は無視される | |
if (out_port_) | |
out_port_.SendMessage(message); | |
// MIDI メッセージ表示 | |
// Fire and forget なので制御は即時に戻る | |
show_midi_message(message); | |
} | |
// Ctrl+C などが押されたときのハンドラ | |
// | |
// インスタンス情報(this ポインタ)を載せて呼ぶことが | |
// できないので static メンバ関数 | |
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) | |
{ | |
if (dwCtrlType == CTRL_C_EVENT) | |
{ | |
// Ctrl+C が来たら event_ をシグナル状態にする | |
winrt::check_bool(::SetEvent(event_.get())); | |
// 割込みがあった旨を表示 | |
std::wcout | |
<< L"Interrupted" | |
<< std::endl; | |
// 本来の Ctrl+C 処理をせず強制終了させない | |
return true; | |
} | |
// Ctrl+C 以外は本来の処理を行う | |
return false; | |
} | |
// MIDI IN ポート | |
MidiInPort in_port_ = nullptr; | |
// MIDI OUT ポート | |
IMidiOutPort out_port_ = nullptr; | |
// MIDI IN ポートのリスト | |
std::vector<port> in_ports_; | |
// MIDI OUT ポートのリスト | |
std::vector<port> out_ports_; | |
// ID の 16 進数 8 桁部分を抽出するパターン | |
// 固定で共通なので static メンバ変数 | |
static const std::wregex hex_id_pattern_; | |
// コンストラクタのスレッド間競合を防ぐ mutex | |
// クラス全体で共通にしなければならないので static メンバ変数 | |
static std::mutex mtx_ctor_; | |
// メッセージ表示中にスレッド間の割込みを防ぐ mutex | |
static std::mutex mtx_show_message_; | |
// 終了要求を示すイベント | |
// Ctrl+C ハンドラがプロセスで 1 つしか持てず | |
// インスタンス情報(this ポインタ)が載せられないので | |
// static メンバ変数 | |
// カーネルのイベントの HANDLE を直接使ってもよいが、 | |
// C++/WinRT では winrt::handle を使った方がよさそう | |
static winrt::handle event_; | |
}; | |
// ID の 16 進数 8 桁部分を抽出するパターン、実体 | |
const std::wregex winrt_midi_transfer::hex_id_pattern_ | |
{ std::wregex(L"#MIDII_([0-9A-F]{8})\\..+#") }; | |
// コンストラクタのスレッド間競合を防ぐ mutex、実体 | |
std::mutex winrt_midi_transfer::mtx_ctor_; | |
// メッセージ表示中にスレッド間の割込みを防ぐ mutex、実体 | |
std::mutex winrt_midi_transfer::mtx_show_message_; | |
// 終了要求を示すイベント、実体 | |
winrt::handle winrt_midi_transfer::event_; | |
// メイン | |
int main() | |
{ | |
// C++/WinRT では最初に必要 | |
init_apartment(); | |
std::wcout | |
<< L"WinRT MIDI Transfer with C++/WinRT" | |
<< std::endl << std::endl | |
<< L"https://gist.github.com/trueroad/" | |
L"9c5317af5f212b2de7c7012e76b9e66b" | |
<< std::endl << std::endl | |
<< L"Copyright (C) 2022 Masamichi Hosoda." | |
<< std::endl | |
<< L"All rights reserved." | |
<< std::endl << std::endl; | |
// インスタンスを作る | |
winrt_midi_transfer wmt; | |
// 転送のコルーチンを呼んで終わるまで待つ | |
wmt.transfer().get(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment