在Swift中使用CoreMIDI获取并显示可用MIDI网络会话
在macOS上用CoreMIDI实现MIDI网络会话实时更新与时钟监听
针对你在macOS上用CoreMIDI实现MIDI网络会话实时更新、并最终获取MIDI时钟的需求,这里给出完整解决方案,解决你当前的核心问题:
一、核心改进点说明
1. 替换轮询,用CoreMIDI通知实现实时更新
CoreMIDI提供原生的设备变化通知机制,无需定时轮询。通过注册MIDIObjectAddNotification,可以在MIDI端点新增/移除时自动刷新列表。
2. 精准过滤MIDI网络会话
当前代码会获取所有MIDI源(包括本地设备),通过检查端点的kMIDIPropertyIsNetwork属性,只保留网络会话类型的端点。
3. 修复连接状态绑定问题
原代码用单个布尔值绑定所有Toggle,导致所有开关同步变化。改用字典存储每个主机的连接状态,实现独立绑定。
4. 新增MIDI时钟监听
通过创建MIDI输入端口,绑定到目标网络会话端点,在消息回调中识别并处理时钟指令(状态字节0xF8)。
二、完整实现代码
import Foundation import SwiftUI import CoreMIDI struct MidiHostList: View { // 存储MIDI网络会话的名称 @State private var midiNetworkHosts = [String]() // 存储每个主机的连接状态(主机名: 是否连接) @State private var hostConnectionStatus: [String: Bool] = [:] // 存储最近收到的MIDI时钟计数 @State private var midiClockCount = 0 // CoreMIDI相关对象 private var midiClient: MIDIClientRef = 0 private var midiInputPort: MIDIPortRef = 0 var body: some View { VStack(alignment: .leading, spacing: 16) { Text("可用MIDI网络会话: \(midiNetworkHosts.count)") .font(.headline) Text("MIDI时钟计数: \(midiClockCount)") .font(.subheadline) List(midiNetworkHosts, id: \.self) { host in HStack { Toggle(isOn: $hostConnectionStatus[host] ?? false) { Text(host) } .onChange(of: hostConnectionStatus[host] ?? false) { isConnected in if isConnected { connectToHost(named: host) } else { disconnectFromHost(named: host) } } if hostConnectionStatus[host] ?? false { Text("(已连接)") .foregroundColor(.green) } } } } .padding() .onAppear { setupMidiClient() registerMidiNotifications() refreshMidiNetworkHosts() } .onDisappear { cleanupMidiResources() } } // MARK: - CoreMIDI 初始化与清理 private func setupMidiClient() { // 创建MIDI客户端 let clientName = "MIDI Network Monitor" as CFString MIDIClientCreate(clientName, nil, nil, &midiClient) // 创建MIDI输入端口,用于接收时钟消息 let inputPortName = "MIDI Clock Input" as CFString MIDIInputPortCreate(midiClient, inputPortName, midiMessageCallback, Unmanaged.passUnretained(self).toOpaque(), &midiInputPort) } private func cleanupMidiResources() { MIDIPortDispose(midiInputPort) MIDIClientDispose(midiClient) } // MARK: - 设备列表刷新与过滤 private func refreshMidiNetworkHosts() { midiNetworkHosts.removeAll() hostConnectionStatus.removeAll() let sourceCount = MIDIGetNumberOfSources() for i in 0..<sourceCount { let sourceEndpoint = MIDIGetSource(i) var isNetwork = 0 // 检查是否为网络端点 let result = MIDIObjectGetIntegerProperty(sourceEndpoint, kMIDIPropertyIsNetwork, &isNetwork) if result == noErr && isNetwork == 1 { var endpointName: Unmanaged<CFString>? MIDIObjectGetStringProperty(sourceEndpoint, kMIDIPropertyName, &endpointName) if let name = endpointName?.takeRetainedValue() as String? { midiNetworkHosts.append(name) hostConnectionStatus[name] = false } } } } // MARK: - MIDI通知注册 private func registerMidiNotifications() { // 监听端点的添加/移除/属性变化 MIDIObjectAddNotification(kMIDIObjectType_Endpoint, kMIDIObjectPropertyName, nil) { notificationPtr, refCon in let notification = notificationPtr.pointee // 只处理网络端点的变化 var isNetwork = 0 MIDIObjectGetIntegerProperty(notification.object, kMIDIPropertyIsNetwork, &isNetwork) if isNetwork == 1 { DispatchQueue.main.async { self.refreshMidiNetworkHosts() } } } } // MARK: - 主机连接与断开 private func connectToHost(named hostName: String) { // 根据名称找到对应的MIDI源端点 guard let sourceEndpoint = findMidiSourceEndpoint(named: hostName) else { return } // 将输入端口连接到该端点 MIDIPortConnectSource(midiInputPort, sourceEndpoint, Unmanaged.passUnretained(self).toOpaque()) } private func disconnectFromHost(named hostName: String) { guard let sourceEndpoint = findMidiSourceEndpoint(named: hostName) else { return } MIDIPortDisconnectSource(midiInputPort, sourceEndpoint) } private func findMidiSourceEndpoint(named hostName: String) -> MIDIEndpointRef? { let sourceCount = MIDIGetNumberOfSources() for i in 0..<sourceCount { let endpoint = MIDIGetSource(i) var endpointName: Unmanaged<CFString>? MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &endpointName) if let name = endpointName?.takeRetainedValue() as String?, name == hostName { return endpoint } } return nil } // MARK: - MIDI消息回调(处理时钟) private func midiMessageCallback(packetList: UnsafePointer<MIDIPacketList>, refCon: UnsafeMutableRawPointer?) { guard let refCon = refCon else { return } let selfPtr = Unmanaged<MidiHostList>.fromOpaque(refCon).takeUnretainedValue() let packets = packetList.pointee var packet = packets.packet repeat { let data = packet.data // MIDI时钟消息的状态字节是0xF8 if data[0] == 0xF8 { DispatchQueue.main.async { selfPtr.midiClockCount += 1 } } } while MIDIPacketNext(&packet) != nil } }
三、关键代码说明
- MIDI客户端与端口创建:
setupMidiClient中创建客户端和输入端口,用于接收MIDI消息。 - 实时更新机制:
registerMidiNotifications注册端点变化通知,当网络会话新增/移除时自动刷新列表。 - 网络端点过滤:通过
kMIDIPropertyIsNetwork属性筛选出仅网络类型的MIDI源,避免混入本地设备。 - 连接状态管理:用字典
hostConnectionStatus存储每个主机的连接状态,实现Toggle的独立绑定。 - 时钟监听:在
midiMessageCallback中识别状态字节0xF8的MIDI时钟消息,更新计数。
内容的提问来源于stack exchange,提问作者Fabian H.
相关产品推荐
相关产品推荐

