今こそMIDI 2.0に取り組むべき理由

Appleプラットフォームで音楽アプリを開発しているなら、MIDI 2.0はもはや将来の話ではありません。macOS 11以降、CoreMIDIはMIDI 2.0をネイティブにサポートしており、対応ハードウェアとソフトウェアのエコシステムは急速に成長しています。

MIDI 2.0は、40年以上の歴史を持つMIDI 1.0規格に対して革新的な改善をもたらします:

  • 32ビットのベロシティとコントローラ解像度 — MIDI 1.0の7ビット値と比較して65,536倍の精度
  • パーノートコントローラ — 個々のノートにピッチベンド、プレッシャー、カスタムパラメータを適用可能
  • Device Discovery(MIDI-CI) — デバイスが自動的に機能をネゴシエーション
  • Property Exchange — 独自SysExなしでデバイスパラメータの読み書きが可能
  • 双方向通信 — デバイスがホストに応答可能

しかし、AppleのCoreMIDIが提供するのはトランスポート層のみ、つまり生のUMPパケットの送受信だけです。Discovery、Property Exchange、プロトコルネゴシエーションなど、それ以上のレイヤーは開発者自身が実装する必要があります。ここでMIDI2Kitの出番です。

MIDI2Kitがプロトコルの複雑さを処理するので、あなたは優れた音楽体験の構築に集中できます。

Swift開発者のためのMIDI 2.0基礎知識

Universal MIDI Packet(UMP)

MIDI 2.0は、MIDI 1.0のバイトストリーム形式をUniversal MIDI Packet(UMP)という新しいパケット形式に置き換えます。すべてのUMPは32ビットアラインされ、1〜4ワード(32ビット整数)の長さを持ちます。最初のワードの先頭4ビットがメッセージタイプをエンコードし、パケットの長さとセマンティクスを決定します。

メッセージタイプの概要:

  • Type 0x0 — ユーティリティメッセージ(JR Timestamp、JR Clock)
  • Type 0x1 — System CommonとSystem Real Time(1ワード)
  • Type 0x2 — MIDI 1.0チャンネルボイス(1ワード、レガシー互換)
  • Type 0x3 — データメッセージ / SysEx 7ビット(2ワード)
  • Type 0x4 — MIDI 2.0チャンネルボイス(2ワード、新仕様の中核)
  • Type 0x5 — データメッセージ / SysEx 8ビット(2ワード)

MIDI2Kitを使えば、これらのビットパターンを手動でパースする必要はありません。UMPParserUMPBuilder型がエンコードとデコードを処理します。

MIDI-CI:インテリジェンス層

MIDI-CI(Capability Inquiry)は、UMPトランスポートの上に位置するプロトコル層です。デバイスの相互検出、機能ネゴシエーション、プロパティ交換、プロファイル設定を可能にします。MIDI-CIメッセージはUMPパケット内のSysExとして転送されます。

MIDI-CIは3つの主要な機能領域を定義します:

  1. Discovery — デバイスが存在と機能を通知(メーカー、モデル、サポート機能)
  2. Property Exchange(PE) — デバイスパラメータの読み書きのためのJSON形式のリクエスト/レスポンスプロトコル
  3. Profile Configuration — 標準化された動作プロファイル(Piano Profile、Drawbar Organ Profileなど)

プロジェクトのセットアップ

MIDI2KitはSwift Package Managerで統合します。Package.swiftに追加してください:

// Package.swift
import PackageDescription

let package = Package(
    name: "MyMIDIApp",
    platforms: [.macOS(.v14), .iOS(.v17)],
    dependencies: [
        .package(
            url: "https://github.com/midi2kit/MIDI2Kit-SDK.git",
            from: "1.0.0"
        )
    ],
    targets: [
        .executableTarget(
            name: "MyMIDIApp",
            dependencies: [
                .product(name: "MIDI2Kit", package: "MIDI2Kit-SDK")
            ]
        )
    ]
)

Xcodeの場合:File → Add Package Dependenciesからhttps://github.com/midi2kit/MIDI2Kit-SDK.gitを貼り付け、MIDI2Kitプロダクトを選択します。

MIDI2Kitは外部依存関係ゼロで、Appleのシステムフレームワーク(CoreMIDI、Foundation)のみに依存します。iOS 17+、macOS 14+、tvOS 17+、watchOS 10+、visionOS 1.0+をサポートしています。

MIDI2Kitはビルド時間ゼロの統合のためにプリビルドXCFrameworkとしても提供されています。詳細はGitHubリポジトリをご覧ください。

UMPメッセージの送受信

