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

View File

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

View File

@ -18,9 +18,12 @@ struct AudioInputSettingsView: View {
VStack(spacing: 40) {
inputModeSection
if audioDeviceManager.inputMode == .custom {
switch audioDeviceManager.inputMode {
case .systemDefault:
systemDefaultSection
case .custom:
customDeviceSection
} else if audioDeviceManager.inputMode == .prioritized {
case .prioritized:
prioritizedDevicesSection
}
}
@ -54,6 +57,36 @@ 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 {
VStack(alignment: .leading, spacing: 20) {
HStack {
@ -69,11 +102,6 @@ struct AudioInputSettingsView: View {
.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) {
ForEach(audioDeviceManager.availableDevices, id: \.id) { device in
DeviceSelectionCard(
@ -240,6 +268,7 @@ struct InputModeCard: View {
private var icon: String {
switch mode {
case .systemDefault: return "display"
case .custom: return "mic.circle.fill"
case .prioritized: return "list.number"
}
@ -247,6 +276,7 @@ struct InputModeCard: View {
private var description: String {
switch mode {
case .systemDefault: return "Use your Mac's default input"
case .custom: return "Select a specific input device"
case .prioritized: return "Set up device priority order"
}