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) }
}
}
}
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)")
}