import Foundation import AVFoundation import CoreAudio import os actor Recorder { 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 enum RecorderError: Error { case couldNotStartRecording case deviceConfigurationFailed } init() { logger.info("Initializing Recorder") setupDeviceChangeObserver() } private func setupDeviceChangeObserver() { logger.info("Setting up device change observer") deviceObserver = AudioDeviceConfiguration.createDeviceChangeObserver { [weak self] in Task { await self?.handleDeviceChange() } } } private func handleDeviceChange() async { guard !isReconfiguring else { logger.warning("Device change already in progress, skipping") return } logger.info("Handling device change") isReconfiguring = true // If we're recording, we need to stop and restart with new device if recorder != nil { logger.info("Active recording detected during device change") let currentURL = recorder?.url let currentDelegate = recorder?.delegate stopRecording() // Wait briefly for the device change to take effect logger.info("Waiting for device change to take effect") try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds if let url = currentURL { do { logger.info("Attempting to restart recording with new device") try await startRecording(toOutputFile: url, delegate: currentDelegate) logger.info("Successfully reconfigured recording with new device") } catch { logger.error("Failed to restart recording after device change: \(error.localizedDescription)") } } } isReconfiguring = false logger.info("Device change handling completed") } private func configureAudioSession(with deviceID: AudioDeviceID) async throws { logger.info("Starting audio session configuration for device ID: \(deviceID)") // Add a small delay to ensure device is ready after system changes try? await Task.sleep(nanoseconds: 50_000_000) // 0.05 seconds do { // Get the audio format from the selected device let format = try AudioDeviceConfiguration.configureAudioSession(with: deviceID) logger.info("Got audio format - Sample rate: \(format.mSampleRate), Channels: \(format.mChannelsPerFrame)") // Configure the device for recording try AudioDeviceConfiguration.setDefaultInputDevice(deviceID) logger.info("Successfully set default input device") } catch { logger.error("Audio session configuration failed: \(error.localizedDescription)") logger.error("Device ID: \(deviceID)") if let deviceName = deviceManager.getDeviceName(deviceID: deviceID) { logger.error("Failed device name: \(deviceName)") } throw error } // Add another small delay to allow configuration to settle try? await Task.sleep(nanoseconds: 50_000_000) // 0.05 seconds if let deviceName = deviceManager.getDeviceName(deviceID: deviceID) { logger.info("Successfully configured recorder with device: \(deviceName) (ID: \(deviceID))") } } func startRecording(toOutputFile url: URL, delegate: AVAudioRecorderDelegate?) async throws { logger.info("Starting recording process") // Check if media is playing and pause it if needed let wasPaused = await mediaController.pauseMediaIfPlaying() if wasPaused { logger.info("Media playback paused for recording") } // Get the current selected device let deviceID = deviceManager.getCurrentDevice() if deviceID != 0 { do { logger.info("Configuring audio session with device ID: \(deviceID)") if let deviceName = deviceManager.getDeviceName(deviceID: deviceID) { logger.info("Attempting to configure device: \(deviceName)") } try await configureAudioSession(with: deviceID) logger.info("Successfully configured audio session") } catch { logger.error("Failed to configure audio device: \(error.localizedDescription), Device ID: \(deviceID)") if let deviceName = deviceManager.getDeviceName(deviceID: deviceID) { logger.error("Failed device name: \(deviceName)") } logger.info("Falling back to default device") } } else { logger.info("Using default audio device (no custom device selected)") } logger.info("Setting up recording with settings: 16000Hz, 1 channel, PCM format") let recordSettings: [String : Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: 16000.0, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] do { logger.info("Initializing AVAudioRecorder with URL: \(url.path)") let recorder = try AVAudioRecorder(url: url, settings: recordSettings) recorder.delegate = delegate logger.info("Attempting to start recording...") if recorder.record() { logger.info("Recording started successfully") self.recorder = recorder } else { logger.error("Failed to start recording - recorder.record() returned false") logger.error("Current device ID: \(deviceID)") if let deviceName = deviceManager.getDeviceName(deviceID: deviceID) { logger.error("Current device name: \(deviceName)") } // Resume media if we paused it but failed to start recording await mediaController.resumeMediaIfPaused() throw RecorderError.couldNotStartRecording } } catch { logger.error("Error creating AVAudioRecorder: \(error.localizedDescription)") logger.error("Recording settings used: \(recordSettings)") logger.error("Output URL: \(url.path)") // Resume media if we paused it but failed to start recording await mediaController.resumeMediaIfPaused() throw error } } func stopRecording() { logger.info("Stopping recording") recorder?.stop() recorder?.delegate = nil // Remove delegate recorder = nil // Force a device change notification to trigger system audio profile reset logger.info("Triggering audio device change notification") NotificationCenter.default.post(name: NSNotification.Name("AudioDeviceChanged"), object: nil) // Resume media if we paused it Task { await mediaController.resumeMediaIfPaused() } logger.info("Recording stopped successfully") } deinit { logger.info("Deinitializing Recorder") if let observer = deviceObserver { NotificationCenter.default.removeObserver(observer) } } }