Search code examples
swiftavaudioengineavaudiofile

Setting AVAudioFile `framePosition` causes crash


Whenever I set the framePosition of an AVAudioFile to something other than the current framePosition I get a crash:

error - 6658

which I looked up in the Apple Docs and is:

kExtAudioFileError_InvalidSeek: An attempt to write, or an offset, is out of bounds.

This is strange to me because I can confirm that the positions I am trying to set as framePosition are smaller than the AVAudioFile's length, and above 0, so the frame should not be out of bounds.

Code:

  // Start Recording to file
  func startRecording(atTimeInFile: TimeInterval) throws {
    let tapNode: AVAudioNode = mixerNode
    let format = tapNode.outputFormat(forBus: 0)
    let semaphore = DispatchSemaphore(value: 0)
    
    duration = 0
    startTime = atTimeInFile
    
    // Check to see if recording file already has content
    
    let f: AVAudioFile? = {
      if FileManager.default.fileExists(atPath: FileManagerHelper.recordingLocalURL().relativePath) {
        return try? AVAudioFile(forReading: FileManagerHelper.recordingLocalURL())
      } else {
        return nil
      }
    }()
    
    if let f = f {
      // The file we opened for writing already has data written to it
      // Let's load the buffer data in from that the file
      guard let bufferData = AVAudioPCMBuffer(pcmFormat: f.processingFormat, frameCapacity: AVAudioFrameCount(f.length)) else {
        throw NSError(domain: "Failed to load the content of the existing file.", code: -10, userInfo: nil)
      }
      
      bufferData.frameLength = bufferData.frameCapacity
      
      do {
        try f.read(into: bufferData)
      } catch {
        throw NSError(domain: "Failed to read from the content of the existing file.", code: -20, userInfo: nil)
      }
      
      // AVAudioFile uses the Core Audio Format (CAF) to write to disk.
      // So we're using the caf file extension.
      file = try AVAudioFile(forWriting: FileManagerHelper.recordingLocalURL(), settings: format.settings)
      
      do {
        file!.framePosition = 0
        try self.file!.write(from: bufferData)
      } catch {
        throw NSError(domain: "Failed to write from the content of the existing file.", code: -30, userInfo: nil)
      }
    }
    
    else {
      file = try AVAudioFile(forWriting: FileManagerHelper.recordingLocalURL(), settings: format.settings)
    }

    if file!.duration == 0 {
      file!.framePosition = AVAudioFramePosition(0)
    } else {
      let percentThrough = CGFloat(atTimeInFile) / CGFloat(file!.duration)
      let pos = AVAudioFramePosition(percentThrough * CGFloat(file!.length))
      
      file!.framePosition = pos // CRASH OCCURS HERE
    }
    
    tapNode.installTap(onBus: 0, bufferSize: bufferSize, format: format, block: {
      (buffer, _) in
      do {
        buffer.frameLength = 1024 // Tap is now called every 40ms. By default, tap is called every 0.375s
        try self.file!.write(from: buffer)
        self.delegate?.recorderDidRecieveAudioBuffer(self, buffer: buffer)
        semaphore.signal()
      } catch {
        log("Error writing mic data to file.", msgType: .error)
      }
    })

    try engine.start()

    semaphore.wait()
    state = .recording
    startObservingTime()
  }

This is a full description of the crash:

    ExtAudioFile.cpp:1084:Seek: about to throw -66568: seek to frame in audio file
