swiftmacoscore-audio

How do I output audio from multiple devices in CoreAudio?


Audio Midi Setup allows a single sound source to be output from multiple devices, but is there a way to do this in CoreAudio?

Could this be done by attaching the same playerNode to two AVAudioEngine? I thought about doing this, but it threw a stack trace and gave me an error.

import Cocoa
import CoreAudio

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet var window: NSWindow!


    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Get the default output device
        var defaultOutputDeviceID = AudioDeviceID(0)
        var defaultOutputDeviceIDSize = UInt32(MemoryLayout<AudioDeviceID>.size)
        var property: AudioObjectPropertyAddress = AudioObjectPropertyAddress(mSelector:kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain)
        var getDefaultOutputDeviceIDStatus = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &property, 0, nil, &defaultOutputDeviceIDSize, &defaultOutputDeviceID)
        
        // Get the number of output devices
        var outputDeviceCount = UInt32(0)
        var outputDeviceCountSize = UInt32(MemoryLayout<UInt32>.size)
        property.mSelector = kAudioHardwarePropertyDevices
        
        var getOutputDeviceCountStatus = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &property, 0, nil, &outputDeviceCountSize, &outputDeviceCount)
        
        // Get the IDs of all output devices
        var outputDeviceIDs = [AudioDeviceID](repeating: 0, count: Int(outputDeviceCount))
        var outputDeviceIDsSize = UInt32(MemoryLayout<AudioDeviceID>.size * Int(outputDeviceCount))
        property.mSelector = kAudioHardwarePropertyDevices
        var getOutputDeviceIDsStatus = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &property, 0, nil, &outputDeviceIDsSize, &outputDeviceIDs)

        // Set the output device IDs to the first two devices
        var outputDeviceIDsToSet = [outputDeviceIDs[0], outputDeviceIDs[1]]
        var outputDeviceIDsToSetSize = UInt32(MemoryLayout<AudioDeviceID>.size * outputDeviceIDsToSet.count)
        property.mSelector = kAudioAggregateDevicePropertyComposition
        var setOutputDeviceIDsStatus = AudioObjectSetPropertyData(defaultOutputDeviceID, &property, 0, nil, outputDeviceIDsToSetSize, &outputDeviceIDsToSet)    }
        }
    }
}

Solution

  • In preparation, struct,

    public Struct DeviceID {
        let deviceID: AudioObjectID
        let UID: String
    }
    

    and a Dictionary<String, DeviceID> consisting of them.

    and code is

    func makeAggregate (devices devs: Array<DeviceID>) -> AudioDeviceID {
        let deviceList = devs.map {
            [
                kAudioSubDeviceUIDKey: $0.UID,
                kAudioSubDeviceDriftCompensationKey : $0.UID == "com.rogueamoeba.Loopback:32B21BC2-536C-43A0-8A7B-8354B85AD4C7" ? 1 : 0
            ]
        }
        
        let description: Dictionary<String, Any> = [
            kAudioAggregateDeviceNameKey: "testDevice",
            kAudioAggregateDeviceUIDKey: UUID().uuidString,
            kAudioAggregateDeviceSubDeviceListKey: deviceList,
            kAudioAggregateDeviceMasterSubDeviceKey: devs.first?.UID as Any,
            kAudioAggregateDeviceClockDeviceKey: devs.first?.UID as Any,
            kAudioAggregateDeviceIsPrivateKey: 1,
            kAudioAggregateDeviceIsStackedKey: 1
        ]
    
        var aggregateDeviceID: AudioDeviceID = 0
        let status: OSStatus = AudioHardwareCreateAggregateDevice(description as CFDictionary, &aggregateDeviceID)
        print(status)
    
        return aggregateDeviceID
    }
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let devices: AudioDevices = AudioDevices()
        var audioFile: AVAudioFile
        print(devices.outputDevices)
    
        let deviceToBind: Array<DeviceID> = [devices.outputDevices["Loopback Audio"]!, devices.outputDevices["BoomAudio"]!]
        audioDeviceId = makeAggregate(devices: deviceToBind)
        print("new device \(audioDeviceId)")
    
        do {
            let fileURL: URL = Bundle.main.url(forResource: "1-03 RTRT", withExtension: "mp3")!
            audioFile = try AVAudioFile(forReading: fileURL)
            engine.attach(player)
            engine.connect(player, to: engine.mainMixerNode, format: nil)
            player.scheduleFile(audioFile, at: nil)
            try engine.outputNode.auAudioUnit.setDeviceID(audioDeviceId)
            try engine.start()
            player.play()
        } catch let error {
            print(error.localizedDescription)
        }
    }
    
    func applicationWillTerminate(_ aNotification: Notification) {
        let destroyStatus: OSStatus = AudioHardwareDestroyAggregateDevice(audioDeviceId)
        print("destroy done \(destroyStatus)")
    }