You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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
    }
}

三、关键代码说明

  1. MIDI客户端与端口创建:setupMidiClient中创建客户端和输入端口,用于接收MIDI消息。
  2. 实时更新机制:registerMidiNotifications注册端点变化通知,当网络会话新增/移除时自动刷新列表。
  3. 网络端点过滤:通过kMIDIPropertyIsNetwork属性筛选出仅网络类型的MIDI源,避免混入本地设备。
  4. 连接状态管理:用字典hostConnectionStatus存储每个主机的连接状态,实现Toggle的独立绑定。
  5. 时钟监听:在midiMessageCallback中识别状态字节0xF8的MIDI时钟消息,更新计数。

内容的提问来源于stack exchange,提问作者Fabian H.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 05:29:54