USB MIDI设备开发:让电子琴与Android对话

说实话,MIDI协议是我在Android开发中觉得最有趣的一块。它不像音频流那样需要处理大量数据,而是用一串串精炼的指令,就能让设备演奏出美妙的音乐。今天我们就来聊聊,怎么让Android手机变成电子琴的大脑。

MIDI协议基础:音乐世界的摩斯密码

MIDI全称是Musical Instrument Digital Interface,说白了就是乐器之间的通用语言。它不传输音频信号,只传输"指令"——比如"按下C键"、"音量调到80"、"换成长笛音色"。

我刚开始接触MIDI时,总觉得它很神秘。后来发现,它其实就是三种消息:音符消息控制变化消息系统专属消息。掌握了这三种,你就掌握了MIDI的精髓。

音符消息:最核心的指令

音符消息分为两种:Note On(按下琴键)和Note Off(松开琴键)。每条消息包含三个字节:

字节 含义 示例值
第1字节 状态字节(包含通道号) 0x90(通道1的Note On)
第2字节 音符编号(0-127) 0x3C(中央C)
第3字节 力度(0-127) 0x64(中等力度)

举个例子,发送 0x90 0x3C 0x64 就是告诉合成器:"在通道1上,以100的力度弹奏中央C"。松开时发送 0x80 0x3C 0x00

我的经验:Note Off消息的力度字节通常设为0,但有些老设备会忽略这个值。我遇到过一台Roland合成器,必须把力度设为0x40才能正常停止音符,否则会一直响。嗯,这就是兼容性问题。

控制变化消息:调节音色和效果

控制变化(Control Change,简称CC)消息用来调整各种参数,比如音量、混响、声像等。它的格式是:

0xB0 + 控制器编号 + 参数值

常用的控制器编号:

  • 7号:主音量(0-127)
  • 10号:声像(左-右)
  • 64号:延音踏板(0=松开,127=踩下)
  • 91号:混响深度

比如 0xB0 0x07 0x50 就是把通道1的音量调到80(约63%)。

避坑指南:我曾经在项目里用CC 7号控制音量,结果发现有些电子琴的响应曲线不是线性的。0x40(64)听起来只有最大音量的20%,而不是50%。后来我查了设备手册,才知道它用的是对数曲线。所以,一定要实测

系统专属消息:厂商的私房菜

系统专属消息(System Exclusive,简称SysEx)是留给厂商自定义的。它没有固定的格式,但通常以 0xF0 开头,以 0xF7 结束。比如切换音色、升级固件、查询设备信息等。

举个例子,切换YAMAHA合成器到"钢琴"音色:

0xF0 0x43 0x10 0x4C 0x00 0x00 0x7F 0x00 0xF7

每个厂商的SysEx格式都不一样,必须查阅设备手册。我建议你直接去官网下载MIDI实现文档,那是最权威的。

Android端USB MIDI Host实现

Android从4.2(API 17)开始支持USB Host模式。我们要做的,就是让手机作为Host,连接电子琴或合成器。

第一步:配置USB权限

AndroidManifest.xml 中声明:

<uses-feature android:name="android.hardware.usb.host" />

<activity ...>
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
    </intent-filter>
    <meta-data
        android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/device_filter" />
</activity>

res/xml/device_filter.xml 中指定设备:

<resources>
    <usb-device vendor-id="0x0499" product-id="0x1500" />
</resources>

vendor-id和product-id可以从设备管理器或 lsusb 命令获取。我一般先插上设备,用 dmesg 看日志,里面会显示这些ID。

第二步:获取USB设备并打开

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();

for (UsbDevice device : deviceList.values()) {
    if (device.getVendorId() == 0x0499 && device.getProductId() == 0x1500) {
        // 请求权限
        PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0,
                new Intent(ACTION_USB_PERMISSION), 0);
        usbManager.requestPermission(device, permissionIntent);
    }
}

拿到权限后,打开设备并找到MIDI接口:

UsbDeviceConnection connection = usbManager.openDevice(device);
UsbInterface usbInterface = device.getInterface(0); // MIDI通常在接口0
connection.claimInterface(usbInterface, true);

// 找到输入和输出端点
UsbEndpoint outEndpoint = usbInterface.getEndpoint(0); // 发送数据
UsbEndpoint inEndpoint = usbInterface.getEndpoint(1);  // 接收数据
注意:不是所有设备都把输入输出端点放在同一个接口。我遇到过一台Korg合成器,它的输入在接口0,输出在接口1。所以最好遍历所有接口和端点,根据端点方向(USB_DIR_IN/USB_DIR_OUT)来区分。

MIDI输入/输出处理

拿到端点后,就可以收发MIDI数据了。Android的USB MIDI使用USB MIDI Event Packet格式,每个包4个字节:

字节 含义
Byte 0 Cable Number (高4位) + Code Index Number (低4位)
Byte 1 MIDI状态字节
Byte 2 MIDI数据字节1
Byte 3 MIDI数据字节2

发送MIDI消息

