|
import CoreAudio |
|
import Foundation |
|
|
|
func deviceName(deviceID: AudioDeviceID) -> String? { |
|
var propertySize = UInt32(MemoryLayout<CFString>.size) |
|
var name: Unmanaged<CFString>? |
|
var address = AudioObjectPropertyAddress( |
|
mSelector: kAudioDevicePropertyDeviceNameCFString, mScope: kAudioObjectPropertyScopeGlobal, |
|
mElement: kAudioObjectPropertyElementMain) |
|
guard AudioObjectGetPropertyData(deviceID, &address, 0, nil, &propertySize, &name) == noErr |
|
else { return nil } |
|
return name?.takeRetainedValue() as String? |
|
} |
|
|
|
func inputDevices() -> [AudioDeviceID] { |
|
var propertySize: UInt32 = 0 |
|
var address = AudioObjectPropertyAddress( |
|
mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, |
|
mElement: kAudioObjectPropertyElementMain) |
|
|
|
guard |
|
AudioObjectGetPropertyDataSize( |
|
AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize) == noErr |
|
else { return [] } |
|
|
|
var deviceIDs = [AudioDeviceID]( |
|
repeating: kAudioObjectUnknown, count: Int(propertySize) / MemoryLayout<AudioDeviceID>.size) |
|
guard |
|
AudioObjectGetPropertyData( |
|
AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &deviceIDs) |
|
== noErr |
|
else { return [] } |
|
return deviceIDs |
|
} |
|
|
|
func setDefaultInput(deviceID: AudioDeviceID) { |
|
var deviceID = deviceID |
|
var address = AudioObjectPropertyAddress( |
|
mSelector: kAudioHardwarePropertyDefaultInputDevice, |
|
mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) |
|
let status = AudioObjectSetPropertyData( |
|
AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, |
|
UInt32(MemoryLayout<AudioDeviceID>.size), &deviceID) |
|
if status != noErr { print("Error setting default input device: \(status)") } |
|
} |
|
|
|
func currentInputDevice() -> AudioDeviceID? { |
|
var deviceID = kAudioObjectUnknown |
|
var propertySize = UInt32(MemoryLayout<AudioDeviceID>.size) |
|
var address = AudioObjectPropertyAddress( |
|
mSelector: kAudioHardwarePropertyDefaultInputDevice, |
|
mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) |
|
guard |
|
AudioObjectGetPropertyData( |
|
AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &deviceID) |
|
== noErr |
|
else { return nil } |
|
return deviceID |
|
} |
|
|
|
func findDevice(containing substring: String) -> AudioDeviceID? { |
|
let devices = inputDevices().filter { |
|
deviceName(deviceID: $0)?.localizedCaseInsensitiveContains(substring) == true |
|
} |
|
|
|
switch devices.count { |
|
case 0: |
|
print("No matching device found.") |
|
return nil |
|
case 1: |
|
return devices.first |
|
default: |
|
print("Multiple matching devices found:") |
|
devices.forEach { print("- \(deviceName(deviceID: $0) ?? "Unknown")") } |
|
return nil // Or handle multiple matches differently, e.g., let the user choose. |
|
} |
|
} |
|
|
|
func monitorInputChanges(deviceNameSubstring: String) { |
|
var address = AudioObjectPropertyAddress( |
|
mSelector: kAudioHardwarePropertyDefaultInputDevice, |
|
mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain) |
|
var lastDeviceID: AudioDeviceID? = currentInputDevice() |
|
|
|
AudioObjectAddPropertyListenerBlock(AudioObjectID(kAudioObjectSystemObject), &address, nil) { |
|
_, _ in |
|
guard let targetDeviceID = findDevice(containing: deviceNameSubstring) else { |
|
print("Target device not found. Stopping monitoring.") |
|
return |
|
} |
|
|
|
guard let currentDeviceID = currentInputDevice(), currentDeviceID != targetDeviceID, |
|
currentDeviceID != lastDeviceID |
|
else { return } |
|
|
|
print("Switching back to target device #\(targetDeviceID).") |
|
setDefaultInput(deviceID: targetDeviceID) |
|
lastDeviceID = targetDeviceID |
|
} |
|
} |
|
|
|
func main() { |
|
guard CommandLine.argc > 1 else { |
|
print("Please provide a device name substring as a parameter.") |
|
return |
|
} |
|
|
|
let deviceNameSubstring = CommandLine.arguments[1] |
|
|
|
guard findDevice(containing: deviceNameSubstring) != nil else { |
|
print("No matching device found. Exiting.") |
|
return |
|
} |
|
|
|
print("Monitoring changes for device matching substring: \(deviceNameSubstring)") |
|
monitorInputChanges(deviceNameSubstring: deviceNameSubstring) |
|
RunLoop.current.run() |
|
} |
|
|
|
main() |