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。
控制变化消息:调节音色和效果
控制变化(Control Change,简称CC)消息用来调整各种参数,比如音量、混响、声像等。它的格式是:
0xB0 + 控制器编号 + 参数值
常用的控制器编号:
- 7号:主音量(0-127)
- 10号:声像(左-右)
- 64号:延音踏板(0=松开,127=踩下)
- 91号:混响深度
比如 0xB0 0x07 0x50 就是把通道1的音量调到80(约63%)。
系统专属消息:厂商的私房菜
系统专属消息(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); // 接收数据
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。
实战:连接电子琴/合成器
好了,理论说完了,我们来做个实际例子。假设你有一台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;
}
}
知识体系总览
下面这张图总结了本章的核心内容,从MIDI协议到Android实现,再到实战应用:
从图中可以看到,整个开发流程是:理解MIDI协议 → 在Android上实现USB Host → 应用到具体设备。每一步都有坑,但掌握了核心逻辑,你就能举一反三。
好了,关于USB MIDI开发就讲到这里。记住,MIDI协议本身并不复杂,复杂的是不同设备的兼容性。多测试、多看设备手册,慢慢你就能摸清它们的脾气。