2020-11-09 18:01:46.032612-0800 Application[15316:800908] [avae]            AVAEInternal.h:109   [AVAudioFile.mm:515:-[AVAudioFile setFramePosition:]: (ExtAudioFileSeek(_imp->_extAudioFile, pos)): error -66568
2020-11-09 18:01:46.034491-0800 Application[15316:800908] *** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error -66568'
*** First throw call stack:
(0x187b1c878 0x19c072c50 0x187a22000 0x197aa3e48 0x197b4270c 0x1004fc164 0x10048b43c 0x10048b198 0x10048b254 0x18a010e54 0x18a01a7b8 0x18a0173f4 0x18a01696c 0x18a00aa98 0x18a009e44 0x18a5111b4 0x18a4ea4ec 0x18a574488 0x18a577440 0x18a56e8ec 0x187a9876c 0x187a98668 0x187a97960 0x187a91a8c 0x187a9121c 0x19eb10784 0x18a4ca200 0x18a4cfa74 0x10048f270 0x1877516c0)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error -66568'
terminating with uncaught exception of type NSException

UPDATE 1: I noticed that I can set the framePosition of f, the AVAudioFile that was opened forReading, but I still can't set the frame position of file, the AVAudioFile opened for writing with the same URL.

UPDATE 2: I tried writing the file to a separate URL than I read it from by replacing

file = try AVAudioFile(forWriting: FileManagerHelper.recordingLocalURL(), settings: format.settings)

With

file = try AVAudioFile(forWriting: FileManagerHelper.recordingLocalURL2(), settings: format.settings)

In the if let f = f block.

However, I still get the out of bounds error when setting fame position.


Solution

  • I suspect this is because you first write, then try to overwrite later. Perhaps try reading in the data and then writing it back.

    I also added in when you create the writing file to use the same format, if it's already defined, in case that was the issue.

      // Start Recording to file
      func startRecording(atTimeInFile: TimeInterval) throws {
        let tapNode: AVAudioNode = mixerNode
        let format = tapNode.outputFormat(forBus: 0)
        let semaphore = DispatchSemaphore(value: 0)
        
        duration = 0
        startTime = atTimeInFile
    
        // Check to see if recording file already has content
        let f: AVAudioFile? = {
          if FileManager.default.fileExists(atPath: FileManagerHelper.recordingLocalURL().relativePath) {
            return try? AVAudioFile(forReading: FileManagerHelper.recordingLocalURL())
          } else {
            return nil
          }
        }()
        
        // AVAudioFile uses the Core Audio Format (CAF) to write to disk.
        // So we're using the caf file extension.
        self.file = try? AVAudioFile(forWriting: FileManagerHelper.recordingLocalURL(),
                                     settings: format.settings,
                                     commonFormat: f?.fileFormat ?? format.commonFormat)
    
        guard let file = self.file else { throw NSError(domain: "Failed to open file for writing") }
    
        var pos = AVAudioFramePosition(0)
        if let dur = f?.duration,
           let len = f?.length {
          if dur != 0 {
            let percentThrough = CGFloat(atTimeInFile) / CGFloat(dur)
            pos = AVAudioFramePosition(percentThrough * CGFloat(len))
          }
        }
    
        tapNode.installTap(onBus: 0,
                           bufferSize: bufferSize,
                           format: format,
                           block: { (buffer, _) in
          do {
            if let fread = f {
              guard let bufferData = AVAudioPCMBuffer(pcmFormat: fread.processingFormat,
                                                      frameCapacity: AVAudioFrameCount(pos)) else {
                throw NSError(domain: "Failed to load the content of the existing file.",
                              code: 0,
                              userInfo: nil)
              }
    
              // read in the data before
              fread.framePosition = AVFramePosition(0)
              try fread.read(into: bufferData)
    
              // write it to file
              try file.write(from: bufferData)
    
              // now write the new data
              buffer.frameLength = 1024 // Tap is now called every 40ms. By default, tap is called every 0.375s
              try file.write(from: buffer)
    
              // now read in the rest if possible
              if file.framePosition < fread.length {
                guard let afterBufferData = AVAudioPCMBuffer(pcmFormat: fread.processingFormat,
                                                             frameCapacity: AVAudioFrameCount(fread.length - file.framePosition)) else {
                  throw NSError(domain: "Failed to load extra content of the existing file.",
                                code: 0,
                                userInfo: nil)
                }
    
                fread.framePosition = file.framePosition
                try fread.read(into: afterBufferData)
                try file.write(from: afterBufferData)
              }
            } else {
              // now write the new data if there wasn't pre-existing
              buffer.frameLength = 1024 // Tap is now called every 40ms. By default, tap is called every 0.375s
              file.write(from: buffer)
            }
    
            // end the closure
            self.delegate?.recorderDidRecieveAudioBuffer(self, buffer: buffer)
            semaphore.signal()
          } catch {
            semaphore.signal()
            log("Error writing mic data to file.", msgType: .error)
          }
        })
    
        try engine.start()
    
        _ = semaphore.wait()
        state = .recording
        startObservingTime()
      }
    

    Also, if you are doing this every 40ms then this could be super slow. I'd recommend refactoring to read all the "tap" buffer and then replicate the read old, write new, write old structure.