import Foundation import AVFoundation import CoreAudio import os @MainActor class Recorder: ObservableObject { private var recorder: AVAudioRecorder? private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "Recorder") private let deviceManager = AudioDeviceManager.shared private var deviceObserver: NSObjectProtocol? private var isReconfiguring = false private let mediaController = MediaController.shared @Published var audioMeter = AudioMeter(averagePower: 0, peakPower: 0) private var audioLevelCheckTask: Task? private var hasDetectedAudioInCurrentSession = false enum RecorderError: Error { case couldNotStartRecording } init() { setupDeviceChangeObserver() } private func setupDeviceChangeObserver() { deviceObserver = AudioDeviceConfiguration.createDeviceChangeObserver { [weak self] in Task { await self?.handleDeviceChange() } } } private func handleDeviceChange() async { guard !isReconfiguring else { return } isReconfiguring = true if recorder != nil { let currentURL = recorder?.url stopRecording() if let url = currentURL { do { try await startRecording(toOutputFile: url) } catch { logger.error("❌ Failed to restart recording after device change: \(error.localizedDescription)") } } } isReconfiguring = false } private func configureAudioSession(with deviceID: AudioDeviceID) async throws { try AudioDeviceConfiguration.setDefaultInputDevice(deviceID) } 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 Task { await mediaController.muteSystemAudio() } let deviceID = deviceManager.getCurrentDevice() if deviceID != 0 { do { try await configureAudioSession(with: deviceID) } catch { logger.warning("⚠️ Failed to configure audio session for device \(deviceID), attempting to continue: \(error.localizedDescription)") } } let recordSettings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: 16000.0, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsFloatKey: false, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsNonInterleaved: false ] do { recorder = try AVAudioRecorder(url: url, settings: recordSettings) recorder?.isMeteringEnabled = true if recorder?.record() == false { logger.error("❌ Could not start recording") throw RecorderError.couldNotStartRecording } audioLevelCheckTask?.cancel() Task { while recorder != nil { updateAudioMeter() try? await Task.sleep(nanoseconds: 33_000_000) } } audioLevelCheckTask = Task { let notificationChecks: [TimeInterval] = [2.0, 8.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() recorder?.stop() recorder = nil audioMeter = AudioMeter(averagePower: 0, peakPower: 0) Task { await mediaController.unmuteSystemAudio() } deviceManager.isRecordingActive = false } private func updateAudioMeter() { guard let recorder = recorder else { return } recorder.updateMeters() let averagePower = recorder.averagePower(forChannel: 0) let peakPower = recorder.peakPower(forChannel: 0) 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 } deinit { if let observer = deviceObserver { NotificationCenter.default.removeObserver(observer) } } } struct AudioMeter: Equatable { let averagePower: Double let peakPower: Double }