【C++】WinMMでMIDIキーボード入力を取得する方法を解説

サムネイル画像 C++

今回は、C++で「MIDIキーボードの入力をWinMMで取得する方法」について解説していきます。

「MIDIキーボードの入力を自分のプログラムで使いたい」「音楽アプリやゲームでMIDI対応したい」と思ったことはありませんか?

この記事を読み終えると、あなたはWinMMを使ったMIDI入力の取得方法をマスターできると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

MIDIとは?

MIDI(Musical Instrument Digital Interface)は、電子楽器やコンピューター間で音楽情報をやり取りするための規格です。

MIDIキーボードから送られるデータには、以下のような情報が含まれます。

  • ノート情報:どの鍵盤が押された/離された
  • ベロシティ:鍵盤を押す強さ(0~127)
  • コントロールチェンジ(CC):つまみやスライダーの値
  • ピッチベンド:音程を変化させるホイールの値

完全版のコードについて

この記事では、実装の要点を解説します。

完全なソースコードは、以下のGitHubリポジトリで公開しています。

🔗 https://github.com/it-kashiros/midi_winmm

リポジトリには以下のファイルが含まれています。

  • midi_input.h:MIDIクラスのヘッダー
  • midi_input.cpp:MIDIクラスの実装
  • main.cpp:デバッグ用のモニタープログラム

Visual Studioで開いて、すぐにビルド・実行できます。

基本的な使い方

WinMMでMIDI入力を取得する基本的な流れは以下の通りです。

  1. midiInOpen()でMIDIデバイスを開く
  2. コールバック関数でMIDIメッセージを受信
  3. 受信したメッセージを解析して状態を更新

必要なヘッダーとライブラリ

▼midi_input.h(抜粋)

#pragma once
#include <windows.h>
#include <mmsystem.h>

#pragma comment(lib, "winmm.lib")

コントローラーと同じく、winmm.libをリンクするだけです。

実装のポイント

1. MIDIデバイスのオープン

MIDIデバイスを開くには、midiInOpen()を使います。

▼midi_input.cpp(抜粋)

bool MidiInput::Initialize() {
    // デバイスがなければ失敗
    if (midiInGetNumDevs() == 0) {
        return false;
    }
    
    // 最初のデバイスを開く
    MMRESULT result = midiInOpen(
        &s_hMidiIn,
        0,  // デバイスID(0が最初のデバイス)
        reinterpret_cast<DWORD_PTR>(MidiCallback),
        0,
        CALLBACK_FUNCTION
    );
    
    if (result != MMSYSERR_NOERROR) {
        return false;
    }
    
    // 入力開始
    midiInStart(s_hMidiIn);
    return true;
}

midiInStart()を呼ぶことで、MIDIメッセージの受信が開始されます。

2. コールバック関数でメッセージ受信

MIDIメッセージは、コールバック関数で受信します。

void CALLBACK MidiInput::MidiCallback(
    HMIDIIN hMidiIn, UINT wMsg,
    DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
    
    if (wMsg == MIM_DATA) {
        // MIDIメッセージを処理
        ProcessMessage(static_cast<DWORD>(dwParam1), static_cast<DWORD>(dwParam2));
    }
}

dwParam1MIDIメッセージの生データが入っています。

3. MIDIメッセージの解析

MIDIメッセージは、ステータスバイトとデータバイトで構成されています。

void MidiInput::ProcessMessage(DWORD param1, DWORD param2) {
    BYTE status = param1 & 0xFF;
    BYTE byte1 = (param1 >> 8) & 0xFF;
    BYTE byte2 = (param1 >> 16) & 0xFF;
    
    BYTE channel = status & 0x0F;       // チャンネル(0-15)
    BYTE statusType = status & 0xF0;   // メッセージタイプ
    
    switch (statusType) {
        case 0x90:  // ノートオン
            if (byte2 > 0) {  // ベロシティが0ならノートオフ
                s_state.noteOn[byte1] = true;
                s_state.velocity[byte1] = byte2;
            }
            break;
            
        case 0x80:  // ノートオフ
            s_state.noteOn[byte1] = false;
            s_state.velocity[byte1] = 0;
            break;
            
        case 0xB0:  // コントロールチェンジ
            s_state.cc[byte1] = byte2;
            break;
            
        case 0xE0:  // ピッチベンド
            s_state.pitchBend = ((byte2 << 7) | byte1) - 8192;
            break;
    }
}

ステータスバイトの上位4ビットでメッセージの種類を判別できます。

主なMIDIメッセージ

ステータスメッセージ説明
0x90ノートオン鍵盤が押された
0x80ノートオフ鍵盤が離された
0xB0コントロールチェンジつまみ・スライダーの値
0xE0ピッチベンドピッチホイールの値
0xC0プログラムチェンジ音色の切り替え
0xD0チャンネルプレッシャー鍵盤全体の圧力

実際の使い方

実装したクラスは、以下のように使います。

▼使用例

#include "midi_input.h"

