基于ALSA的MIDI技术:如何检测虚拟MIDI端口的连接订阅状态
要检测你创建的虚拟MIDI端口是否有外部设备通过aconnect或Patchage连接,核心是查询ALSA Sequencer的订阅关系——当外部设备连接到你的端口时,会在ALSA seq系统中生成一条订阅记录。下面提供两种实用方案:
方案1:主动查询当前连接状态
这种方法适合在生成MIDI事件前,一次性检查是否有设备连接。步骤如下:
- 先获取你的客户端ID和端口ID(
snd_seq_create_simple_port的返回值就是端口ID) - 通过
subs结构体指定查询目标,遍历所有订阅到你端口的外部设备
代码示例
#include <alsa/asoundlib.h> #include <stdio.h> int main() { snd_seq_t *handle; int port_id; int client_id; // 初始化seq客户端(假设你已经完成这一步) if (snd_seq_open(&handle, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { fprintf(stderr, "Failed to open ALSA sequencer\n"); return 1; } snd_seq_set_client_name(handle, "My MIDI App"); // 创建你提到的端口 port_id = snd_seq_create_simple_port(handle, "my port", SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC); if (port_id < 0) { fprintf(stderr, "Failed to create MIDI port\n"); snd_seq_close(handle); return 1; } client_id = snd_seq_client_id(handle); // 开始查询连接状态 snd_seq_subscription_t *subs; snd_seq_port_info_t *pinfo; int connected_devices = 0; snd_seq_subscription_alloca(&subs); snd_seq_port_info_alloca(&pinfo); // 设置查询目标为我们的端口,方向为“外部端口输出到我们的端口” snd_seq_subscription_set_dest(subs, client_id, port_id); snd_seq_subscription_set_dir(subs, SND_SEQ_SUBSCRIPTION_DIR_OUTPUT); // 启动查询 if (snd_seq_query_subscribers(handle, subs, 1) < 0) { fprintf(stderr, "Failed to query subscribers\n"); snd_seq_close(handle); return 1; } // 遍历所有匹配的订阅 while (snd_seq_get_port_info(handle, pinfo) >= 0) { connected_devices++; const char *client_name = snd_seq_port_info_get_client_name(pinfo); const char *port_name = snd_seq_port_info_get_name(pinfo); printf("Connected device: %s (%s)\n", client_name, port_name); } if (connected_devices > 0) { printf("\n✅ 有%d个设备已连接,可以开始生成MIDI事件\n", connected_devices); // 在这里执行你的MIDI事件生成逻辑 } else { printf("\n❌ 当前没有设备连接到端口\n"); } snd_seq_close(handle); return 0; }
关键说明
SND_SEQ_SUBSCRIPTION_DIR_OUTPUT表示我们要查询的是外部端口向我们的端口发送数据的订阅关系,这正是aconnect建立的连接类型- 编译时需要链接ALSA库:
gcc your_code.c -o midi_check -lasound
方案2:实时监听连接/断开事件
如果你需要实时感知设备的连接或断开(比如动态启停MIDI事件生成),可以监听ALSA seq的特定事件:
代码示例
#include <alsa/asoundlib.h> #include <stdio.h> #include <unistd.h> int main() { snd_seq_t *handle; int port_id; int client_id; int connected = 0; // 初始化seq客户端和端口(同方案1) if (snd_seq_open(&handle, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { fprintf(stderr, "Failed to open ALSA sequencer\n"); return 1; } snd_seq_set_client_name(handle, "My MIDI App"); port_id = snd_seq_create_simple_port(handle, "my port", SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC); if (port_id < 0) { fprintf(stderr, "Failed to create MIDI port\n"); snd_seq_close(handle); return 1; } client_id = snd_seq_client_id(handle); // 设置非阻塞模式,避免事件输入阻塞程序 snd_seq_set_nonblock(handle, 1); snd_seq_event_t *ev; snd_seq_event_alloca(&ev); printf("等待设备连接...\n"); while (1) { int result = snd_seq_event_input(handle, ev); if (result < 0) { if (result == -EAGAIN) { // 没有事件,短暂休眠后继续 usleep(10000); continue; } fprintf(stderr, "Error reading seq event\n"); break; } switch (ev->type) { case SND_SEQ_EVENT_PORT_SUBSCRIBED: // 确认订阅的目标是我们的端口 if (ev->data.connect.dest.client == client_id && ev->data.connect.dest.port == port_id) { connected = 1; printf("✅ 设备已连接!开始生成MIDI事件\n"); // 启动你的MIDI事件生成逻辑 } break; case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: if (ev->data.connect.dest.client == client_id && ev->data.connect.dest.port == port_id) { connected = 0; printf("❌ 设备已断开!停止生成MIDI事件\n"); // 停止你的MIDI事件生成逻辑 } break; default: // 忽略其他无关事件 break; } } snd_seq_close(handle); return 0; }
关键说明
SND_SEQ_EVENT_PORT_SUBSCRIBED和SND_SEQ_EVENT_PORT_UNSUBSCRIBED是ALSA seq专门用于通知端口订阅变化的事件- 非阻塞模式确保你的程序可以同时处理其他任务,不会卡在事件输入上
内容的提问来源于stack exchange,提问作者Folkert van Heusden




