MIDI キーボードの入力を受け取る

Recipe ID: hw-014

電子ピアノや MIDI コントローラーからの入力を取得します。midir クレートを使用します。

1. 依存関係の追加

src-tauri ディレクトリで以下のコマンドを実行してください。

cd src-tauri
cargo add midir

2. Rust 側の実装 (lib.rs)

MIDI 入力をコールバック形式で受け取り、フロントエンドにイベントとして送信します。

// src-tauri/src/lib.rs
use midir::{Ignore, MidiInput};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, State};

// MIDI 接続を維持するための状態
struct MidiState {
    conn: Mutex<Option<midir::MidiInputConnection<()>>>,
}

// 利用可能な MIDI 入力ポートの一覧を取得
#[tauri::command]
fn list_midi_ports() -> Result<Vec<String>, String> {
    let midi_in = MidiInput::new("Port Scanner").map_err(|e| e.to_string())?;
    let ports = midi_in.ports();

    let names: Vec<String> = ports
        .iter()
        .filter_map(|p| midi_in.port_name(p).ok())
        .collect();

    Ok(names)
}

// 指定したポートに接続して MIDI 入力の監視を開始
#[tauri::command]
fn start_midi_listener(
    app: AppHandle,
    state: State<MidiState>,
    port_index: usize,
) -> Result<String, String> {
    let mut midi_in = MidiInput::new("My MIDI Input").map_err(|e| e.to_string())?;
    midi_in.ignore(Ignore::None);

    let ports = midi_in.ports();
    let port = ports.get(port_index).ok_or("Invalid port index")?;
    let port_name = midi_in.port_name(port).unwrap_or_default();

    println!("Connecting to {}", port_name);

    let app_handle = app.clone();

    // 接続とコールバック定義
    let conn_in = midi_in
        .connect(
            port,
            "tauri-midi-input",
            move |_stamp, message, _| {
                // メッセージ (例: [144, 60, 100] -> Note On, Middle C, Velocity 100)
                let _ = app_handle.emit("midi-message", message.to_vec());
            },
            (),
        )
        .map_err(|e| e.to_string())?;

    // State に保存して接続を維持
    *state.conn.lock().unwrap() = Some(conn_in);

    Ok(format!("Connected to {}", port_name))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .manage(MidiState {
            conn: Mutex::new(None),
        })
        .invoke_handler(tauri::generate_handler![
            list_midi_ports,
            start_midi_listener
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

3. フロントエンドの実装 (TypeScript)

Rust 側で定義したコマンドを呼び出し、イベントを購読します。

import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

// 利用可能なポートの一覧を取得
async function listPorts() {
  const ports = await invoke<string[]>('list_midi_ports');
  console.log('Available ports:', ports);
  return ports;
}

// 指定したインデックスのポートで受信を開始
async function startListening(index: number) {
  try {
    const result = await invoke<string>('start_midi_listener', { portIndex: index });
    console.log(result);
  } catch (error) {
    console.error('Failed to start listener:', error);
  }
}

// イベントリスナーの設定
async function setupMidiListener() {
  await listen<number[]>('midi-message', (event) => {
    // ペイロードは number[] (例: [144, 60, 100])
    console.log('Received MIDI:', event.payload);
  });
}

4. Web MIDI API との比較

フロントエンドの Web MIDI API (navigator.requestMIDIAccess) も強力で、Tauri から問題なく使用できます。特別な理由(バックエンドで信号加工したい、OS固有の挙動が必要など)がない限り、Web MIDI API を使うほうが簡単な場合が多いです。

ただし、Linux では Web MIDI API がうまく動作しないケース(権限周り)があるため、Rust 側での実装はフォールバックとして有用です。