MIDI2Kitクライアントの作成

MIDI2Kitの中心的な型はMIDI2Clientです。MIDIサブシステムへの接続を管理するSwift actorです。初期化と開始の方法:

import MIDI2Kit

// アプリ名でクライアントを作成
let client = try MIDI2Client(name: "MyMIDIApp")

// クライアントを開始 — CoreMIDIに接続し、
// MIDI-CI Discoveryを自動的に開始します
try await client.start()

UMPイベントのリスニング

MIDI2KitはAsyncSequenceベースのイベントストリームですべてのイベントを配信します。Swift Concurrencyと自然に統合されます:

// イベントストリームを作成
let eventStream = await client.makeEventStream()

// 構造化された並行処理でイベントを処理
Task {
    for await event in eventStream {
        switch event {
        case .midi2ChannelVoice(let msg):
            switch msg.status {
            case .noteOn:
                print("ノートオン: \(msg.note) ベロシティ: \(msg.velocity32)")
            case .noteOff:
                print("ノートオフ: \(msg.note)")
            case .controlChange:
                print("CC \(msg.index): \(msg.value32)")
            default:
                break
            }

        case .deviceDiscovered(let device):
            print("新しいデバイス: \(device.displayName)")

        case .deviceDisappeared(let muid):
            print("デバイス切断: \(muid)")

        default:
            break
        }
    }
}

MIDI 2.0メッセージの送信

UMPBuilderを使ってメッセージを構築し、宛先に送信します:

// 高解像度ベロシティでMIDI 2.0ノートオンメッセージを構築
let noteOn = UMPBuilder.midi2NoteOn(
    group: 0,
    channel: 0,
    note: 60,           // ミドルC
    velocity: .midi2(0xC000_0000),  // 32ビット解像度で約75%
    attributeType: .none
)

// 特定の宛先に送信
try await client.send(noteOn, to: destinationID)

// パーノートピッチベンドを構築(MIDI 2.0独自の機能)
let perNoteBend = UMPBuilder.midi2PerNotePitchBend(
    group: 0,
    channel: 0,
    note: 60,
    value: 0x6000_0000  // わずかな上方ベンド
)

try await client.send(perNoteBend, to: destinationID)

MIDI 1.0と2.0の変換

MIDI2KitはMIDI 1.0とMIDI 2.0メッセージ形式の双方向変換を提供します。MIDI 1.0のみをサポートするデバイスとの通信に不可欠です:

// MIDI 1.0メッセージをMIDI 2.0に変換
let midi1NoteOn = UMPBuilder.midi1NoteOn(
    group: 0, channel: 0, note: 60, velocity: 100
)
let midi2Equivalent = UMPConverter.toMIDI2(midi1NoteOn)

// MIDI 2.0メッセージをMIDI 1.0に変換
let downscaled = UMPConverter.toMIDI1(midi2Equivalent)

// ベロシティはスケーリングされます:MIDI 2.0 32ビット → MIDI 1.0 7ビット
// 精度の損失を最小限にするため適切な丸め処理が行われます

MIDI-CIによるDevice Discovery

MIDI 2.0の最も強力な機能の一つが自動デバイス検出です。MIDI-CI Discoveryを使えば、アプリがすべてのMIDI 2.0対応デバイスを見つけ、その機能を学習し、どのように対話するかを決定できます — すべてユーザーの介入なしに。

Discoveryの仕組み

client.start()を呼び出すと、MIDI2Kitは自動的にMIDI-CI Discoveryプロセスを開始します:

  1. すべての接続済みMIDIポートにDiscoveryメッセージがブロードキャストされます
  2. MIDI 2.0対応デバイスがMUID(一意の識別子)、メーカー情報、サポート機能とともに応答します
  3. MIDI2Kitが各デバイスにDiscoveredDeviceオブジェクトを割り当て、イベントストリームで配信します
  4. デバイスが切断されるとInvalidateMUIDメッセージが送信され、MIDI2KitがdeviceDisappearedイベントを発行します
// デバイスを検出して機能を確認
for await event in await client.makeEventStream() {
    if case .deviceDiscovered(let device) = event {
        print("デバイス: \(device.displayName)")
        print("  メーカー: \(device.manufacturer)")
        print("  モデル: \(device.model)")
        print("  MUID: \(device.muid)")
        print("  PE対応: \(device.supportsPropertyExchange)")
        print("  プロファイル対応: \(device.supportsProfileConfiguration)")

        // いつでも現在検出済みのすべてのデバイスを取得可能
        let allDevices = await client.discoveredDevices
        print("オンラインデバイス数: \(allDevices.count)")
    }
}

