Linux 上的 MIDI 2.0¶
概述¶
MIDI 2.0 是一种扩展协议,旨在为传统的 MIDI 1.0 提供更高的分辨率和更精细的控制。为支持 MIDI 2.0 而引入的根本性变化包括:
支持通用 MIDI 数据包 (UMP)
支持 MIDI 2.0 协议消息
UMP 和传统 MIDI 1.0 字节流之间的透明转换
用于属性和配置文件配置的 MIDI-CI
UMP 是一种新的容器格式,用于保存所有 MIDI 协议 1.0 和 MIDI 2.0 协议消息。与之前的字节流不同,它是 32 位对齐的,并且每个消息都可以放在单个数据包中。UMP 可以发送多达 16 个“UMP 组”的事件,其中每个 UMP 组包含多达 16 个 MIDI 通道。
MIDI 2.0 协议是一种扩展协议,旨在实现比旧的 MIDI 1.0 协议更高的分辨率和更多控制。
MIDI-CI 是一种高层协议,可以与 MIDI 设备进行通信,以实现灵活的配置文件和配置。它以特殊的 SysEx 形式表示。
对于 Linux 实现,内核支持 UMP 传输以及 UMP 上的 MIDI 协议的编码/解码,而 MIDI-CI 在用户空间通过标准的 SysEx 支持。
截至本文撰写之时,只有 USB MIDI 设备原生支持 UMP 和 Linux 2.0。UMP 支持本身非常通用,因此可以被其他传输层使用,尽管它也可以以不同的方式实现(例如作为 ALSA 音序器客户端)。
UMP 设备的访问方式有两种:通过 rawmidi 设备访问和通过 ALSA 音序器 API 访问。
ALSA 音序器 API 已扩展为允许 UMP 数据包的有效负载。允许在 MIDI 1.0 和 MIDI 2.0 音序器客户端之间自由连接,并且事件会透明地转换。
内核配置¶
添加了以下新配置以支持 MIDI 2.0:CONFIG_SND_UMP、CONFIG_SND_UMP_LEGACY_RAWMIDI、CONFIG_SND_SEQ_UMP、CONFIG_SND_SEQ_UMP_CLIENT 和 CONFIG_SND_USB_AUDIO_MIDI_V2。第一个可见的配置是 CONFIG_SND_USB_AUDIO_MIDI_V2,当你选择它(设置为 =y)时,会自动选择对 UMP (CONFIG_SND_UMP) 的核心支持和音序器绑定 (CONFIG_SND_SEQ_UMP_CLIENT)。
此外,CONFIG_SND_UMP_LEGACY_RAWMIDI=y 将启用对 UMP 端点的传统原始 MIDI 设备的支持。
带有 USB MIDI 2.0 的原始 MIDI 设备¶
当设备支持 MIDI 2.0 时,USB 音频驱动程序会探测并使用 MIDI 2.0 接口(始终在 altset 1 中找到)作为默认接口,而不是 MIDI 1.0 接口(在 altset 0 中)。你也可以通过将 midi2_enable=0 选项传递给 snd-usb-audio 驱动程序模块来切换回与旧的 MIDI 1.0 接口的绑定。
USB 音频驱动程序会尝试查询 UMP 端点和 UMP 功能块信息,这些信息自 UMP v1.1 起提供,并根据这些信息构建拓扑。当设备较旧且不响应新的 UMP 查询时,驱动程序会回退并根据 USB 描述符中的组终端块 (GTB) 信息构建拓扑。某些设备可能会被意外的 UMP 命令搞乱;在这种情况下,请将 midi2_ump_probe=0 选项传递给 snd-usb-audio 驱动程序以跳过 UMP v1.1 查询。
当探测到 MIDI 2.0 设备时,内核会为设备的每个 UMP 端点创建一个原始 MIDI 设备。其设备名称为 /dev/snd/umpC*D*,与 MIDI 1.0 的标准原始 MIDI 设备名称 /dev/snd/midiC*D* 不同,以避免传统应用程序错误地访问 UMP 设备。
你可以直接从/向这个 UMP 原始 MIDI 设备读取和写入 UMP 数据包数据。例如,像下面这样通过 hexdump 读取将以十六进制格式显示卡 0 设备 0 的传入 UMP 数据包
% hexdump -C /dev/snd/umpC0D0
00000000 01 07 b0 20 00 07 b0 20 64 3c 90 20 64 3c 80 20 |... ... d<. d<. |
与 MIDI 1.0 字节流不同,UMP 是一个 32 位数据包,并且读取或写入设备的大小也对齐到 32 位(即 4 个字节)。
UMP 数据包有效负载中的 32 位字始终采用 CPU 原生字节序。传输驱动程序负责将 UMP 字从系统字节序转换为所需的传输字节序/字节顺序。
当设置了 CONFIG_SND_UMP_LEGACY_RAWMIDI 时,驱动程序会另外创建一个标准的原始 MIDI 设备,名称为 /dev/snd/midiC*D*。它包含 16 个子流,每个子流对应一个(从 0 开始)UMP 组。传统应用程序可以通过 MIDI 1.0 字节流格式中的每个子流访问指定的组。通过 ALSA rawmidi API,你可以打开任意子流,而仅打开 /dev/snd/midiC*D* 将最终打开第一个子流。
每个 UMP 端点都可以提供附加信息,这些信息是从通过 UMP 1.1 流消息或 USB MIDI 2.0 描述符查询的信息构建的。并且 UMP 端点可以包含一个或多个 UMP 块,其中 UMP 块是 ALSA UMP 实现中引入的一种抽象,用于表示 UMP 组之间的关联。UMP 块对应于 UMP 1.1 规范中的功能块。当 UMP 1.1 功能块信息不可用时,它会部分从 USB MIDI 2.0 规范中定义的组终端块 (GTB) 填充。
UMP 端点和 UMP 块的信息可以在 proc 文件 /proc/asound/card*/midi* 中找到。例如
% cat /proc/asound/card1/midi0
ProtoZOA MIDI
Type: UMP
EP Name: ProtoZOA
EP Product ID: ABCD12345678
UMP Version: 0x0000
Protocol Caps: 0x00000100
Protocol: 0x00000100
Num Blocks: 3
Block 0 (ProtoZOA Main)
Direction: bidirection
Active: Yes
Groups: 1-1
Is MIDI1: No
Block 1 (ProtoZOA Ext IN)
Direction: output
Active: Yes
Groups: 2-2
Is MIDI1: Yes (Low Speed)
....
请注意,上面 proc 文件中显示的 Groups 字段指示从 1 开始的 UMP 组号(从-到)。
这些额外的 UMP 端点和 UMP 块信息可以通过新的 ioctl SNDRV_UMP_IOCTL_ENDPOINT_INFO 和 SNDRV_UMP_IOCTL_BLOCK_INFO 分别获得。
原始 MIDI 名称和 UMP 端点名称通常相同,并且在 USB MIDI 的情况下,它是从相应 USB MIDI 接口描述符的 iInterface 中获取的。如果未提供,则会从 USB 设备描述符的 iProduct 中复制作为后备。
端点产品 ID 是一个字符串字段,并且应该是唯一的。它从 USB MIDI 设备的 iSerialNumber 复制而来。
协议功能和实际协议位在 asound.h 中定义。
带有 USB MIDI 2.0 的 ALSA 音序器¶
除了原始 MIDI 接口之外,ALSA 音序器接口也支持新的 UMP MIDI 2.0 设备。现在,每个 ALSA 音序器客户端都可以设置其 MIDI 版本(0、1 或 2),以声明自身是传统设备、UMP MIDI 1.0 设备还是 UMP MIDI 2.0 设备。第一个传统客户端是按原样发送/接收旧音序器事件的客户端。同时,UMP MIDI 1.0 和 2.0 客户端在 UMP 的扩展事件记录中发送和接收。MIDI 版本在 snd_seq_client_info 的新 midi_version 字段中看到。
可以通过指定新的事件标志位 SNDRV_SEQ_EVENT_UMP,在嵌入的音序器事件中发送/接收 UMP 数据包。当设置此标志时,事件具有 16 字节(128 位)的数据有效负载,用于保存 UMP 数据包。如果没有 SNDRV_SEQ_EVENT_UMP 位标志,则事件将像以前一样被视为传统事件(最大 12 字节数据有效负载)。
设置 SNDRV_SEQ_EVENT_UMP 标志后,将忽略 UMP 音序器事件的类型字段(但应默认设置为 0)。
每个客户端的类型可以在 /proc/asound/seq/clients 中看到。例如
% cat /proc/asound/seq/clients
Client info
cur clients : 3
....
Client 14 : "Midi Through" [Kernel Legacy]
Port 0 : "Midi Through Port-0" (RWe-)
Client 20 : "ProtoZOA" [Kernel UMP MIDI1]
UMP Endpoint: ProtoZOA
UMP Block 0: ProtoZOA Main [Active]
Groups: 1-1
UMP Block 1: ProtoZOA Ext IN [Active]
Groups: 2-2
UMP Block 2: ProtoZOA Ext OUT [Active]
Groups: 3-3
Port 0 : "MIDI 2.0" (RWeX) [In/Out]
Port 1 : "ProtoZOA Main" (RWeX) [In/Out]
Port 2 : "ProtoZOA Ext IN" (-We-) [Out]
Port 3 : "ProtoZOA Ext OUT" (R-e-) [In]
在这里你可以找到两种类型的内核客户端,客户端 14 的“Legacy”和客户端 20 的“UMP MIDI1”,后者是 USB MIDI 2.0 设备。USB MIDI 2.0 客户端始终将端口 0 作为“MIDI 2.0”,其余端口从 1 开始用于每个 UMP 组(例如,组 1 的端口 1)。在此示例中,该设备有三个活动组(Main、Ext IN 和 Ext OUT),这些组作为音序器端口从 1 到 3 公开。“MIDI 2.0”端口用于 UMP 端点,它与其他 UMP 组端口的区别在于,UMP 端点端口发送来自设备上所有端口的事件(“捕获所有”),而每个 UMP 组端口仅发送来自给定 UMP 组的事件。此外,无 UMP 组的消息(例如 UMP 消息类型 0x0f)仅发送到 UMP 端点端口。
请注意,尽管每个 UMP 音序器客户端通常会创建 16 个端口,但那些不属于任何 UMP 块(或属于非活动 UMP 块)的端口会被标记为非活动状态,并且它们不会出现在 proc 输出中。在上面的示例中,存在从 4 到 16 的音序器端口,但未在那里显示。
上面的 proc 文件也显示了 UMP 块信息。在原始 MIDI proc 输出中可以找到相同的条目(但具有更详细的信息)。
当客户端在不同的 MIDI 版本之间连接时,事件会根据客户端的版本自动转换,不仅在传统类型和 UMP MIDI 1.0/2.0 类型之间,而且也在 UMP MIDI 1.0 和 2.0 类型之间转换。例如,在传统模式下运行 ProtoZOA Main 端口上的 aseqdump 程序将为你提供如下输出
% aseqdump -p 20:1
Waiting for data. Press Ctrl+C to end.
Source Event Ch Data
20:1 Note on 0, note 60, velocity 100
20:1 Note off 0, note 60, velocity 100
20:1 Control change 0, controller 11, value 4
当你以 MIDI 2.0 模式运行 aseqdump 时,它将接收到如下高精度数据
% aseqdump -u 2 -p 20:1
Waiting for data. Press Ctrl+C to end.
Source Event Ch Data
20:1 Note on 0, note 60, velocity 0xc924, attr type = 0, data = 0x0
20:1 Note off 0, note 60, velocity 0xc924, attr type = 0, data = 0x0
20:1 Control change 0, controller 11, value 0x2000000
而数据由 ALSA 音序器核心自动转换。
原始 MIDI API 扩展¶
可以通过新的 ioctl SNDRV_UMP_IOCTL_ENDPOINT_INFO 获得额外的 UMP 端点信息。它包含关联的卡和设备编号、位标志、协议、UMP 块的数量、端点的名称字符串等。
协议在两个字段中指定,协议功能和当前协议。两者都包含位标志,这些标志在上字节中指定 MIDI 协议版本(SNDRV_UMP_EP_INFO_PROTO_MIDI1 或 SNDRV_UMP_EP_INFO_PROTO_MIDI2),在下字节中指定抖动减少时间戳(SNDRV_UMP_EP_INFO_PROTO_JRTS_TX 和 SNDRV_UMP_EP_INFO_PROTO_JRTS_RX)。
一个 UMP 端点最多可以包含 32 个 UMP 块,并且当前分配的块数显示在端点信息中。
每个 UMP 块信息可以通过另一个新的 ioctl SNDRV_UMP_IOCTL_BLOCK_INFO 获得。必须传递要查询的块的块 ID 号(从 0 开始)。接收到的数据包含块的关联方向、第一个关联的组 ID(从 0 开始)和组的数量、块的名称字符串等。
方向可以是 SNDRV_UMP_DIR_INPUT、SNDRV_UMP_DIR_OUTPUT 或 SNDRV_UMP_DIR_BIDIRECTION。
对于支持 UMP v1.1 的设备,UMP MIDI 协议可以通过“流配置请求”消息(UMP 类型 0x0f,状态 0x05)切换。当 UMP 核心收到这样的消息时,它会更新 UMP EP 信息和相应的音序器客户端。
控制 API 扩展¶
引入新的 ioctl SNDRV_CTL_IOCTL_UMP_NEXT_DEVICE 用于查询下一个 UMP rawmidi 设备,而现有的 ioctl SNDRV_CTL_IOCTL_RAWMIDI_NEXT_DEVICE 仅查询传统的 rawmidi 设备。
对于设置要打开的子设备(子流编号),请像普通的 rawmidi 一样使用 ioctl SNDRV_CTL_IOCTL_RAWMIDI_PREFER_SUBDEVICE。
两个新的 ioctl SNDRV_CTL_IOCTL_UMP_ENDPOINT_INFO 和 SNDRV_CTL_IOCTL_UMP_BLOCK_INFO 通过 ALSA 控制 API 提供指定 UMP 设备的 UMP 端点和 UMP 块信息,而无需打开实际的 (UMP) rawmidi 设备。card 字段在查询时被忽略,始终与控制接口的卡绑定。
音序器 API 扩展¶
midi_version 字段已添加到 snd_seq_client_info 中,以指示每个客户端的当前 MIDI 版本(0、1 或 2)。当 midi_version 为 1 或 2 时,从 UMP 音序器客户端读取的对齐方式也从之前的 28 字节更改为 32 字节,以适应扩展的有效负载。写入的对齐大小没有更改,但每个事件的大小可能因下面的新位标志而异。
为每个音序器事件标志添加了 SNDRV_SEQ_EVENT_UMP 标志位。当设置此位标志时,音序器事件将扩展为具有 16 字节的较大有效负载,而不是传统的 12 字节,并且该事件的有效负载中包含 UMP 数据包。
新的音序器端口类型位 (SNDRV_SEQ_PORT_TYPE_MIDI_UMP) 指示该端口是否支持 UMP。
音序器端口具有新的功能位,用于指示非活动端口 (SNDRV_SEQ_PORT_CAP_INACTIVE) 和 UMP 端点端口 (SNDRV_SEQ_PORT_CAP_UMP_ENDPOINT)。
可以通过将新的过滤器位 SNDRV_SEQ_FILTER_NO_CONVERT 设置到客户端信息中来禁止 ALSA 音序器客户端的事件转换。例如,内核直通客户端 (snd-seq-dummy) 会在内部设置此标志。
端口信息获得新字段 direction 来指示端口的方向(SNDRV_SEQ_PORT_DIR_INPUT、SNDRV_SEQ_PORT_DIR_OUTPUT 或 SNDRV_SEQ_PORT_DIR_BIDIRECTION)。
端口信息的另一个附加字段是 ump_group,它指定关联的 UMP 组号(从 1 开始)。当它为非零时,在交付到指定组时会更新 UMP 数据包中的 UMP 组字段(更正为从 0 开始)。如果每个音序器端口是特定于某个 UMP 组的端口,则应该设置此字段。
每个客户端都可以在 group_filter 位图中为 UMP 组设置额外的事件过滤器。该过滤器由从 1 开始的组号的位图组成。例如,当设置位 1 时,来自组 1(即第一个组)的消息将被过滤掉且不会被传递。位 0 用于过滤 UMP 无组消息。
为支持 UMP 的客户端添加了两个新的 ioctl:SNDRV_SEQ_IOCTL_GET_CLIENT_UMP_INFO 和 SNDRV_SEQ_IOCTL_SET_CLIENT_UMP_INFO。它们用于获取和设置与音序器客户端关联的 snd_ump_endpoint_info 或 snd_ump_block_info 数据。USB MIDI 驱动程序从底层 UMP rawmidi 提供这些信息,而用户空间客户端可以通过 *_SET ioctl 提供其自己的数据。对于端点数据,将 0 传递给 type 字段,而对于块数据,将块号 + 1 传递给 type 字段。为内核客户端设置数据将导致错误。
使用 UMP 1.1,功能块信息可能会动态更改。当从设备收到功能块的更新时,ALSA 音序器核心会相应地更改相应的音序器端口名称和属性,并通过向 ALSA 音序器系统端口的通知来通知更改,类似于正常的端口更改通知。
MIDI2 USB Gadget 功能驱动程序¶
最新的内核包含对 USB MIDI 2.0 gadget 功能驱动程序的支持,该驱动程序可用于原型设计和调试 MIDI 2.0 功能。
对于 MIDI2 gadget 驱动程序,需要启用 CONFIG_USB_GADGET、CONFIG_USB_CONFIGFS 和 CONFIG_USB_CONFIGFS_F_MIDI2。
此外,要使用 gadget 驱动程序,您需要一个工作的 UDC 驱动程序。在下面的示例中,我们使用 dummy_hcd 驱动程序(通过 CONFIG_USB_DUMMY_HCD 启用),该驱动程序可在 PC 和 VM 上用于调试目的。还有其他 UDC 驱动程序取决于平台,它们也可以用于真实的设备。
首先,在运行 gadget 的系统上,加载 libcomposite 模块
% modprobe libcomposite
并且您将在 configfs 空间(通常在现代操作系统上的 /sys/kernel/config 下)拥有 usb_gadget 子目录。然后在那里创建一个 gadget 实例并添加配置,例如
% cd /sys/kernel/config
% mkdir usb_gadget/g1
% cd usb_gadget/g1
% mkdir configs/c.1
% mkdir functions/midi2.usb0
% echo 0x0004 > idProduct
% echo 0x17b3 > idVendor
% mkdir strings/0x409
% echo "ACME Enterprises" > strings/0x409/manufacturer
% echo "ACMESynth" > strings/0x409/product
% echo "ABCD12345" > strings/0x409/serialnumber
% mkdir configs/c.1/strings/0x409
% echo "Monosynth" > configs/c.1/strings/0x409/configuration
% echo 120 > configs/c.1/MaxPower
此时,必须有一个子目录 ep.0,这是 UMP 端点的配置。您可以填写端点信息,例如
% echo "ACMESynth" > functions/midi2.usb0/iface_name
% echo "ACMESynth" > functions/midi2.usb0/ep.0/ep_name
% echo "ABCD12345" > functions/midi2.usb0/ep.0/product_id
% echo 0x0123 > functions/midi2.usb0/ep.0/family
% echo 0x4567 > functions/midi2.usb0/ep.0/model
% echo 0x123456 > functions/midi2.usb0/ep.0/manufacturer
% echo 0x12345678 > functions/midi2.usb0/ep.0/sw_revision
可以设置默认的 MIDI 协议为 1 或 2
% echo 2 > functions/midi2.usb0/ep.0/protocol
并且,您可以在此端点子目录下找到一个子目录 block.0。这定义了功能块信息
% echo "Monosynth" > functions/midi2.usb0/ep.0/block.0/name
% echo 0 > functions/midi2.usb0/ep.0/block.0/first_group
% echo 1 > functions/midi2.usb0/ep.0/block.0/num_groups
最后,链接配置并启用它
% ln -s functions/midi2.usb0 configs/c.1
% echo dummy_udc.0 > UDC
其中 dummy_udc.0 是一个示例,它因系统而异。您可以在 /sys/class/udc 中找到 UDC 实例,并传递找到的名称。
% ls /sys/class/udc
dummy_udc.0
现在,已启用 MIDI 2.0 gadget 设备,并且 gadget 主机通过 f_midi2 驱动程序创建一个新的声卡实例,其中包含一个 UMP rawmidi 设备。
% cat /proc/asound/cards
....
1 [Gadget ]: f_midi2 - MIDI 2.0 Gadget
MIDI 2.0 Gadget
在连接的主机上,也应该出现类似的卡,但具有 configfs 中给定的卡和设备名称。
% cat /proc/asound/cards
....
2 [ACMESynth ]: USB-Audio - ACMESynth
ACME Enterprises ACMESynth at usb-dummy_hcd.0-1, high speed
您可以在 gadget 端播放 MIDI 文件
% aplaymidi -p 20:1 to_host.mid
这将在连接的主机上显示为来自 MIDI 设备的输入
% aseqdump -p 20:0 -u 2
反之亦然,在连接的主机上播放也将作为 gadget 上的输入。
每个功能块可能具有不同的方向和 UI 提示,通过 direction 和 ui_hint 属性指定。传递 1 表示仅输入,2 表示仅输出,3 表示双向(默认值)。例如
% echo 2 > functions/midi2.usb0/ep.0/block.0/direction
% echo 2 > functions/midi2.usb0/ep.0/block.0/ui_hint
当您需要多个功能块时,您可以动态创建子目录 block.1、block.2 等,并在链接之前在上面的配置过程中对其进行配置。例如,为键盘创建第二个功能块
% mkdir functions/midi2.usb0/ep.0/block.1
% echo "Keyboard" > functions/midi2.usb0/ep.0/block.1/name
% echo 1 > functions/midi2.usb0/ep.0/block.1/first_group
% echo 1 > functions/midi2.usb0/ep.0/block.1/num_groups
% echo 1 > functions/midi2.usb0/ep.0/block.1/direction
% echo 1 > functions/midi2.usb0/ep.0/block.1/ui_hint
block.* 子目录也可以动态删除(除了持久存在的 block.0)。
要为 MIDI 1.0 I/O 分配功能块,请在 is_midi1 属性中设置。1 表示 MIDI 1.0,2 表示低速连接的 MIDI 1.0。
% echo 2 > functions/midi2.usb0/ep.0/block.1/is_midi1
要禁用 gadget 驱动程序中 UMP 流消息的处理,请将 0 传递给顶层配置中的 process_ump 属性。
% echo 0 > functions/midi2.usb0/process_ump
gadget 驱动程序也支持 altset 0 上的 MIDI 1.0 接口。当连接的主机选择 MIDI 1.0 接口时,gadget 上的 UMP I/O 将从/转换为 USB MIDI 1.0 数据包,而 gadget 驱动程序继续通过 UMP rawmidi 与用户空间通信。
MIDI 1.0 端口从每个功能块中的配置进行设置。例如
% echo 0 > functions/midi2.usb0/ep.0/block.0/midi1_first_group
% echo 1 > functions/midi2.usb0/ep.0/block.0/midi1_num_groups
上面的配置将为 MIDI 1.0 接口启用组 1(索引 0)。请注意,这些组必须在为功能块本身定义的组中。
gadget 驱动程序也支持多个 UMP 端点。与功能块类似,您可以在卡顶层配置下创建一个新的子目录 ep.1 以启用新的端点
% mkdir functions/midi2.usb0/ep.1
并在那里创建一个新的功能块。例如,为这个新端点的功能块创建 4 个组
% mkdir functions/midi2.usb0/ep.1/block.0
% echo 4 > functions/midi2.usb0/ep.1/block.0/num_groups
现在,您总共有 4 个 rawmidi 设备:前两个是端点 0 和端点 1 的 UMP rawmidi 设备,另外两个是对应于 EP 0 和 EP 1 的传统 MIDI 1.0 rawmidi 设备。
可以通过带有 RAWMIDI iface 的控制元素“操作模式”告知 gadget 上的当前 altsetting。例如,您可以通过在 gadget 主机上运行的 amixer 程序读取它,例如
% amixer -c1 cget iface=RAWMIDI,name='Operation Mode'
; type=INTEGER,access=r--v----,values=1,min=0,max=2,step=0
: values=2
该值(在第二个返回行中显示为 : values=)表示 1 为 MIDI 1.0(altset 0),2 为 MIDI 2.0(altset 1),0 为未设置。
到目前为止,配置在绑定后无法更改。