-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauto_switch_mic.swift
More file actions
311 lines (268 loc) · 10.3 KB
/
auto_switch_mic.swift
File metadata and controls
311 lines (268 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import AppKit
import CoreAudio
import ServiceManagement
// MARK: - Audio Helpers
func getInputDevices() -> [(name: String, id: AudioDeviceID)] {
var propAddr = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &propAddr, 0, nil, &dataSize)
let count = Int(dataSize) / MemoryLayout<AudioDeviceID>.size
var devices = [AudioDeviceID](repeating: 0, count: count)
AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &propAddr, 0, nil, &dataSize, &devices)
var result: [(String, AudioDeviceID)] = []
for device in devices {
// Check if device has input channels
var streamAddr = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreams,
mScope: kAudioObjectPropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)
var streamSize: UInt32 = 0
AudioObjectGetPropertyDataSize(device, &streamAddr, 0, nil, &streamSize)
if streamSize == 0 { continue }
var nameAddr = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyDeviceNameCFString,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var name: CFString = "" as CFString
var size = UInt32(MemoryLayout<CFString>.size)
AudioObjectGetPropertyData(device, &nameAddr, 0, nil, &size, &name)
result.append((name as String, device))
}
return result
}
func getDefaultInputDevice() -> AudioDeviceID {
var addr = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var deviceID: AudioDeviceID = 0
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &addr, 0, nil, &size, &deviceID)
return deviceID
}
func getDeviceName(_ deviceID: AudioDeviceID) -> String {
var nameAddr = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyDeviceNameCFString,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var name: CFString = "" as CFString
var size = UInt32(MemoryLayout<CFString>.size)
AudioObjectGetPropertyData(deviceID, &nameAddr, 0, nil, &size, &name)
return name as String
}
// MARK: - App Delegate
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private var statusItem: NSStatusItem!
private var listListenerAddr = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
private var defaultInputListenerAddr = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var targetDeviceName: String {
get { UserDefaults.standard.string(forKey: "targetDevice") ?? "Wireless Mic Rx" }
set {
UserDefaults.standard.set(newValue, forKey: "targetDevice")
switchToTarget()
}
}
var isEnabled: Bool {
get { UserDefaults.standard.object(forKey: "isEnabled") as? Bool ?? true }
set {
UserDefaults.standard.set(newValue, forKey: "isEnabled")
if newValue { switchToTarget() }
updateIcon()
}
}
// MARK: - Launch at Login
var launchAtLogin: Bool {
SMAppService.mainApp.status == .enabled
}
func setLaunchAtLogin(_ enabled: Bool) {
do {
if enabled {
try SMAppService.mainApp.register()
NSLog("Login item registered")
} else {
try SMAppService.mainApp.unregister()
NSLog("Login item unregistered")
}
} catch {
NSLog("Failed to update login item: %@", error.localizedDescription)
}
}
func applicationDidFinishLaunching(_ notification: Notification) {
// Migrate: remove old LaunchAgent plist if present
let oldPlist = NSHomeDirectory() + "/Library/LaunchAgents/com.auto-switch-mic.plist"
if FileManager.default.fileExists(atPath: oldPlist) {
try? FileManager.default.removeItem(atPath: oldPlist)
NSLog("Migrated: removed old LaunchAgent plist")
setLaunchAtLogin(true)
}
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let menu = NSMenu()
menu.delegate = self
statusItem.menu = menu
setupAudioListeners()
if isEnabled { switchToTarget() }
updateIcon()
NSLog("auto_switch_mic running (menu bar mode), target: %@", targetDeviceName)
}
// MARK: - Menu
func menuNeedsUpdate(_ menu: NSMenu) {
menu.removeAllItems()
let currentDefault = getDefaultInputDevice()
let currentName = getDeviceName(currentDefault)
// Current status
let statusText = NSMenuItem(title: "当前输入: \(currentName)", action: nil, keyEquivalent: "")
statusText.isEnabled = false
menu.addItem(statusText)
menu.addItem(NSMenuItem.separator())
// Enable/disable
let toggleItem = NSMenuItem(
title: isEnabled ? "已启用自动切换" : "已停用自动切换",
action: #selector(toggleEnabled),
keyEquivalent: ""
)
toggleItem.target = self
toggleItem.state = isEnabled ? .on : .off
menu.addItem(toggleItem)
// Launch at login
let loginItem = NSMenuItem(
title: "开机自动启动",
action: #selector(toggleLaunchAtLogin),
keyEquivalent: ""
)
loginItem.target = self
loginItem.state = launchAtLogin ? .on : .off
menu.addItem(loginItem)
menu.addItem(NSMenuItem.separator())
// Device list
let header = NSMenuItem(title: "目标麦克风:", action: nil, keyEquivalent: "")
header.isEnabled = false
menu.addItem(header)
let inputDevices = getInputDevices()
if inputDevices.isEmpty {
let noDevice = NSMenuItem(title: " (无可用输入设备)", action: nil, keyEquivalent: "")
noDevice.isEnabled = false
menu.addItem(noDevice)
} else {
for (name, _) in inputDevices {
let item = NSMenuItem(title: name, action: #selector(selectDevice(_:)), keyEquivalent: "")
item.target = self
item.representedObject = name
item.indentationLevel = 1
if name == targetDeviceName {
item.state = .on
}
menu.addItem(item)
}
}
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "退出", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
}
// MARK: - Actions
@objc func toggleEnabled() {
isEnabled = !isEnabled
}
@objc func toggleLaunchAtLogin() {
setLaunchAtLogin(!launchAtLogin)
}
@objc func selectDevice(_ sender: NSMenuItem) {
if let name = sender.representedObject as? String {
targetDeviceName = name
}
}
// MARK: - Icon
func updateIcon() {
guard let button = statusItem.button else { return }
let symbolName: String
if !isEnabled {
symbolName = "mic.slash"
} else if isTargetActive() {
symbolName = "mic.fill"
} else {
symbolName = "mic"
}
if let img = NSImage(systemSymbolName: symbolName, accessibilityDescription: "Auto Switch Mic") {
img.isTemplate = true
button.image = img
}
}
func isTargetActive() -> Bool {
let current = getDefaultInputDevice()
let name = getDeviceName(current)
return name == targetDeviceName
}
// MARK: - Audio Monitoring
func setupAudioListeners() {
// Watch device list changes (plug/unplug)
AudioObjectAddPropertyListenerBlock(
AudioObjectID(kAudioObjectSystemObject),
&listListenerAddr,
nil
) { [weak self] _, _ in
DispatchQueue.main.async {
guard let self = self, self.isEnabled else { return }
self.switchToTarget()
}
}
// Watch default input device changes
AudioObjectAddPropertyListenerBlock(
AudioObjectID(kAudioObjectSystemObject),
&defaultInputListenerAddr,
nil
) { [weak self] _, _ in
DispatchQueue.main.async {
guard let self = self, self.isEnabled else { return }
self.switchToTarget()
}
}
}
func switchToTarget() {
let devices = getInputDevices()
let currentDefault = getDefaultInputDevice()
for (name, deviceID) in devices {
if name == targetDeviceName {
if deviceID == currentDefault {
NSLog("Already using: %@", name)
updateIcon()
return
}
var addr = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var id = deviceID
AudioObjectSetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&addr, 0, nil,
UInt32(MemoryLayout<AudioDeviceID>.size), &id
)
NSLog("Switched input to: %@", name)
updateIcon()
return
}
}
NSLog("Target device not found: %@", targetDeviceName)
updateIcon()
}
}
// MARK: - Main
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()