A talk about the details to create an app which records audio
AVFoundation &Audio CaptureCan you hear me?Ronan Rodrigo Nunes iOS Software Engineer @ Uber ronanrodrigo.dev
View Slide
Straight forward implementation
Is simple right?let recordingSession = AVAudioSession.sharedInstance()audioSession.setCategory(.playAndRecord, mode: .default, options:[.mixWithOthers])recordingSession.setActive(true)recordingSession.requestRecordPermission { (allowed) inaudioRecorder = AVAudioRecorder(url: filename, settings: settings)audioRecorder?.record()}
But…let recordingSession = AVAudioSession.sharedInstance()
I see AVAudioEngine…let audioEngine = AVAudioEngine()let inputNode = audioEngine.inputNodelet inputFormat = inputNode.inputFormat(forBus: 0)inputNode.installTap(onBus: 0,bufferSize: size,format: inputFormat,block: handle(buffer:time:))
⚠ Terminating app due to uncaughtexception com.apple.coreaudio.avfaudioreason: required condition is false:format.sampleRate == hwFormat.sampleRate
A route changeNotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification).compactMap { notification -> AVAudioSession.RouteChangeReason? in //... }.sink(receiveValue: { [weak self] routeChangeReason inswitch routeChangeReason {case .unknown: Logger.log("🏴☠ unknown")case .newDeviceAvailable: Logger.log("🏴☠ newDeviceAvailable")case .oldDeviceUnavailable: Logger.log("🏴☠ oldDeviceUnavailable")case .override: Logger.log("🏴☠ override")case .wakeFromSleep: Logger.log("🏴☠ wakeFromSleep")case .noSuitableRouteForCategory: Logger.log("🏴☠ noSuitableRouteForCategory")case .routeConfigurationChange: Logger.log("🏴☠ routeConfigurationChange")case .categoryChange: Logger.log("🏴☠ categoryChange")}})
Time to reset the captureaudioEngine.inputNode.removeTap(onBus: 0)notificationCancellable?.cancel()notificationCancellable = nilaudioEngine.stop()audioEngine = AVAudioEngine()setupAudioSession()startCapture()
Seems to be fixed…
Dumping the Audio Sessionprivate func dumpAudioSession() -> String {return """preferredSampleRate: \(audioSession.preferredSampleRate)preferredInputNumberOfChannels: \(audioSession.preferredInputNumberOfChannels)maximumInputNumberOfChannels: \(audioSession.maximumInputNumberOfChannels)inputNumberOfChannels: \(audioSession.inputNumberOfChannels)isInputAvailable: \(audioSession.isInputAvailable)inputDataSources: \((audioSession.inputDataSources?.map { "\($0.dataSourceName)" }))inputDataSource: \(audioSession.inputDataSource?.dataSourceName)inputSelectedDatasource: \(audioSession.currentRoute.inputs.compactMap { $0.selectedDataSource?.dataSourceName })sampleRate: \(audioSession.sampleRate)preferredOutputNumberOfChannels: \(audioSession.preferredOutputNumberOfChannels)outputSelectedDatasource: \(audioSession.currentRoute.outputs.compactMap { $0.selectedDataSource?.dataSourceName })outputDataSources: \(audioSession.outputDataSources)outputDataSource: \(audioSession.outputDataSource)"""}
Now restartif dumpAudioSession() == requiredConfig { return }audioEngine.inputNode.removeTap(onBus: 0)notificationCancellable?.cancel()notificationCancellable = nilaudioEngine.stop()audioEngine = AVAudioEngine()setupAudioSession()startCapture()
A new issue
A new issueAVAudioSession is a singleton!
Audio resource providerengineProvider.engine(for: consumer).compactMap { availability -> AudioResourceAudioEngine? inswitch availability {case let .available(engine): return engine//...}}.subscribe(onNext: { engine intry? engine.inputNode.installTap { buffer, time in//...}})