import Foundation import AVFoundation import CoreAudio import os @MainActor class Recorder: NSObject, ObservableObject { private var recorder: CoreAudioRecorder? private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "Recorder") private let deviceManager = AudioDeviceManager.shared private var deviceObserver: NSObjectProtocol? private var deviceSwitchObserver: NSObjectProtocol? private var isReconfiguring = false private let mediaController = MediaController.shared private let playbackController = PlaybackController.shared @Published var audioMeter = AudioMeter(averagePower: 0, peakPower: 0) private var audioLevelCheckTask: Task? private var audioMeterUpdateTask: Task? private var audioRestorationTask: Task? private var hasDetectedAudioInCurrentSession = false enum RecorderError: Error { case couldNotStartRecording } override init() { super.init() setupDeviceChangeObserver() setupDeviceSwitchObserver() } private func setupDeviceChangeObserver() { deviceObserver = AudioDeviceConfiguration.createDeviceChangeObserver { [weak self] in Task { await self?.handleDeviceChange() } } } private func setupDeviceSwitchObserver() { deviceSwitchObserver = NotificationCenter.default.addObserver( forName: .audioDeviceSwitchRequired, object: nil, queue: .main ) { [weak self] notification in Task { await self?.handleDeviceSwitchRequired(notification) } } } private func handleDeviceChange() async { guard !isReconfiguring else { return } guard recorder != nil else { return } isReconfiguring = true try? await Task.sleep(nanoseconds: 200_000_000) await MainActor.run { NotificationCenter.default.post(name: .toggleMiniRecorder, object: nil) } isReconfiguring = false } private func handleDeviceSwitchRequired(_ notification: Notification) async { guard !isReconfiguring else { return } guard let recorder = recorder else { return } guard let userInfo = notification.userInfo, let newDeviceID = userInfo["newDeviceID"] as? AudioDeviceID else { logger.error("Device switch notification missing newDeviceID") return } // Prevent concurrent device switches and handleDeviceChange() interference isReconfiguring = true defer { isReconfiguring = false } logger.notice("🎙️ Device switch required: switching to device \(newDeviceID)") do { try recorder.switchDevice(to: newDeviceID) // Notify user about the switch if let deviceName = deviceManager.availableDevices.first(where: { $0.id == newDeviceID })?.name { await MainActor.run { NotificationManager.shared.showNotification( title: "Switched to: \(deviceName)", type: .info ) } } logger.notice("🎙️ Successfully switched recording to device \(newDeviceID)") } catch { logger.error("❌ Failed to switch device: \(error.localizedDescription)") // If switch fails, stop recording and notify user await handleRecordingError(error) } } func startRecording(toOutputFile url: URL) async throws { deviceManager.isRecordingActive = true let currentDeviceID = deviceManager.getCurrentDevice() let lastDeviceID = UserDefaults.standard.string(forKey: "lastUsedMicrophoneDeviceID") if String(currentDeviceID) != lastDeviceID { if let deviceName = deviceManager.availableDevices.first(where: { $0.id == currentDeviceID })?.name { await MainActor.run { NotificationManager.shared.showNotification( title: "Using: \(deviceName)", type: .info ) } } } UserDefaults.standard.set(String(currentDeviceID), forKey: "lastUsedMicrophoneDeviceID") hasDetectedAudioInCurrentSession = false let deviceID = deviceManager.getCurrentDevice() do { let coreAudioRecorder = CoreAudioRecorder() recorder = coreAudioRecorder try coreAudioRecorder.startRecording(toOutputFile: url, deviceID: deviceID) audioRestorationTask?.cancel() audioRestorationTask = nil Task { [weak self] in guard let self = self else { return } await self.playbackController.pauseMedia() _ = await self.mediaController.muteSystemAudio() } audioLevelCheckTask?.cancel() audioMeterUpdateTask?.cancel() audioMeterUpdateTask = Task { while recorder != nil && !Task.isCancelled { updateAudioMeter() try? await Task.sleep(nanoseconds: 17_000_000) } } audioLevelCheckTask = Task { let notificationChecks: [TimeInterval] = [5.0, 12.0] for delay in notificationChecks { try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) if Task.isCancelled { return } if self.hasDetectedAudioInCurrentSession { return } await MainActor.run { NotificationManager.shared.showNotification( title: "No Audio Detected", type: .warning ) } } } } catch { logger.error("Failed to create audio recorder: \(error.localizedDescription)") stopRecording() throw RecorderError.couldNotStartRecording } } func stopRecording() { audioLevelCheckTask?.cancel() audioMeterUpdateTask?.cancel() recorder?.stopRecording() recorder = nil audioMeter = AudioMeter(averagePower: 0, peakPower: 0) audioRestorationTask = Task { await mediaController.unmuteSystemAudio() await playbackController.resumeMedia() } deviceManager.isRecordingActive = false } private func handleRecordingError(_ error: Error) async { logger.error("❌ Recording error occurred: \(error.localizedDescription)") // Stop the recording stopRecording() // Notify the user about the recording failure await MainActor.run { NotificationManager.shared.showNotification( title: "Recording Failed: \(error.localizedDescription)", type: .error ) } } private func updateAudioMeter() { guard let recorder = recorder else { return } let averagePower = recorder.averagePower let peakPower = recorder.peakPower let minVisibleDb: Float = -60.0 let maxVisibleDb: Float = 0.0 let normalizedAverage: Float if averagePower < minVisibleDb { normalizedAverage = 0.0 } else if averagePower >= maxVisibleDb { normalizedAverage = 1.0 } else { normalizedAverage = (averagePower - minVisibleDb) / (maxVisibleDb - minVisibleDb) } let normalizedPeak: Float if peakPower < minVisibleDb { normalizedPeak = 0.0 } else if peakPower >= maxVisibleDb { normalizedPeak = 1.0 } else { normalizedPeak = (peakPower - minVisibleDb) / (maxVisibleDb - minVisibleDb) } let newAudioMeter = AudioMeter(averagePower: Double(normalizedAverage), peakPower: Double(normalizedPeak)) if !hasDetectedAudioInCurrentSession && newAudioMeter.averagePower > 0.01 { hasDetectedAudioInCurrentSession = true } audioMeter = newAudioMeter } // MARK: - Cleanup deinit { audioLevelCheckTask?.cancel() audioMeterUpdateTask?.cancel() audioRestorationTask?.cancel() if let observer = deviceObserver { NotificationCenter.default.removeObserver(observer) } if let observer = deviceSwitchObserver { NotificationCenter.default.removeObserver(observer) } } } struct AudioMeter: Equatable { let averagePower: Double let peakPower: Double }