Add System Default audio input mode
This commit is contained in:
parent
c2c955c18b
commit
c457cf89d7
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user