对 CoreMIDI 目的地的混淆

Confusion over CoreMIDI Destinations

给定以下代码,如果我使用 if 分支中的第一种方法获得 MIDIDestination 代码工作正常,并发送 MIDI 数据。如果我使用 else 分支中的第二种方法,则不会发送任何数据。

var client = MIDIClientRef()
var port = MIDIPortRef()
var dest = MIDIEndpointRef()

MIDIClientCreate("jveditor" as CFString, nil, nil, &client)
MIDIOutputPortCreate(client, "output" as CFString, &port)

if false {
    dest = MIDIGetDestination(1)
} else {
    var device = MIDIGetExternalDevice(0)
    var entity = MIDIDeviceGetEntity(device, 0)
    dest = MIDIEntityGetDestination(entity, 0)
}

var name: Unmanaged<CFString>?
MIDIObjectGetStringProperty(dest, kMIDIPropertyDisplayName, &name)
print(name?.takeUnretainedValue() as! String)

var gmOn : [UInt8] = [ 0xf0, 0x7e, 0x7f, 0x09, 0x01, 0xf7 ]

var pktlist = MIDIPacketList()
var current = MIDIPacketListInit(&pktlist)
current = MIDIPacketListAdd(&pktlist, MemoryLayout<MIDIPacketList>.stride, current, 0, gmOn.count, &gmOn)

MIDISend(port, dest, &pktlist)

两种情况打印的设备名称都是正确的,每次调用的状态都是noErr.

我注意到,如果我要求 kMIDIManufacturerName 属性,我会得到不同的结果——特别是使用第一种方法,我会从 USB MIDI 接口得到 Generic MIDI 设备已连接,使用第二种方法我获得了通过音频 MIDI 设置应用程序配置的 Roland 的值。

我想使用第二种方法的原因是为了过滤掉没有所需制造商名称的设备,但如上所述我无法获得工作输出。

任何人都可以解释这两种方法之间的区别,以及为什么后者不起作用,最好就我如何解决这个问题提出建议吗?

听起来您只想找到 MIDI 目标端点以与特定制造商的设备通信。不幸的是,这实际上是不可能的,因为没有用于发现存在哪些 MIDI 设备、它们的属性是什么以及它们如何连接到计算机的协议。

(请记住,MIDI 是 1980 年代的原始技术。它甚至不需要双向通信。有完全有效的 MIDI 设置以及您可以向其发送数据的 MIDI 设备,但是永远无法从中接收数据,反之亦然。)

计算机知道连接到它的 MIDI 接口(例如,USB-MIDI 接口)。 CoreMIDI 称这些为“设备”。您可以找出有多少个,每个端口有多少个端口等。但是无法找到有关物理 MIDI 设备(例如连接到它们的键盘和合成器)的任何信息。

“外部设备”试图绕过发现问题。当您按下“添加设备”按钮时,它们会出现在音频 MIDI 设置中。就这些!

理想情况下,您的用户会在其设置中为每个物理 MIDI 设备创建一个外部设备,输入每个设备的所有属性,并以完美反映其物理 MIDI 电缆的方式设置所有连接。

不幸的是,现实中:

  • 可能没有任何外部设备。在 Audio MIDI Setup 中创建它们没有太大好处,而且输入很多无聊的数据,因此大多数人不会打扰。
  • 如果有外部设备,您不能相信用户添加的任何信息。例如,制造商可能不正确,或者可能拼写错误。
  • 强制用户在使用您的软件之前在音频 MIDI 设置中进行设置是非常不友好的。因此,没有应用程序会这样做......因此没有人在音频 MIDI 设置中进行任何设置。这是一个 chicken-and-egg 问题。
  • 即使有外部设备,您的用户也可能希望将 MIDI 发送到其他端点(如其他应用程序创建的虚拟端点),这些端点显然没有连接到外部设备。你应该让他们为所欲为。

documentation for MIDIGetDevice()提出了一个很好的建议:

If a client iterates through the devices and entities in the system, it will not ever visit any virtual sources and destinations created by other clients. Also, a device iteration will return devices which are "offline" (were present in the past but are not currently present), while iterations through the system's sources and destinations will not include the endpoints of offline devices.

Thus clients should usually use MIDIGetNumberOfSources, MIDIGetSource, MIDIGetNumberOfDestinations and MIDIGetDestination, rather iterating through devices and entities to locate endpoints.

换句话说:使用 MIDIGetNumberOfDestinationsMIDIGetDestination 来获取可能的目的地,然后让您的用户选择其中之一。就这些了。

如果您真的想做更多:

  • 给定目标端点,您可以使用 MIDIEndpointGetEntityMIDIEndpointGetDevice 到达 MIDI 接口。
  • 给定任何 MIDI 对象,您可以找到它与其他对象的联系。使用 MIDIObjectGetDataProperty to get the value of property kMIDIPropertyConnectionUniqueID, which is an array of the unique IDs of connected objects. Then use MIDIObjectFindByUniqueID 获取对象。 outObjectType 会告诉你它是什么类型的对象。

但这很尴尬,您不能保证找到任何有用的信息。

根据 Kurt Revis 的回答提示,我找到了解决方案。

我需要找到的目的地与外部设备的相关联,使用kMIDIPropertyConnectionUniqueID属性找到它们之间的连接那个来源。

用下面的代码替换问题中 if / else 分支中的代码:

var external = MIDIGetExternalDevice(0)
var entity = MIDIDeviceGetEntity(external, 0)
var src = MIDIEntityGetSource(entity, 0)

var connID : Int32 = 0
var dest = MIDIObjectRef()
var type = MIDIObjectType.other

MIDIObjectGetIntegerProperty(src, kMIDIPropertyConnectionUniqueID, &connID)
MIDIObjectFindByUniqueID(connID, &dest, &type)

A 属性 转储表明连接唯一 ID 属性 实际上是一个数据 属性(可能包含多个 ID),但结果 CFData 似乎在big-endian 格式,因此将其作为整数读取 属性 似乎工作正常。