デバイスのフィルタリングと管理

実際のアプリケーションでは、機能でデバイスをフィルタリングしてデバイスリストを管理する必要があります:

actor DeviceManager {
    private var devices: [MUID: DiscoveredDevice] = [:]

    func handle(_ event: MIDI2Event) {
        switch event {
        case .deviceDiscovered(let device):
            devices[device.muid] = device

        case .deviceDisappeared(let muid):
            devices.removeValue(forKey: muid)

        default:
            break
        }
    }

    /// Property Exchangeをサポートするデバイスのみを返す
    var peCapableDevices: [DiscoveredDevice] {
        devices.values.filter { $0.supportsPropertyExchange }
    }
}

Property Exchangeの基礎

Property Exchange(PE)は、デバイスパラメータの読み書きのためのMIDI-CIのリクエスト/レスポンスプロトコルです。MIDIデバイス向けのRESTful APIと考えてください — JSONライクなペイロードを使ってリソースのGETやSETができ、すべてUMP内のSysExとして転送されます。

デバイスプロパティの読み取り

最も一般的なPE操作はDeviceInfoリソースの読み取りです。すべてのPE対応デバイスがサポートする必要があります:

// 基本的なデバイス情報を取得
let deviceInfo = try await client.getDeviceInfo(from: device.muid)

print("メーカー: \(deviceInfo.manufacturerName ?? "不明")")
print("製品名: \(deviceInfo.productName ?? "不明")")
print("ファームウェア: \(deviceInfo.firmwareVersion ?? "不明")")
print("シリアル: \(deviceInfo.serialNumber ?? "N/A")")

リソースリストの閲覧

デバイスはクエリや変更が可能なリソースのリストを公開できます:

// デバイスの利用可能なリソースリストを取得
let resources = try await client.getResourceList(from: device.muid)

for resource in resources {
    print("リソース: \(resource.resource)")
    print("  GET可能: \(resource.canGet)")
    print("  SET可能: \(resource.canSet)")
    print("  購読可能: \(resource.canSubscribe)")
}

// 特定のリソースを読み取り
let channelList = try await client.getProperty(
    resource: "ChannelList",
    from: device.muid
)

// プロパティ値を設定
try await client.setProperty(
    resource: "ProgramName",
    value: ["name": "My Custom Patch"],
    on: device.muid
)

プロパティ変更の購読

PEはサブスクリプションをサポートしており、デバイスのプロパティが変更されたときにリアルタイムで更新を受け取れます:

// リソースの変更を購読
let subscription = try await client.subscribe(
    to: "CurrentProgram",
    on: device.muid
)

// AsyncSequenceとして更新を受信
Task {
    for await update in subscription.updates {
        print("プログラム変更: \(update.value)")
    }
}

// 終了時に購読解除
try await subscription.cancel()

接続の処理:USB、Bluetooth、ネットワーク

MIDI2KitはCoreMIDIがサポートするすべてのトランスポートタイプで動作します。UMPアーキテクチャの優れた点は、デバイスの接続方法に関係なくコードを変更する必要がないことです。

USB MIDI

USB MIDIデバイスは最も簡単で、CoreMIDIを通じて自動的に表示されます。MIDI2Kitはプラグインされるとすぐに検出し、MIDI-CIをサポートしている場合はdeviceDiscoveredイベントを発行します。

Bluetooth MIDI

iOSでBluetooth MIDIを使用するには、接続UIを表示する必要があります。MIDI2Kitはこのためのヘルパーを提供しています:

// システムのBluetooth MIDIピッカーを表示(iOS)
#if os(iOS)
import CoreAudioKit

let picker = CABTMIDICentralViewController()
present(picker, animated: true)

// 接続されると、デバイスはMIDI2Kitのイベントストリームに
// 自動的に表示されます — 追加のセットアップは不要
#endif

ネットワークMIDI

macOSはCoreMIDIを通じてネットワークMIDIセッションをサポートしています。MIDI2Kitはネットワークのエンドポイントも他のエンドポイントと同様に扱います — 確立されると、DiscoveryとProperty Exchangeは同じように動作します:

// ネットワークMIDIセッションはmacOSの
// Audio MIDI設定で管理されます。
// セッションが確立されると、エンドポイントは
// 他のMIDIデバイスと同様にMIDI2Kitに表示されます。

// 必要に応じてトランスポートタイプを識別可能:
let endpoint = await client.endpoints.first {
    $0.displayName == "Network Session 1"
}

