uchan が 2021年02月28日22時38分29秒 に編集
誤字修正
本文の変更
調とはハ長調やイ短調と呼ばれる、音楽を特徴付ける属性の一つです。音楽の血液型のようなものと考えることもでき、世の音楽の多くは 12 種類の調に分類することができます。MIDI 信号を用いて調を判定する調判定機を作ってみたので紹介します。 ![MIDI調判定機の外観](https://camo.elchika.com/3de9a1f984c6f2b5d134c0f97064960d3d383bca/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f65346432643137332d303138332d346430312d626265322d613063316663663537333766/) ## 調判定とは [Wikipedia の調の定義](https://ja.wikipedia.org/wiki/調) は難しい言葉で書いてあるので、実は筆者もよく分かりません。今回、この装置を一緒に作った絶対音感を持つ共同制作者曰く、調ごとに異なるイメージ(人格みたいなもの)があり、この調はこんな音楽によく使われる、という感覚があるそうです。すごいなあ。 技術的には、音楽の構成音を調べることにより調を判定できます。例えば「ド、ミ、ソ」だけで構成される音楽の場合、ハ長調、ヘ長調、ト長調のいずれかである、と判定できます。 現実にはその調に含まれない音をスパイス的に使ったり、あるいは途中で転調したりするため、「どちらかというと、ハ長調っぽい」とか「この部分はイ短調だなあ」というような判定になります。 ## デモ動画(Hey Jude) YouTube でデモ動画を公開しています。弾き始めは C と F が等確率なので仮に C を表示していますが、途中でシ♭(B♭)が出てくることで F の方が優勢となり、表示が切り替わります。 https://www.youtube.com/watch?v=hQ5n5zbAjok ## 動作の様子 MIDI 判定ソフトウェアを Raspberry Pi で起動した直後は、まだ調が判定されていないことを示す「-」(ハイフン)が表示されます。 ![調が判定されていないことを示すハイフン表示](https://camo.elchika.com/4079b8feb37f910555c0880b2cf75a276ae010d1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f34333266303339642d653037622d343963622d626462352d333864663962386631656631/) 試しにド、ミ、ソを弾くと「C」(ハ長調)と判定されます。 ![ハ長調を示すC表示](https://camo.elchika.com/25c43f0729e5bba42e86942771e0a6ae5dcd180a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f64663433633365392d356263642d343366322d616461342d376635313832313636396134/) 続けて、いくつか黒鍵を弾くことで「D♭」(変ニ長調)を表示させてみた様子です。 ![D♭表示](https://camo.elchika.com/35fb7273219421c4354be75df21aec6f166fc9cc/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f35643961653535352d333663632d346334382d383964382d653733396233373039343265/) ## 調判定機の構成 Raspberry Pi 4 の UART 機能で MIDI 信号を受信し、調判定の結果を 7 セグメント LED と赤、青の LED で表示します。7 セグメント LED にはドレミを英語表記でC、D、E、……と表示します。赤と青の LED はそれぞれ、シャープ、フラットに対応します。 ![調判定機の全体像](https://camo.elchika.com/5feed8dac89be542be26345403d51ba7f252448b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f37646236366333342d363436302d343563322d613335382d333862333431313565666361/) 使用した部品と回路図は以下の通りです。 - Raspberry Pi 4 - 7 セグメント LED C-551SR - LED(赤と青) - DIN コネクタ - フォトカプラ FOD817B - NPN トランジスタ 2SC1815 - 小信号用ダイオード 1SS178 - 各種抵抗器 - 両面スルーホール基板 - DIN コネクタ用ピッチ変換基板 - 各種ピンヘッダ、ピンソケット ![回路図](https://camo.elchika.com/0e78cbe3694f054cf5116853ef053891af51f6ff/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f36333465356166352d303032652d343131382d623661302d323164363239613234623361/) 以下に示すように、Raspberry Pi と基板の接続は、Raspberry Pi から出ているピンヘッダに、基板裏面に実装したピンソケットを指すことで行いました。これをやるために両面スルーホールの基板を選んだというわけです。 ![Raspberry Pi と基板の接続を裏から見た様子](https://camo.elchika.com/30fc3baa2349b04f5e1a64f11ca0322c9e273fdd/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f38613735633535352d633336352d343061342d623131662d336166323237643838613135/) ## MIDI 受信回路 先に示した回路図の左側が MIDI の受信回路です。5 ピンの DIN コネクタから入力された MIDI 信号を、汎用フォトカプラ FOD817B で電気的に分離しつつ、R2 で電流→電圧変換を行っています。 MIDI 信号は電流制御の信号として送受信します。「論理 0」のときに 5mA、「論理 1」のときに 0mA という仕様になっており、電圧制御ではないので注意が必要です。Raspberry Pi の GPIO を含む、一般的なロジック回路は電圧入力ですから、電流→電圧変換が必要なのです。 [MIDI 1.0 の規格書](http://amei.or.jp/midistandardcommittee/MIDIspcj.html) によると、送信側と受信側を電気的に分離する必要があるとのことです。規格書には、そのために使用できる高速オプトアイソレータ(フォトカプラ)として、PC-900V などの製品が紹介されています。 ただ、そのような高速オプトアイソレータは若干高いので、[汎用 4 ピンフォトカプラを使った MIDI 入力](http://pcm1723.g3.xrea.com/html/p4pcmidi.htm) を参考にして汎用フォトカプラ FOD817B で受信回路を作ってみました。この記事を検証した結果は別記事 [フォトカプラの出力特性改善の検証](https://elchika.com/article/b330bca7-08c7-4707-a755-36699731f7f7/) に載せてあります。2SC1815 を 2 つ使ったカレントミラー回路がキモで、この回路によって FOD817B の出力波形の品質が大きく高まり、MIDI の受信ミスが発生しにくくなります。 ## 判定表示回路 先に示した回路図の右側が表示回路です。 調判定結果の表示はとても単純で、7 セグメント LED と赤、青 LED を Raspberry Pi の GPIO に接続しているだけです。Raspberry Pi の GPIO に流せる電流量は、どうやら 1 つのピンあたり 16mA まで、合計 50mA まで、ということなので、同時に 9 個の LED を点灯させても壊れないように、1 つあたり 1〜2 mA 程度になるよう、抵抗値を決めています。 シャープを赤、フラットを青にしているのは、共同制作者の感性によるものです。電子回路的には接続すべき抵抗値が異なるだけで、何色であろうが制御方法に差はありません。 ## Raspberry Pi の UART 速度の調整
MIDI 信号は電流駆動なので、受信回路は少し特殊なものになりましたが、通信プロトコルは一般的な UART と同等ですから、Raspberry Pi の UART 回路で受信できます。R2 により電圧に変換された信号を Raspberry Pi の GPIO15 で受信し、内蔵 UART 機能によってハードウェア的に受信します。
MIDI 信号は電流駆動なので、受信回路は少し特殊なものになりましたが、通信プロトコルは一般的な UART と同等ですから、Raspberry Pi の UART 回路で受信できます。R2 により電圧に変換された信号を Raspberry Pi の GPIO15 に接続し、内蔵 UART 機能によってハードウェア的に受信します。
ただし、MIDI の通信速度は 31.25Kbps となっていますので、一般的な UART で想定される通信速度とは異なります。今回使おうとしている Raspberry Pi のライブラリ [pigpio](http://abyz.me.uk/rpi/pigpio/) のシリアル通信機能では、近しいビットレートとしては 19200 と 38400 が用意されています。しかし、どちらも MIDI には使えそうもありません。 しかし、通信速度に関しては Raspberry Pi の設定を変更すると対応できるので、問題ありません。設定方法は [The definitive guide to MIDI IN with Raspberry Pi's GPIO](https://www.samplerbox.org/article/midiinwithrpi) が詳しいです。 参考までに、/boot/config.txt に設定を追記する前と後の stty コマンドの表示を載せておきます。 **追記前の stty** は次の通りです。 ``` $ sudo stty -F /dev/ttyAMA0 -a speed 3000000 baud; rows 0; columns 0; line = 15; ≪略≫ ``` /boot/config.txt に設定を 3 行追記します。 ``` enable_uart=1 dtoverlay=pi3-miniuart-bt dtoverlay=midi-uart0 ``` **追記後の stty** は次の通りです。 ``` $ sudo stty -F /dev/ttyAMA0 -a speed 115200 baud; rows 24; columns 80; line = 0; ≪略≫ ``` 「dtoverlay=pi3-miniuart-bt」の行は、UART の回路がデフォルトで Bluetooth 用に使用されるのを、シリアル通信(miniuart)に使うように切り替えるための設定だそうです。この設定をすると Bluetooth が使えなくなると思いますが、今回は Bluetooth を使いませんから問題ありません。 「dtoverlay=midi-uart0」が UART0(GPIO14/15 で使える UART 送受信回路)の速度を MIDI 用の速度にするための設定です。この設定をした上で、ソフトウェアから 38400bps を選択することで、31.25Kbps のシリアル通信信号となるはずです。 ## 通信速度の確認 dtoverlay=midi-uart0 の設定有無で、本当に UART の動作速度が変わるかを検証しました。dtoverlay=midi-uart0 の設定の有無を変えてみて、UART 出力信号を観測します。検証は Python 用ライブラリ pySerial を用いて、38400bps の設定でデータを送信します。 まず、midi-uart0 無しでは、次の通り 38400bps の設定通りの信号が出ます。 ![Raspberry Piから38400baudの信号が出ている様子](https://camo.elchika.com/a3cfd1b29780a99aa4db595e59e9a6d316fa31c8/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f62663562623730382d373938622d346165332d393937302d623266663038396231306234/) スタートビットから次のスタートビットまでが 260.4μs ですから、1 ビットあたり 26.04μs、すなわち 1[s] / 26.04[μs/bit] = 38402[bps] となります。期待通りですね。 次に、midi-uart0 を追加したときの信号の様子を見ます。設定は相変わらず 38400bps を使います。 ![Raspberry Piから31250baudの信号が出ている様子](https://camo.elchika.com/db6c855b0ba7d7f805609684593da94056edf725/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f36653061333837322d393734612d346466372d626433302d666636626531306366346364/) 測定値は 10 ビットで 320μs です。1[s] / 32.0[μs/bit] = 31250[bps] となり、こちらも期待通りになりました。ということで config.txt への dtoverlay=midi-uart0 の追加により、Raspberry Pi 内蔵の UART 回路のクロックが MIDI に適したものに変わることが確認できました。 ## UART の用途変更設定 通信速度の実験をしているとき、UART によるシリアル通信が不安定になる現象が発生しました。調査すると、どうやら Raspberry Pi(というか Linux)が持つシリアルポートログイン機能とシリアル通信が競合しているようなのです。[ラズパイのGPIOシリアル通信時のエラー:serial.serialutil.SerialException device reports readiness to read but returned no data](https://www.suzu6.net/posts/32/) を参考にして、シリアルポートでのログインを無効にします。 設定は上記サイトに詳しく説明されているので、手順をメモしておくだけにします。 1. sudo raspi-config 2. Interfacing Options 3. P6 Serial 4. "Would you like a login shell to be accessible over serial?" → いいえ 5. "Would you like the serial port hardware to be enabled?" → はい ## MIDI データの構造 さて、早速調判定のプログラムを紹介しよう、と思ったのですが、その前に MIDI データの構造とその解析方法を説明しなければなりません。詳しくは [MIDI 1.0 の規格書](http://amei.or.jp/midistandardcommittee/MIDIspcj.html) を参照いただくとして、ここではプログラムの説明に必要な範囲で仕様を説明します。 MIDI 楽器から出力される MIDI データとは、31.25Kbps で送られてくるバイト列です。バイト列は次の順で並んで送信されてきます。 ``` <ステータス・バイト> <ステータス・バイト> <データ・バイト> <ステータス・バイト> <データ・バイト> <データ・バイト> ``` ステータス・バイトと、それに続いて送られてくるデータ・バイトをまとめて「MIDI メッセージ」と呼びます。ステータス・バイトは最上位ビットが 1、データ・バイトは最上位ビットが 0 であることにより区別できるようになっています。 ステータス・バイトは MIDI メッセージの種類を表します。MIDI メッセージの種類に応じてデータ・バイトの個数が決まります。 代表的な MIDI メッセージには「ノート・オン」があります。ノート・オンは鍵盤を押したときに発せられる MIDI メッセージです。ノート・オンはデータ・バイトが 2 バイトで、鍵番号とベロシティです。鍵番号は中央のド(C)を 60 とし、1 オクターブで 12 だけ増減する値です。オクターブ下のドが 48、オクターブ上のドが 72 という具合に。ベロシティは鍵盤を押した強さを表す数値です。メゾフォルテに相当する値が 64 と定められています。試しに手元の MIDI ピアノの中央のドを叩いてみると、次のような 3 バイトが送られてきました。 ``` 0x90 0x3c 0x33 ``` 0x90 がノート・オン、0x3c が鍵番号(10 進数で 60)、0x33 がベロシティです。 この後紹介する調判定プログラムでは、ノート・オンの他にノート・オフ(0x80)、アクティブ・センシング(0xfe)が登場します。ノート・オフは名前の通り、鍵盤を離したことを表すメッセージです。フォーマットはノート・オンと同様、データ・バイトは 2 バイトです。アクティブ・センシングは MIDI 機器が接続されているかどうかを認識するために、最大 300ms 周期で送られてくる信号です。分野によっては「ハートビート信号」と言えば伝わりが良いでしょうか。 ## 調判定プログラム 最後になりましたが、pigpio ライブラリを用いて UART から 1 バイトずつ受信し、MIDI データを解析、調を判定して表示するプログラムを紹介します。まずはソースコード全体を掲載します。 ```cpp #include <array> #include <cstdlib> #include <iostream> #include <numeric> #include <pigpio.h> #include <queue> #include <string> #define LED_A 27 #define LED_B 22 #define LED_C 4 #define LED_D 3 #define LED_E 2 #define LED_F 18 #define LED_G 17 #define LED_RED 24 #define LED_BLUE 23 using namespace std; const uint8_t kLed7SegTable[] = { // ABCDEFG 0b01111110, // 0 0b00110000, // 1 0b01101101, // 2 0b01111001, // 3 0b00110011, // 4 0b01011011, // 5 0b01011111, // 6 0b01110000, // 7 0b01111111, // 8 0b01111011, // 9 0b01110111, // A 0b00011111, // B 0b01001110, // C 0b00111101, // D 0b01001111, // E 0b01000111, // F 0b01011110, // G 0b00000001, // - }; void Write7Seg(char c) { uint8_t v = 0; if ('0' <= c && c <= '9') { v = kLed7SegTable[c - '0']; } else if ('A' <= c && c <= 'G') { v = kLed7SegTable[c - 'A' + 10]; } else if ('a' <= c && c <= 'g') { v = kLed7SegTable[c - 'a' + 10]; } else if (c == '-') { v = kLed7SegTable[c - '-' + 17]; } int pins[] = { LED_G, LED_F, LED_E, LED_D, LED_C, LED_B, LED_A }; for (int i = 0; i < 7; ++i) { gpioSetMode(pins[i], PI_OUTPUT); gpioWrite(pins[i], v & 1); v >>= 1; } } int ser = -1; void CloseSerial() { if (ser >= 0) { serClose(ser); } } struct KeyConcordance { int key; double concordance; }; bool operator<(const KeyConcordance& lhs, const KeyConcordance& rhs){ if (lhs.concordance < rhs.concordance) return true; if (lhs.concordance > rhs.concordance) return false; return lhs.key > rhs.key; } int KeyIdentify(const array<int, 12>& note_count){ priority_queue<KeyConcordance> keyc; array<int, 7> maj{0,2,4,5,7,9,11}; int all = accumulate(note_count.begin(), note_count.end(), 0); for (int i = 0; i < 12; i++) { int match = 0; array<int, 12> scale{}; // 調の構成7音 for (int j = 0; j < 7; j++) { scale[(maj[j] + i) % 12]++; } // 構成音に該当する場合は出現数を計上 for (int j = 0; j < 12; j++) { if (scale[j] != 0){ match += note_count[j]; } } double concordance = (double)match / (double)all; keyc.push(KeyConcordance{i, concordance}); } return keyc.top().key; } int main(int argc, char** argv) { if (gpioInitialise() < 0) { return -1; } atexit(gpioTerminate); char serDevice[] = "/dev/ttyAMA0"; int ser = serOpen(serDevice, 38400, 0); if (ser < 0) { cerr << "failed to open /dev/ttyAMA0" << endl; return -1; } atexit(CloseSerial); array<int, 12> note_count{}; gpioSetMode(LED_RED, PI_OUTPUT); gpioSetMode(LED_BLUE, PI_OUTPUT); int s_seconds, s_micros; // 1秒を測る起点の時刻 gpioTime(PI_TIME_RELATIVE, &s_seconds, &s_micros); bool started = false; // 1度でもノートオンイベントが発生したか string event = "none"; // 直前に発生したイベントの種別(ランニング・ステータスの判断) while (1) { int c = serReadByte(ser); if (c < 0) { gpioSleep(PI_TIME_RELATIVE, 0, 300); continue; } else if (event == "unknown" && (c & 0x80) == 0) { continue; } else if (c == 0x90) { event = "noteon"; started = true; int note = serReadByte(ser); int velo = serReadByte(ser); note = note % 12; // 0=C, 11=B if (velo > 0) { note_count[note]++; } } else if (c == 0x80) { event = "noteoff"; serReadByte(ser); serReadByte(ser); } else if (c == 0xB0) { event = "controll"; //sustainなど serReadByte(ser); serReadByte(ser); } else if (c == 0xFE) { int seconds, micros; gpioTime(PI_TIME_RELATIVE, &seconds, µs); if ((seconds - s_seconds) > 1) { int tone = -1; // 判定結果(表示) if (started) { tone = KeyIdentify(note_count); } char white = 'C'; bool flat = false; bool sharp = false; switch (tone) { case -1: white = '-'; break; case 0: white = 'C'; break; case 1: white = 'D'; flat = true; break; case 2: white = 'D'; break; case 3: white = 'E'; flat = true; break; case 4: white = 'E'; break; case 5: white = 'F'; break; case 6: white = 'F'; sharp = true; break; case 7: white = 'G'; break; case 8: white = 'A'; flat = true; break; case 9: white = 'A'; break; case 10: white = 'B'; flat = true; break; case 11: white = 'B'; break; } Write7Seg(white); gpioWrite(LED_BLUE, flat); gpioWrite(LED_RED, sharp); s_seconds += 1; } } else if ((c & 0x80) != 0) { event = "unknown"; continue; } else if (event == "noteon") { // ランニング・ステータス int note = c; int velo = serReadByte(ser); note = note % 12; // 0=C, 11=B if (velo > 0) { note_count[note]++; } } else if (event == "noteoff") { // ランニング・ステータス serReadByte(ser); } else if (event == "controll") { // ランニング・ステータス serReadByte(ser); } } } ``` プログラム全体の構造を大まかに説明すると、次のような部分から構成されています。 - 7 セグメント LED 制御部 - 定数 `kLed7SegTable` は 7 セグメント LED の字形を定義します。 - 関数 `Write7Seg()` は 7 セグメント LED に与えられた文字を表示します。 - 調判定部 - 構造体 `KeyConcordance` は調と、その一致度(concordance)の組を表します。 - 関数 `KeyIdentify()` は与えられた鍵情報から調を判定します。引数 `note_count` は、ド(C)=0、シ(B)=11 とする配列で、それぞれの鍵が押された回数が要素に記録されています。 - MIDI 受信部 - 関数 `main()` は各種の初期化の後、MIDI データを読んで解析し、1 秒間隔で `KeyIdentify()` を呼び、結果を表示します。 以降、それぞれ簡単に説明します。 ## 7 セグメント LED 制御部 7 セグメント LED は棒状に光る LED がデジタル数字の配置になっている部品です。今回の回路の C-551SR がそれです。C-551SR に内蔵された 7 つの LED に、それぞれ Raspberry Pi の GPIO を接続してあるため、各 LED を独立に制御できます。実は 7 セグメント LED にはドット(デシマルポイント)もあるため、合計 8 つの LED から構成されていますが、今回はドットは使いません。 どの GPIO を ON/OFF すればどの数字を表示できるかはぱっと見分かりにくいので、`kLed7SegTable` という定数で数字と GPIO の組み合わせを変換できるようにしました。 7 セグメント LED は数字を表示するためのものですが、工夫するとアルファベットも表示可能です。ただ、大文字と小文字が自在に表示できるわけではなく、例えば b や d などはどう頑張っても大文字の表現ができません。そこで、仕方なく小文字で表示します。 `Write7Seg()` は、与えられた文字を表示するための GPIO の組み合わせを `kLed7SegTable` により取得し、GPIO に信号を出力します。 ## 調判定部 調判定を行う `KeyIdentify()` は、鍵情報を入力して調を判定し、0 から 11 までの整数で出力します。入力する鍵情報 `note_count` は、ド(C)からシ(B)までの 12 要素の配列で、各要素には「それぞれの鍵が何回押されたか」という情報が記録されています。鍵が何回押されたかという情報は、MIDI のノート・オンメッセージを、各鍵について合計したものになっています。 もちろん、ピアノの鍵盤は 12 個よりもずっと多いので、`note_count` に集計する際は、オクターブの違いを無視するようにしました。MIDI における鍵番号は、中央のドが 60、オクターブ下のドが 48、……、最も低いドが 0、となっています。また、半音上がるごとに鍵番号が 1 だけ増えます。ド♯が 61、レが 62、……、シが 71、という具合に。したがって、鍵番号を 12 で割った余りを `note_count` の添え字とすれば、オクターブ違いを無視して鍵が押された回数を集計できるという仕組みです。 実際の調の判定アルゴリズムは後ほど説明しますが、今回作ったものは非常に簡易的なものです。もっと本格的な調判定を行うには、`note_count` だけでは不十分です。例えば同時に押された鍵の組み合わせや、その組み合わせが何回出現したか、あるいはそれらの時系列データなどを総合的に計算する方が、調の判定はより高度な物になります。 共同制作者によれば、今後、判定アルゴリズムをどんどん改良していきたい、と言っていました。楽しみですね。今回は電子工作をやったことがない共同制作者に電子工作を体験してもらうというのが趣旨だったので、アルゴリズムはとても簡素なものにとどめ、はんだ付けなどを楽しむというところに時間を使いました。 ## MIDI 受信部 MIDI 受信プログラムは実験に使った MIDI ピアノの出力を観察したり、MIDI の規格書を読んだりして作りました。ステータス・バイトを読み、その値にしたがってコマンドを解釈する、というのが基本路線になります。そこに、細々とした仕様が組み合わさって上記のような壮大な while ループになっています。 基本となるステータス・バイトはノート・オフ(0x80)、ノート・オン(0x90)、コントロール・チェンジ(0xb0)、アクティブ・センシング(0xfe)となります。なぜこの 4 つかというと、実験に用いた MIDI ピアノからの出力を見る限り、これら以外には出力されないからです。 MIDI 規格では、前 3 つのステータス・バイトは下位 4 ビットにチャンネル番号が入ることになっています。したがって、正確に書けば n をチャンネル番号として、0x8n、0x9n、0xbn、となります。しかし、観測する限りは常に n=0 となるようでした。もしかしたら楽器の設定を変えると n の値を調整できる可能性もありますが。 注意すべきは **ランニング・ステータス** という仕様です。少しでも通信データ量を減らすために、同じステータス・バイトが連続する場合はステータス・バイトを省略してよいことになっています。通常、ノート・オンやノート・オフはいくつも連続します。ドミソを同時に押せば、ノート・オンが 3 つ連続した後、ノート・オフが 3 つ連続することになります。この場合、ステータス・バイトは 3 つのノート・オンの前に 1 回、3 つのノート・オフの前に 1 回だけ送信すれば良く、合計 4 バイトの短縮が可能です。 さらに、ノート・オフを「ベロシティ 0 のノート・オン」で代替することにより、ランニング・ステータスの効果を高めることができます。今回の調判定ではノート・オフを無視しているので、どのみち関係ありませんでしたが。 アクティブ・センシングは最大 300ms に 1 回送信されてくることになっています。実測すると 250ms 程度の周期で送られてくることが分かりました。そこで、アクティブ・センシング信号を起点として、前回から 1 秒以上経ったかどうかの判定を行い、1 秒経過していたら `KeyIdentify()` を呼び出す、という処理を行うことにしました。このような定期的な処理はタイマー割り込みで行うのが普通ですが、アクティブ・センシングが来ることを前提とすれば、タイマー割り込みよりも簡単に実装できました。 ## 調判定アルゴリズム 以下、共同制作者からの寄稿です。 今回のプログラムでは、構成音が共通である平行調、たとえばハ長調とイ短調などの2調は区別せず、一括して長調の基音を表示することにしました。 `KeyIdentify()` 関数は、12 調(=7 音の組み合わせ 12 通り)のすべてについて合致率を算出し、12 調のうち最も一致率の高い調を検出する関数です。 ピアノの鍵盤をみると、1 オクターブにつき 7 つの白鍵と 5 つの黒鍵が存在します。これらを左から 0〜11 の番号を割り当てて取り扱うことにします。たとえばハ長調は、この 12 種類の音のうち、ドレミファソラシ(0、2、4、5、7、9、11)の 7 種類の音で構成されています。逆に言うと、のこり 5 種類の音、すなわち 5 黒鍵は、基本的に登場しません。 調判定では、12 調それぞれが、固有の 7 音の組み合わせを持つという特徴を利用し、候補を絞っていきます。具体的には、ハ長調は 7 音の組み合わせとしてドレミファソラシを持っている一方、ト長調はソラシドレミファ♯、という 7 音の組み合わせを持っています。ファとファ♯が異なる構成音となっていますね。それぞれの調が持つこの 7 つの組み合わせは、調に固有のもので、互いに異なります。以下に、それぞれの調を構成する 7 音の組み合わせ一覧を示します。 ![12 調の構成 7 音](https://camo.elchika.com/3876f5c086323e4d494e2d24eea69f80e9bff8da/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f33633531333063302d313261632d343965652d393965382d336435656237313939393136/) しかし、実際の音楽では、その調の構成 7 音から外れた音(臨時記号)が登場することもあるし、構成 7 音のすべてが最後まで揃わないこともあります。ですので、「1 音でも外れたら該当調の可能性を排除する」とか、「構成 7 音の全てが揃ったら判定する」などの方針を取るのは得策ではありません。 そこで、今回の方針としては、0〜11 それぞれの鍵の登場回数(ノート・オンの回数)を計上した `note_count` 配列を利用し、各調について「一致率 = 構成 7 音の合計出現回数 / すべてのノート・オンの合計回数」を計算して、一致率が最も高い調を表示することにしました。(一致率は 1 が最大です) [Hey Jude を演奏したデモ動画](https://www.youtube.com/watch?v=hQ5n5zbAjok) について。 2 小節目まで: 白鍵しか登場しておらず、C(ハ長調)または F(ヘ長調)であることが分かりますが、2 つのいずれであるかを確定することができません。一致率 = 1 の候補のうちのひとつである C が表示されています。 3 小節目以降: はじめて登場した B♭ の音により、F(ヘ長調)を導くことができました。表示が C から F に変わった様子が観察できます。 現状の調判定ロジックは上述のようなシンプルな作りですが、経過時間による重み付け、和声(同時に鳴っている音の組合せ)などを考慮することで、さらに精度をあげることができると思いますので、今後さらに挑戦してみたいと思います。