Add System Default audio input mode

This commit is contained in:
Beingpax 2026-01-11 20:12:12 +05:45
parent c2c955c18b
commit c457cf89d7
3 changed files with 102 additions and 74 deletions

View File

@ -10,6 +10,7 @@ struct PrioritizedDevice: Codable, Identifiable {
} }
enum AudioInputMode: String, CaseIterable { enum AudioInputMode: String, CaseIterable {
case systemDefault = "System Default"
case custom = "Custom Device" case custom = "Custom Device"
case prioritized = "Prioritized" case prioritized = "Prioritized"
} }
@ -20,77 +21,77 @@ class AudioDeviceManager: ObservableObject {
@Published var selectedDeviceID: AudioDeviceID? @Published var selectedDeviceID: AudioDeviceID?
@Published var inputMode: AudioInputMode = .custom @Published var inputMode: AudioInputMode = .custom
@Published var prioritizedDevices: [PrioritizedDevice] = [] @Published var prioritizedDevices: [PrioritizedDevice] = []
var fallbackDeviceID: AudioDeviceID?
var isRecordingActive: Bool = false var isRecordingActive: Bool = false
static let shared = AudioDeviceManager() static let shared = AudioDeviceManager()
init() { init() {
setupFallbackDevice()
loadPrioritizedDevices() loadPrioritizedDevices()
if let savedMode = UserDefaults.standard.audioInputModeRawValue, if let savedMode = UserDefaults.standard.audioInputModeRawValue,
let mode = AudioInputMode(rawValue: savedMode) { let mode = AudioInputMode(rawValue: savedMode) {
inputMode = mode inputMode = mode
} else { } else {
inputMode = .custom inputMode = .systemDefault
} }
loadAvailableDevices { [weak self] in loadAvailableDevices { [weak self] in
self?.migrateFromSystemDefaultIfNeeded()
self?.initializeSelectedDevice() self?.initializeSelectedDevice()
} }
setupDeviceChangeNotifications() setupDeviceChangeNotifications()
} }
private func migrateFromSystemDefaultIfNeeded() { /// Returns the current system default input device from macOS
if let savedModeRaw = UserDefaults.standard.audioInputModeRawValue, func getSystemDefaultDevice() -> AudioDeviceID? {
savedModeRaw == "System Default" { var deviceID = AudioDeviceID(0)
logger.notice("🎙️ Migrating from System Default mode to Custom mode") var propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
if let fallbackID = fallbackDeviceID { mSelector: kAudioHardwarePropertyDefaultInputDevice,
selectedDeviceID = fallbackID mScope: kAudioObjectPropertyScopeGlobal,
if let device = availableDevices.first(where: { $0.id == fallbackID }) { mElement: kAudioObjectPropertyElementMain
UserDefaults.standard.selectedAudioDeviceUID = device.uid
logger.notice("🎙️ Migrated to Custom mode with device: \(device.name)")
}
}
UserDefaults.standard.audioInputModeRawValue = AudioInputMode.custom.rawValue
}
}
func setupFallbackDevice() {
let deviceID: AudioDeviceID? = getDeviceProperty(
deviceID: AudioObjectID(kAudioObjectSystemObject),
selector: kAudioHardwarePropertyDefaultInputDevice
) )
if let deviceID = deviceID { let status = AudioObjectGetPropertyData(
fallbackDeviceID = deviceID AudioObjectID(kAudioObjectSystemObject),
} else { &address,
logger.error("Failed to get fallback device") 0,
nil,
&propertySize,
&deviceID
)
guard status == noErr, deviceID != 0 else {
logger.error("Failed to get system default device: \(status)")
return nil
} }
return deviceID
}
func getSystemDefaultDeviceName() -> String? {
guard let deviceID = getSystemDefaultDevice() else { return nil }
return getDeviceName(deviceID: deviceID)
} }
private func initializeSelectedDevice() { private func initializeSelectedDevice() {
if inputMode == .prioritized { switch inputMode {
case .systemDefault:
logger.notice("🎙️ Using System Default mode")
case .prioritized:
selectHighestPriorityAvailableDevice() selectHighestPriorityAvailableDevice()
return case .custom:
} if let savedUID = UserDefaults.standard.selectedAudioDeviceUID {
if let device = availableDevices.first(where: { $0.uid == savedUID }) {
if let savedUID = UserDefaults.standard.selectedAudioDeviceUID { selectedDeviceID = device.id
if let device = availableDevices.first(where: { $0.uid == savedUID }) { } else {
selectedDeviceID = device.id logger.warning("🎙️ Saved device UID \(savedUID) is no longer available")
UserDefaults.standard.removeObject(forKey: UserDefaults.Keys.selectedAudioDeviceUID)
fallbackToDefaultDevice()
}
} else { } else {
logger.warning("🎙️ Saved device UID \(savedUID) is no longer available")
UserDefaults.standard.removeObject(forKey: UserDefaults.Keys.selectedAudioDeviceUID)
fallbackToDefaultDevice() fallbackToDefaultDevice()
} }
} else {
fallbackToDefaultDevice()
} }
} }
@ -179,7 +180,6 @@ class AudioDeviceManager: ObservableObject {
self.availableDevices = devices.map { ($0.id, $0.uid, $0.name) } self.availableDevices = devices.map { ($0.id, $0.uid, $0.name) }
if let currentID = self.selectedDeviceID, !devices.contains(where: { $0.id == currentID }) { if let currentID = self.selectedDeviceID, !devices.contains(where: { $0.id == currentID }) {
self.logger.warning("🎙️ Currently selected device is no longer available") self.logger.warning("🎙️ Currently selected device is no longer available")
// Skip auto-fallback during recording; handleDeviceListChange manages it
if !self.isRecordingActive { if !self.isRecordingActive {
if self.inputMode == .prioritized { if self.inputMode == .prioritized {
self.selectHighestPriorityAvailableDevice() self.selectHighestPriorityAvailableDevice()
@ -274,12 +274,17 @@ class AudioDeviceManager: ObservableObject {
inputMode = mode inputMode = mode
UserDefaults.standard.audioInputModeRawValue = mode.rawValue UserDefaults.standard.audioInputModeRawValue = mode.rawValue
if selectedDeviceID == nil { switch mode {
if inputMode == .custom { case .systemDefault:
break
case .custom:
if selectedDeviceID == nil {
if let firstDevice = availableDevices.first { if let firstDevice = availableDevices.first {
selectDevice(id: firstDevice.id) selectDevice(id: firstDevice.id)
} }
} else if inputMode == .prioritized { }
case .prioritized:
if selectedDeviceID == nil {
selectHighestPriorityAvailableDevice() selectHighestPriorityAvailableDevice()
} }
} }
@ -289,13 +294,13 @@ class AudioDeviceManager: ObservableObject {
func getCurrentDevice() -> AudioDeviceID { func getCurrentDevice() -> AudioDeviceID {
switch inputMode { switch inputMode {
case .systemDefault:
return getSystemDefaultDevice() ?? findBestAvailableDevice() ?? 0
case .custom: case .custom:
if let id = selectedDeviceID, isDeviceAvailable(id) { if let id = selectedDeviceID, isDeviceAvailable(id) {
return id return id
} else {
// Use smart device finding instead of stale fallback
return findBestAvailableDevice() ?? 0
} }
return findBestAvailableDevice() ?? 0
case .prioritized: case .prioritized:
let sortedDevices = prioritizedDevices.sorted { $0.priority < $1.priority } let sortedDevices = prioritizedDevices.sorted { $0.priority < $1.priority }
for device in sortedDevices { for device in sortedDevices {
@ -303,7 +308,6 @@ class AudioDeviceManager: ObservableObject {
return available.id return available.id
} }
} }
// Use smart device finding instead of stale fallback
return findBestAvailableDevice() ?? 0 return findBestAvailableDevice() ?? 0
} }
} }
@ -404,19 +408,19 @@ class AudioDeviceManager: ObservableObject {
loadAvailableDevices { [weak self] in loadAvailableDevices { [weak self] in
guard let self = self else { return } guard let self = self else { return }
// If recording is active, check if we need to switch devices if self.inputMode == .systemDefault {
self.notifyDeviceChange()
return
}
if self.isRecordingActive { if self.isRecordingActive {
guard let currentID = self.selectedDeviceID else { return } guard let currentID = self.selectedDeviceID else { return }
// Check if current recording device is still available
if !self.isDeviceAvailable(currentID) { if !self.isDeviceAvailable(currentID) {
self.logger.warning("🎙️ Recording device \(currentID) no longer available - requesting switch") self.logger.warning("🎙️ Recording device \(currentID) no longer available - requesting switch")
// Find next device based on input mode
let newDeviceID: AudioDeviceID? let newDeviceID: AudioDeviceID?
if self.inputMode == .prioritized { if self.inputMode == .prioritized {
// Respect priority order: find next available prioritized device
let sortedDevices = self.prioritizedDevices.sorted { $0.priority < $1.priority } let sortedDevices = self.prioritizedDevices.sorted { $0.priority < $1.priority }
let priorityDeviceID = sortedDevices.compactMap { device in let priorityDeviceID = sortedDevices.compactMap { device in
self.availableDevices.first(where: { $0.uid == device.id })?.id self.availableDevices.first(where: { $0.uid == device.id })?.id
@ -425,19 +429,15 @@ class AudioDeviceManager: ObservableObject {
if let deviceID = priorityDeviceID { if let deviceID = priorityDeviceID {
newDeviceID = deviceID newDeviceID = deviceID
} else { } else {
// No priority devices available, fall back to best available
self.logger.warning("🎙️ No priority devices available, using fallback") self.logger.warning("🎙️ No priority devices available, using fallback")
newDeviceID = self.findBestAvailableDevice() newDeviceID = self.findBestAvailableDevice()
} }
} else { } else {
// Custom mode: use best available device
newDeviceID = self.findBestAvailableDevice() newDeviceID = self.findBestAvailableDevice()
} }
if let deviceID = newDeviceID { if let deviceID = newDeviceID {
self.selectedDeviceID = deviceID self.selectedDeviceID = deviceID
// Notify recorder to switch devices mid-recording
NotificationCenter.default.post( NotificationCenter.default.post(
name: .audioDeviceSwitchRequired, name: .audioDeviceSwitchRequired,
object: nil, object: nil,
@ -445,14 +445,12 @@ class AudioDeviceManager: ObservableObject {
) )
} else { } else {
self.logger.error("No audio input devices available!") self.logger.error("No audio input devices available!")
// Stop recording since no device is available
NotificationCenter.default.post(name: .toggleMiniRecorder, object: nil) NotificationCenter.default.post(name: .toggleMiniRecorder, object: nil)
} }
} }
return return
} }
// Not recording - handle normally
if self.inputMode == .prioritized { if self.inputMode == .prioritized {
self.selectHighestPriorityAvailableDevice() self.selectHighestPriorityAvailableDevice()
} else if self.inputMode == .custom, } else if self.inputMode == .custom,

View File

@ -101,11 +101,11 @@ class SystemInfoService {
private func getCurrentAudioDevice() -> String { private func getCurrentAudioDevice() -> String {
let audioManager = AudioDeviceManager.shared let audioManager = AudioDeviceManager.shared
if let deviceID = audioManager.selectedDeviceID ?? audioManager.fallbackDeviceID, let deviceID = audioManager.getCurrentDevice()
let deviceName = audioManager.getDeviceName(deviceID: deviceID) { if deviceID != 0, let deviceName = audioManager.getDeviceName(deviceID: deviceID) {
return deviceName return deviceName
} }
return "System Default" return "Unknown"
} }
private func getAvailableAudioDevices() -> String { private func getAvailableAudioDevices() -> String {

View File

@ -17,10 +17,13 @@ struct AudioInputSettingsView: View {
private var mainContent: some View { private var mainContent: some View {
VStack(spacing: 40) { VStack(spacing: 40) {
inputModeSection inputModeSection
if audioDeviceManager.inputMode == .custom { switch audioDeviceManager.inputMode {
case .systemDefault:
systemDefaultSection
case .custom:
customDeviceSection customDeviceSection
} else if audioDeviceManager.inputMode == .prioritized { case .prioritized:
prioritizedDevicesSection prioritizedDevicesSection
} }
} }
@ -54,25 +57,50 @@ struct AudioInputSettingsView: View {
} }
} }
private var systemDefaultSection: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Current Device")
.font(.title2)
.fontWeight(.semibold)
HStack {
Image(systemName: "display")
.foregroundStyle(.secondary)
Text(audioDeviceManager.getSystemDefaultDeviceName() ?? "No device available")
.foregroundStyle(.primary)
Spacer()
Label("Active", systemImage: "wave.3.right")
.font(.caption)
.foregroundStyle(.green)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
Capsule()
.fill(.green.opacity(0.1))
)
}
.padding()
.background(CardBackground(isSelected: false))
}
}
private var customDeviceSection: some View { private var customDeviceSection: some View {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
HStack { HStack {
Text("Available Devices") Text("Available Devices")
.font(.title2) .font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
Spacer() Spacer()
Button(action: { audioDeviceManager.loadAvailableDevices() }) { Button(action: { audioDeviceManager.loadAvailableDevices() }) {
Label("Refresh", systemImage: "arrow.clockwise") Label("Refresh", systemImage: "arrow.clockwise")
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
Text("Note: Selecting a device here will override your Mac\'s system-wide default microphone.")
.font(.caption)
.foregroundColor(.secondary)
.padding(.bottom, 8)
VStack(spacing: 12) { VStack(spacing: 12) {
ForEach(audioDeviceManager.availableDevices, id: \.id) { device in ForEach(audioDeviceManager.availableDevices, id: \.id) { device in
@ -237,9 +265,10 @@ struct InputModeCard: View {
let mode: AudioInputMode let mode: AudioInputMode
let isSelected: Bool let isSelected: Bool
let action: () -> Void let action: () -> Void
private var icon: String { private var icon: String {
switch mode { switch mode {
case .systemDefault: return "display"
case .custom: return "mic.circle.fill" case .custom: return "mic.circle.fill"
case .prioritized: return "list.number" case .prioritized: return "list.number"
} }
@ -247,6 +276,7 @@ struct InputModeCard: View {
private var description: String { private var description: String {
switch mode { switch mode {
case .systemDefault: return "Use your Mac's default input"
case .custom: return "Select a specific input device" case .custom: return "Select a specific input device"
case .prioritized: return "Set up device priority order" case .prioritized: return "Set up device priority order"
} }