if let endpoint {
    print("トランスポート: \(endpoint.transportType)")
    // .usb, .bluetooth, .network など
}

よく使うパターンとベストプラクティス

1. 構造化された並行処理を活用する

MIDI2KitはSwiftのactorとAsyncSequenceをベースに構築されています。構造化された並行処理を活用して、よくある落とし穴を避けましょう:

// 推奨:並列操作にはTaskGroupを使用
try await withThrowingTaskGroup(of: Void.self) { group in
    let devices = await client.discoveredDevices

    for device in devices where device.supportsPropertyExchange {
        group.addTask {
            let info = try await client.getDeviceInfo(from: device.muid)
            print("\(device.displayName): \(info.productName ?? "不明")")
        }
    }
}

// 推奨:イベント処理をクリーンにキャンセル
let task = Task {
    for await event in await client.makeEventStream() {
        handleEvent(event)
    }
}

// シャットダウン時:
task.cancel()
try await client.stop()

2. デバイス切断を適切に処理する

デバイスはいつでも切断される可能性があります。常にdeviceDisappearedイベントを処理してクリーンアップしましょう:

for await event in await client.makeEventStream() {
    switch event {
    case .deviceDiscovered(let device):
        await deviceManager.add(device)
        // Discovery後にのみPEクエリを開始
        if device.supportsPropertyExchange {
            Task { await fetchDeviceDetails(device) }
        }

    case .deviceDisappeared(let muid):
        await deviceManager.remove(muid)
        // このデバイスの保留中のPEトランザクションをキャンセル
        await cancelPendingRequests(for: muid)

    default:
        break
    }
}

3. レート制限を尊重する

MIDI-CIトランザクションはSysExベースで帯域幅を多く消費する可能性があります。高速なPEリクエストの連発は避けましょう:

// 非推奨:連続した高速リクエスト
for resource in resources {
    let value = try await client.getProperty(
        resource: resource.name, from: muid
    )
}

// 推奨:短い遅延でレート制限
for resource in resources {
    let value = try await client.getProperty(
        resource: resource.name, from: muid
    )
    try await Task.sleep(for: .milliseconds(50))
}

4. テスト用にResponderを構築する

MIDI2KitのMIDI2ResponderClientを使えば、仮想MIDI 2.0デバイスを作成できます — ハードウェアなしでのテストに最適です:

// 仮想MIDI 2.0レスポンダーを作成
let responder = try MIDI2ResponderClient(
    name: "Virtual Synth",
    manufacturer: "MyCompany",
    model: "TestSynth"
)

// 型付きリソースを追加
await responder.addResource("DeviceStatus") {
    ComputedResource(getTyped: { context in
        DeviceStatus(
            battery: 85,
            firmware: "2.1.0",
            activeVoices: 12
        )
    })
}

// 設定可能なリソースを追加
await responder.addResource("ProgramName") {
    StoredResource(
        initialValue: ProgramInfo(name: "Init", bank: 0, number: 0),
        onSet: { newValue, context in
            print("プログラムが変更されました: \(newValue.name)")
            return .ok
        }
    )
}

// 開始 — レスポンダーは他のMIDI 2.0アプリから見えるようになります
try await responder.start()

5. 型システムを活用する

MIDI2Kitは可能な限り強い型付けの値を使用します。生のバイトよりも型付きAPIを優先しましょう:

// 生の整数よりも型付きベロシティを優先
let velocity = Velocity.midi2(0xB000_0000)  // 約69%
let velocity7 = Velocity.midi1(100)         // 7ビット
let velocityMax = Velocity.max              // フルベロシティ

// オクターブ情報付きのノートナンバーを使用
let note = NoteNumber(60)  // ミドルC
print(note.name)           // "C4"
print(note.frequency)      // 261.63 Hz

// 型安全にメッセージを構築
let msg = UMPBuilder.midi2NoteOn(
    group: 0,
    channel: 0,
    note: note,
    velocity: velocity,
    attributeType: .none
)

次のステップ

このガイドでは、SwiftとMIDI2Kitを使ったMIDI 2.0開発の基礎をカバーしました。次のステップとして:

MIDI 2.0は、楽器デバイスの通信方法における世代交代です。MIDI2Kitを使えば、純粋なSwiftで今日からMIDI 2.0の開発を始められます — Cブリッジなし、コールバックピラミッドなし、スレッドセーフティの心配なし。

MIDI 2.0で開発を始めませんか?

MIDI2Kitはオープンソース、MITライセンスで、本番環境に対応しています。

GitHubで見る はじめ方ガイド

関連記事

← すべての記事