#!/usr/bin/env swift import Foundation import CoreAudio // Extensions and Helpers extension Int32 { var fourCC: String { let utf16 = [ UInt16((self >> 24) & 0xFF), UInt16((self >> 16) & 0xFF), UInt16((self >> 8) & 0xFF), UInt16(self & 0xFF) ] return String(utf16CodeUnits: utf16, count: 4) } } // Safer Property Getter func getPropertyData(objectID: AudioObjectID, selector: AudioObjectPropertySelector, initialValue: T) -> T? { var address = AudioObjectPropertyAddress( mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var size = UInt32(MemoryLayout.size) var value = initialValue let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value) if status == noErr { return value } return nil } // CFString Helper func getStringProperty(objectID: AudioObjectID, selector: AudioObjectPropertySelector) -> String? { var address = AudioObjectPropertyAddress( mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) // CFStringRef is just a pointer, so size of Optional> is pointer size var size = UInt32(MemoryLayout?>.size) var value: Unmanaged? let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value) if status == noErr, let existingValue = value { return existingValue.takeRetainedValue() as String } return nil } func findDeviceByName(_ name: String) -> AudioObjectID? { // System Object is 1 let systemID = AudioObjectID(kAudioObjectSystemObject) // Get all devices var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(systemID, &address, 0, nil, &size) == noErr else { return nil } let count = Int(size) / MemoryLayout.size var deviceIDs = [AudioObjectID](repeating: 0, count: count) guard AudioObjectGetPropertyData(systemID, &address, 0, nil, &size, &deviceIDs) == noErr else { return nil } for id in deviceIDs { if let devName = getStringProperty(objectID: id, selector: kAudioDevicePropertyDeviceNameCFString) { if devName == name { return id } } } return nil } func findDeviceByUID(_ uid: String) -> AudioObjectID? { let systemID = AudioObjectID(kAudioObjectSystemObject) var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(systemID, &address, 0, nil, &size) == noErr else { return nil } let count = Int(size) / MemoryLayout.size var deviceIDs = [AudioObjectID](repeating: 0, count: count) guard AudioObjectGetPropertyData(systemID, &address, 0, nil, &size, &deviceIDs) == noErr else { return nil } for id in deviceIDs { if let devUID = getStringProperty(objectID: id, selector: kAudioDevicePropertyDeviceUID) { if devUID == uid { return id } } } return nil } func createAggregateDevice() { print("Searching for devices...") guard let blackHoleID = findDeviceByName("BlackHole 2ch") else { print("Error: BlackHole 2ch not found. Please install it first.") exit(1) } print("Found BlackHole 2ch (ID: \(blackHoleID))") // --- PART 1: Hearbit Audio (Input: Mic + BlackHole) --- print("\n--- Creating 'Hearbit Audio' (Input) ---") // Default Input var defaultInputID: AudioObjectID = 0 var size = UInt32(MemoryLayout.size) var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) if AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &defaultInputID) != noErr { print("Error: Could not find default input.") exit(1) } print("Found Default Input (ID: \(defaultInputID))") // Check for existing "Hearbit Audio" let inputUID = "hearbit_audio_aggregate_v1" if let existingID = findDeviceByUID(inputUID) { print("Found existing Hearbit Audio (ID: \(existingID)). Destroying...") AudioHardwareDestroyAggregateDevice(existingID) Thread.sleep(forTimeInterval: 0.5) } guard let bhUID = getStringProperty(objectID: blackHoleID, selector: kAudioDevicePropertyDeviceUID) else { print("Error: Could not get BlackHole UID.") exit(1) } guard let micUID = getStringProperty(objectID: defaultInputID, selector: kAudioDevicePropertyDeviceUID) else { print("Error: Could not get Default Input UID.") exit(1) } var subDevicesUIDs = [bhUID] if micUID != bhUID { subDevicesUIDs.append(micUID) } let subDevicesArray = subDevicesUIDs.map { [kAudioSubDeviceUIDKey: $0] } let inputDesc: [String: Any] = [ kAudioAggregateDeviceNameKey: "Hearbit Audio", kAudioAggregateDeviceUIDKey: inputUID, kAudioAggregateDeviceIsPrivateKey: Int(0), kAudioAggregateDeviceIsStackedKey: Int(0), kAudioAggregateDeviceSubDeviceListKey: subDevicesArray ] var outInputID: AudioObjectID = 0 let errIn = AudioHardwareCreateAggregateDevice(inputDesc as CFDictionary, &outInputID) if errIn == noErr { print("Success! Created 'Hearbit Audio' with ID: \(outInputID)") } else { print("Failed to create 'Hearbit Audio'. Error: \(errIn)") } // --- PART 2: Cleanup Unstable "Hearbit Speakers" --- // The previous "Hearbit Speakers" device caused MS Teams to crash. // We strictly remove it here to restore stability. print("\n--- Cleaning up Unstable Devices ---") let stopOutputUID = "hearbit_speakers_aggregate_v1" if let existingOutID = findDeviceByUID(stopOutputUID) { print("Found unstable 'Hearbit Speakers' (ID: \(existingOutID)). Removing to fix Teams crash...") let errDist = AudioHardwareDestroyAggregateDevice(existingOutID) if errDist == noErr { print("Successfully removed unstable device.") } else { print("Warning: Failed to remove device. Error: \(errDist)") } } else { print("No unstable 'Hearbit Speakers' found. System is clean.") } exit(0) } createAggregateDevice()