Skip to content

Instantly share code, notes, and snippets.

@coreequip
Last active February 27, 2025 14:04
Show Gist options
  • Save coreequip/71db296b780b677eaaa565decae99b68 to your computer and use it in GitHub Desktop.
Save coreequip/71db296b780b677eaaa565decae99b68 to your computer and use it in GitHub Desktop.
MicSwitchDetect™ - A little daemon that detect audio input source change and sets the input source back to the users choice.

MicSwitchDetect™

A little daemon that detect audio input source change and sets the input source back to the users choice.

1. Compile mic-switch-detect.swift

swiftc mic-switch-detect.swift

Should create a mic-switch-detect file.

2. Make executable

chmod +x ./mic-switch-detect

3. Edit and place the PLIST

  • Edit line 10 to point to your in 1. compiled mic-switch-detect binary.
  • Edit line 11 with a (partial) name of your microphone.

Place the plist file in ~/Library/LaunchAgents folder.

4. Run daemon

Run in shell:

launchctl load ~/Library/LaunchAgents/de.corequip.mic-switch-detect.plist

Have fun. 🥳

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>de.corequip.micswitch</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/your/mic-switch-detect</string>
<string>MyFancyMic</string>
</array>
<key>KeepAlive</key>
<true />
<key>RunAtLoad</key>
<true />
</dict>
</plist>
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment