如何在CoreAudio中实现单音源多设备音频输出?
问题解答
1. CoreAudio能否实现单个音源多设备输出?
完全可以,这和Audio Midi Setup的底层实现逻辑一致,CoreAudio原生支持该功能。最标准、最省心的做法是创建聚合音频设备(Aggregate Device)——将多个物理输出设备整合为一个逻辑设备,音源只需输出到这个聚合设备,就能同步输出到所有组成它的物理设备,框架会自动处理时钟同步、格式匹配等底层问题。
也可以通过手动分流音频数据的方式实现,但聚合设备的方式更简洁可靠。
2. 能否将同一个playerNode附加到两个AVAudioEngine?
不行。AVAudioPlayerNode是AVAudioEngine处理图的一部分,每个节点只能关联到一个engine的上下文。尝试将同一个node添加到多个engine会触发框架内部的断言错误(也就是你遇到的堆栈报错),这是AVAudioEngine的设计约束。
如果需要向多个engine输出音频,要么为每个engine创建独立的playerNode,要么通过在源节点上添加tap复制音频数据,再分别喂给不同的输出节点/engine。
你的代码问题与修正
你当前的代码直接修改默认输出设备的聚合属性,这是错误的——默认输出设备通常是单个物理设备,不是聚合设备,无法直接设置kAudioAggregateDevicePropertyComposition属性。正确的做法是先创建一个新的聚合设备,再配置其组成:
import Cocoa import CoreAudio @main class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet var window: NSWindow! func applicationDidFinishLaunching(_ aNotification: Notification) { // 1. 创建新的聚合设备 var aggregateDeviceID = AudioDeviceID(0) var aggregateDeviceIDSize = UInt32(MemoryLayout<AudioDeviceID>.size) var createProperty = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyCreateAggregateDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let createStatus = AudioObjectSetPropertyData( AudioObjectID(kAudioObjectSystemObject), &createProperty, 0, nil, aggregateDeviceIDSize, &aggregateDeviceID ) guard createStatus == noErr else { print("创建聚合设备失败,错误码:\(createStatus)") return } // 2. 获取所有输出设备ID var outputDeviceCount = UInt32(0) var outputDeviceCountSize = UInt32(MemoryLayout<UInt32>.size) var deviceProperty = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var getDeviceCountStatus = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &deviceProperty, 0, nil, &outputDeviceCountSize, &outputDeviceCount ) guard getDeviceCountStatus == noErr, outputDeviceCount >= 2 else { print("获取输出设备失败或设备数量不足") return } var outputDeviceIDs = [AudioDeviceID](repeating: 0, count: Int(outputDeviceCount)) var outputDeviceIDsSize = UInt32(MemoryLayout<AudioDeviceID>.size * Int(outputDeviceCount)) var getDeviceIDsStatus = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &deviceProperty, 0, nil, &outputDeviceIDsSize, &outputDeviceIDs ) guard getDeviceIDsStatus == noErr else { print("获取设备ID失败") return } // 3. 配置聚合设备的组成(取前两个输出设备) let targetDevices = [outputDeviceIDs[0], outputDeviceIDs[1]] var compositionProperty = AudioObjectPropertyAddress( mSelector: kAudioAggregateDevicePropertyComposition, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var compositionSize = UInt32(MemoryLayout<AudioDeviceID>.size * targetDevices.count) let setCompositionStatus = AudioObjectSetPropertyData( aggregateDeviceID, &compositionProperty, 0, nil, compositionSize, targetDevices ) if setCompositionStatus == noErr { print("聚合设备配置成功,ID:\(aggregateDeviceID)") // 可选:将聚合设备设置为默认输出设备 var defaultOutputProperty = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let setDefaultStatus = AudioObjectSetPropertyData( AudioObjectID(kAudioObjectSystemObject), &defaultOutputProperty, 0, nil, aggregateDeviceIDSize, &aggregateDeviceID ) if setDefaultStatus == noErr { print("已将聚合设备设为默认输出") } } else { print("配置聚合设备失败,错误码:\(setCompositionStatus)") } } }
后续使用聚合设备
创建并配置好聚合设备后,你可以在AVAudioEngine中指定该设备作为输出:
let engine = AVAudioEngine() let playerNode = AVAudioPlayerNode() engine.attach(playerNode) // 获取聚合设备的AVAudioDevice实例 guard let aggregateDevice = AVAudioDevice(identifier: aggregateDeviceID) else { return } // 设置engine的输出设备 try? engine.outputNode.setOutputDevice(aggregateDevice) // 连接playerNode到输出节点 engine.connect(playerNode, to: engine.outputNode, format: yourAudioFormat) // 加载并播放音频 let audioFile = try AVAudioFile(forReading: yourAudioURL) playerNode.scheduleFile(audioFile, at: nil) try engine.start() playerNode.play()
内容的提问来源于stack exchange,提问作者Чайка
相关产品推荐
相关产品推荐

