Handle AVAudioEngineConfigurationChange when record audio with AVAudioEngine

Hi everyone, I was working on some code that involves recording audio with AVAudioEngine and got an issue that just crashes the app:

EXC_BREAKPOINT
Exception 6, Code 1, Subcode 4304279688
+0x009888 AudioRecordModule.setupAudioEngine
+0x009788
AudioRecordModule.setupAudioEngine
+0x00c5bc
AudioRecordModule.handleConfigurationChange

Below is the relevant code in the Recorder class.

public class AudioRecordModule: Module {

  private var audioEngine: AVAudioEngine?

  private func startRecording(options recordingOptions: RecordingOptions) {
                try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: .mixWithOthers)
                try AVAudioSession.sharedInstance().setActive(true)

outputFormat = AVAudioFormat(
                commonFormat: recordingOptions.bitDepth == 32 ? .pcmFormatInt32 : .pcmFormatInt16,
                sampleRate: Double(recordingOptions.sampleRate),
                channels: AVAudioChannelCount(recordingOptions.channels),
                interleaved: true
            )!

            let fileUri = URL(string: recordingOptions.fileUri)!

            let formatSettings: [String: Any] = [
                AVFormatIDKey: kAudioFormatMPEG4AAC,
                AVSampleRateKey: recordingOptions.sampleRate,
                AVNumberOfChannelsKey: recordingOptions.channels,
                AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_Constant,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
            ]

            self.recordedFile = try AVAudioFile(
                forWriting: fileUri,
                settings: formatSettings,
                commonFormat: outputFormat.commonFormat,
                interleaved: outputFormat.isInterleaved
            )

            if !hadSetupNotification {
                setupNotifications()
            }
  }

  func handleConfigurationChange() {
    DispatchQueue.main.async {
      self.releaseAudioEngine()
      self.setupAudioEngine()
      if self.state == "recording" {
        // we could attempt to keep recording
        do {
          try self.audioEngine?.start()
        } catch {
          self.internalPauseRecording()
          self.sendInterruptEvent()
        }
      }
    }
  }

  func setupNotifications() {
    nc.addObserver(
      forName: Notification.Name.AVAudioEngineConfigurationChange,
      object: nil,
      queue: nil
    ) { [weak self] _ in
      guard let weakself = self else {
        return
      }
      if weakself.state != "inactive" {
        weakself.handleConfigurationChange()
      }
    }
  }

  private func setupAudioEngine() {
    self.audioEngine = nil
    let audioEngine = AVAudioEngine()
    self.audioEngine = audioEngine

    let inputNode = audioEngine.inputNode
    let inputFormat = inputNode.inputFormat(forBus: 0)

    let converter = AVAudioConverter(from: inputFormat, to: outputFormat)!

    inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) {
      (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
      do {
        let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
          outStatus.pointee = AVAudioConverterInputStatus.haveData
          return buffer
        }
        let frameCapacity =
          AVAudioFrameCount(self.outputFormat.sampleRate) * buffer.frameLength
          / AVAudioFrameCount(buffer.format.sampleRate)
        let outputBuffer = AVAudioPCMBuffer(
          pcmFormat: self.outputFormat,
          frameCapacity: frameCapacity
        )!
        var error: NSError?
        converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)

        if let error = error {
          throw error
        } else {
          try self.recordedFile?.write(from: outputBuffer)
        }
      } catch {
        print(error)
      }
    }
  }

  private func releaseAudioEngine() {
    if let audioEngine = self.audioEngine {
      audioEngine.inputNode.removeTap(onBus: 0)
      audioEngine.stop()
    }
    audioEngine = nil
  }
}

Beside that, the record module works normally. It is just the configuration change that it does not handle well.

I understand that when configuration changes, I need to reinit the audio engine to have the correct input format (since the new config/audio device can have different sample rate and such). If I don't do that, the app also crashes perhaps due to the mismatch.

AVAudioRecorder is not an option for me.

Thank you for your help.

Handle AVAudioEngineConfigurationChange when record audio with AVAudioEngine
 
 
Q