int main() {
    // 初期化
    if (!MidiInput::Initialize()) {
        printf("MIDIデバイスが見つかりません\n");
        return 1;
    }
    
    while (true) {
        MidiInput::Update();
        
        // 特定のノートが押されているか
        if (MidiInput::IsNoteOn(60)) {  // C4(ド)
            printf("ドが押されています\n");
        }
        
        // 押されている鍵盤の数
        int count = MidiInput::GetNoteOnCount();
        printf("押されている鍵盤: %d\n", count);
        
        // モジュレーションホイール(CC1)の値
        float mod = MidiInput::GetCCFloat(1);  // 0.0~1.0
        
        // ピッチベンドの値
        int bend = MidiInput::GetPitchBend();  // -8192~8191
        
        Sleep(16);
    }
    
    MidiInput::Finalize();
    return 0;
}

デバッグモニター

リポジトリのmain.cppには、MIDIキーボードの入力をリアルタイムで表示するモニターが実装されています。

▼実行すると、以下のような画面が表示されます

===============================================================================
                          MIDI INPUT DEBUG                                     
===============================================================================
 Device: USB MIDI Keyboard
-------------------------------------------------------------------------------
 Last: NoteOn   Ch: 1  C4(60) Vel:82
-------------------------------------------------------------------------------
 Piano (A0-E4):
 ___.__._____.__._____.__._____#__._____.__._____.__._
 Piano (F4-C8):
 ___.__._____.__._____.__._____.__._____.__._____.__._
-------------------------------------------------------------------------------
 Notes: 1  Low: C4(60)  High: C4(60)
 Active: C4
-------------------------------------------------------------------------------
 Pitch:      0 | Mod(CC1):   0 | Vol(CC7): 127 | Sustain: OFF
===============================================================================
 ESC: Exit

88鍵ピアノの鍵盤状態が視覚化され、押された鍵盤が#で表示されます。

ノート番号と音名の対応

MIDIでは、ノート番号0~127で音程を表します。

  • 60:C4(中央のド)
  • 69:A4(440Hz のラ)
  • 21:A0(88鍵ピアノの最低音)
  • 108:C8(88鍵ピアノの最高音)

リポジトリのコードには、ノート番号を音名に変換する関数が用意されています。

const char* MidiInput::GetNoteName(BYTE note) {
    static char buf[8];
    int octave = (note / 12) - 1;
    int n = note % 12;
    sprintf_s(buf, sizeof(buf), "%s%d", NOTE_NAMES[n], octave);
    return buf;
}

// 使用例
printf("%s\n", MidiInput::GetNoteName(60));  // "C4"

便利な機能

実装したクラスには、以下のような便利な機能があります。

押されている鍵盤の情報

// 何か鍵盤が押されているか
bool isAny = MidiInput::IsAnyNoteOn();

// 押されている鍵盤の数
int count = MidiInput::GetNoteOnCount();

// 最も低い音
int low = MidiInput::GetLowestNote();

// 最も高い音
int high = MidiInput::GetHighestNote();

コントロールチェンジ(CC)

MIDIキーボードのつまみやスライダーの値を取得できます。

// CC1(モジュレーションホイール)
BYTE mod = MidiInput::GetCC(1);         // 0~127
float modF = MidiInput::GetCCFloat(1);  // 0.0~1.0

// CC7(ボリューム)
BYTE vol = MidiInput::GetCC(7);

// CC64(サステインペダル)
BYTE sustain = MidiInput::GetCC(64);    // 0-63: OFF, 64-127: ON

ピッチベンド

// ピッチベンドの値
int bend = MidiInput::GetPitchBend();       // -8192~8191
float bendF = MidiInput::GetPitchBendFloat();  // -1.0~1.0

注意点

1. コールバック関数の制約

MIDIコールバック関数は、システムのスレッドから呼ばれます

そのため、以下のような処理は避けましょう。

  • ❌ 時間のかかる処理
  • ❌ ファイル入出力
  • ❌ ウィンドウ操作
  • ✅ 状態の更新のみ

2. デバイスの切断

MIDIキーボードが切断された場合MIM_CLOSEメッセージが届きます。

リポジトリのコードでは、これを検知してconnectedフラグを更新しています。

3. ノートオフの扱い

MIDIでは、ベロシティが0のノートオンをノートオフとして扱うことがあります。

case 0x90:  // ノートオン
    if (byte2 > 0) {
        // 本当のノートオン
        s_state.noteOn[byte1] = true;
    } else {
        // ベロシティ0 = ノートオフ
        s_state.noteOn[byte1] = false;
    }
    break;

動作確認

リポジトリダウンロードして、Visual Studioで開いてください。

  1. Visual Studioでプロジェクトを開く
  2. コントローラーをPCに接続
  3. ビルド&実行(F5)
  4. コントローラーを操作して、入力が表示されることを確認

ESCキーで終了できます。

まとめ

WinMMを使ったMIDI入力取得についてまとめます。

  • WinMM:Windows標準のマルチメディアAPI
  • midiInOpen()でデバイスを開く
  • コールバック関数でメッセージを受信
  • ステータスバイトでメッセージの種類を判別
  • ノートオン・オフ、CC、ピッチベンドなどを取得
  • 完全なコードはGitHubリポジトリで公開中

これでWinMMを使ったMIDI入力の取得はバッチリですね!

GitHubのコードを参考に、音楽アプリやゲームにMIDI対応を組み込んでみてください。

ここまで読んでくださり、ありがとうございました。

この記事が皆様の学習に役立てば幸いです。