public void sendMidiMessage(UsbDeviceConnection connection, UsbEndpoint outEndpoint,
                            byte status, byte data1, byte data2) {
    byte[] packet = new byte[4];
    packet[0] = (byte) 0x09; // Cable 0, Code Index = 0x9 (Note On)
    packet[1] = status;
    packet[2] = data1;
    packet[3] = data2;
    connection.bulkTransfer(outEndpoint, packet, packet.length, 100);
}

调用示例:发送Note On(通道1,中央C,力度100)

sendMidiMessage(connection, outEndpoint, (byte) 0x90, (byte) 0x3C, (byte) 0x64);

接收MIDI消息

接收需要开一个线程循环读取:

new Thread(() -> {
    byte[] buffer = new byte[64];
    while (isRunning) {
        int length = connection.bulkTransfer(inEndpoint, buffer, buffer.length, 50);
        if (length > 0) {
            for (int i = 0; i < length; i += 4) {
                byte cin = (byte) (buffer[i] & 0x0F);
                byte status = buffer[i + 1];
                byte data1 = buffer[i + 2];
                byte data2 = buffer[i + 3];
                // 根据CIN解析MIDI消息
                handleMidiMessage(status, data1, data2);
            }
        }
    }
}).start();

Code Index Number(CIN)告诉你怎么解析后面的字节。比如CIN=0x9表示Note On,CIN=0x8表示Note Off,CIN=0xB表示Control Change。

我的习惯:我会用一个队列来缓冲收到的MIDI消息,然后在主线程中处理。这样不会阻塞USB读取线程,也方便做时间戳标记。你想想看,如果用户快速弹奏一串音符,队列能保证顺序不乱。

实战:连接电子琴/合成器

好了,理论说完了,我们来做个实际例子。假设你有一台YAMAHA P-125电子琴,想用Android手机控制它的音色和音量。

步骤1:连接设备

用USB线连接手机和电子琴。手机端会弹出权限请求,点击"允许"。如果没弹出,检查一下设备过滤器里的vendor-id和product-id是否正确。

步骤2:发送SysEx切换音色

YAMAHA P-125切换音色的SysEx格式是:

0xF0 0x43 0x10 0x4C 0x00 0x00 0x7F 0x<音色号> 0xF7

比如切换到"电钢琴"音色(音色号=0x04):

byte[] sysEx = new byte[] {
    (byte) 0xF0, (byte) 0x43, (byte) 0x10, (byte) 0x4C,
    (byte) 0x00, (byte) 0x00, (byte) 0x7F, (byte) 0x04,
    (byte) 0xF7
};
connection.bulkTransfer(outEndpoint, sysEx, sysEx.length, 100);

步骤3:实时控制音量

用CC 7号控制主音量:

// 音量滑块变化时调用
public void onVolumeChanged(int volume) {
    // volume范围0-100,映射到0-127
    int midiVolume = (int) (volume * 1.27);
    sendMidiMessage(connection, outEndpoint, (byte) 0xB0, (byte) 0x07, (byte) midiVolume);
}

步骤4:接收键盘输入

当你在电子琴上弹奏时,手机会收到Note On/Off消息。你可以把这些消息显示在屏幕上,或者录制下来:

private void handleMidiMessage(byte status, byte data1, byte data2) {
    int channel = status & 0x0F;
    int messageType = status & 0xF0;

    switch (messageType) {
        case 0x90: // Note On
            if (data2 > 0) {
                Log.d("MIDI", "通道" + channel + " 按下音符 " + data1 + " 力度 " + data2);
                // 可以在这里记录音符
            } else {
                // 力度为0等同于Note Off
                Log.d("MIDI", "通道" + channel + " 松开音符 " + data1);
            }
            break;
        case 0x80: // Note Off
            Log.d("MIDI", "通道" + channel + " 松开音符 " + data1);
            break;
        case 0xB0: // Control Change
            Log.d("MIDI", "通道" + channel + " 控制器 " + data1 + " 值 " + data2);
            break;
    }
}
避坑指南:有些电子琴的Note On消息中,力度为0时表示松开,而不是发送Note Off。我刚开始没注意这个,结果程序里同时处理了Note On(力度0)和Note Off,导致一个音符被记录两次。后来我统一处理:力度>0才是按下,否则都是松开

知识体系总览

下面这张图总结了本章的核心内容,从MIDI协议到Android实现,再到实战应用:

USB MIDI设备开发知识体系 MIDI协议基础 音符消息 控制变化 系统专属 Android USB MIDI Host USB权限 打开设备 收发数据 实战:连接电子琴/合成器 切换音色(SysEx) 控制音量(CC) 接收键盘输入 录制/回放

从图中可以看到,整个开发流程是:理解MIDI协议 → 在Android上实现USB Host → 应用到具体设备。每一步都有坑,但掌握了核心逻辑,你就能举一反三。

好了,关于USB MIDI开发就讲到这里。记住,MIDI协议本身并不复杂,复杂的是不同设备的兼容性。多测试、多看设备手册,慢慢你就能摸清它们的脾气。

公众号:蓝海资料掘金营,微信deep3321