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

如何在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,提问作者Чайка

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 13:03:12