0

[TLDR: Receiving an ASSERTION FAILURE on CABufferList.h (find error at the bottom) when trying to save streamed audio data]

I am having trouble saving microphone audio that is streamed between devices using Multipeer Connectivity. So far I have two devices connected to each other using Multipeer Connectivity and have them sending messages and streams to each other.

Finally I have the StreamDelegate method

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    
    // create a buffer for capturing the inputstream data
    let bufferSize = 2048
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
    defer {
        buffer.deallocate()
    }
    
    var audioBuffer: AudioBuffer!
    var audioBufferList: AudioBufferList!
    
    switch eventCode {
        case .hasBytesAvailable:
            // if the input stream has bytes available
            // return the actual number of bytes placed in the buffer;
            let read = self.inputStream.read(buffer, maxLength: bufferSize)
            if read < 0 {
                //Stream error occured
                print(self.inputStream.streamError!)
            } else if read == 0 {
                //EOF
                break
            }
            
            guard let mData = UnsafeMutableRawPointer(buffer) else { return }
            audioBuffer = AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(read), mData: mData)
            audioBufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer)
            let audioBufferListPointer = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: read)
            audioBufferListPointer.pointee = audioBufferList
            
            DispatchQueue.main.async {
                if self.ezRecorder == nil {
                    self.recordAudio()
                }
                
                self.ezRecorder?.appendData(from: audioBufferListPointer, withBufferSize: UInt32(read))
            }
        
            print("hasBytesAvailable \(audioBuffer!)")
        case .endEncountered:
            print("endEncountered")
            if self.inputStream != nil {
                self.inputStream.delegate = nil
                self.inputStream.remove(from: .current, forMode: .default)
                self.inputStream.close()
                self.inputStream = nil
            }
        case .errorOccurred:
            print("errorOccurred")
        case .hasSpaceAvailable:
            print("hasSpaceAvailable")
        case .openCompleted:
            print("openCompleted")
        default:
            break
    }
}

I am getting the stream of data however when I try to save it as an audio file using EZRecorder, I get the following error message

[default]            CABufferList.h:184   ASSERTION FAILURE [(nBytes <= buf->mDataByteSize) != 0 is false]:

I suspect the error could be arising when I create AudioStreamBasicDescription for EZRecorder.

I understand there may be other errors here and I appreciate any suggestions to solve the bug and improve the code. Thanks

NSCoder
  • 1,594
  • 5
  • 25
  • 47
  • I suspect your buffer is getting deallocated (in the call for `defer`), before your stream is able to save - as your stream saving code is in an async block dispatched to main thread - which will run after the end of the function call. You may want to make a copy of the buffer and pass to your saving code. – mani Oct 24 '22 at 14:09
  • This sort of thing is best achieved with a circular buffer, where you can read into to the tail of the buffer and write from the head of the buffer. As the read of read/write is not matched and your writes are async. – mani Oct 24 '22 at 14:19

1 Answers1

0

EZAudio comes with TPCircularBuffer - use that.

Because writing the buffer to file is an async operation, this becomes a great use case for a circular buffer where we have one producer and one consumer.

Use the EZAudioUtilities where possible.

Update: EZRecorder write expects bufferSize to be number of frames to write and not bytes

So something like this should work:

class StreamDelegateInstance: NSObject {
  private static let MaxReadSize = 2048
  private static let BufferSize = MaxReadSize * 4
  
  private var availableReadBytesPtr = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
  private var availableWriteBytesPtr = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
  
  private var ezRecorder: EZRecorder?
  
  private var buffer =  UnsafeMutablePointer<TPCircularBuffer>.allocate(capacity: 1)
  private var inputStream: InputStream?
  
  init(inputStream: InputStream? = nil) {
    self.inputStream = inputStream
    super.init()
    EZAudioUtilities.circularBuffer(buffer, withSize: Int32(StreamDelegateInstance.BufferSize))
    ensureWriteStream()
  }
  
  deinit {
    EZAudioUtilities.freeCircularBuffer(buffer)
    buffer.deallocate()
    availableReadBytesPtr.deallocate()
    availableWriteBytesPtr.deallocate()
    self.ezRecorder?.closeAudioFile()
    self.ezRecorder = nil
  }
  
  private func ensureWriteStream() {
    guard self.ezRecorder == nil else { return }
    // stores audio to temporary folder
    let audioOutputPath = NSTemporaryDirectory() + "audioOutput2.aiff"
    let audioOutputURL = URL(fileURLWithPath: audioOutputPath)
    print(audioOutputURL)
    
//    let audioStreamBasicDescription = AudioStreamBasicDescription(mSampleRate: 44100.0, mFormatID: kAudioFormatLinearPCM, mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 1, mBitsPerChannel: 32, mReserved: 1081729024)
    
//    EZAudioUtilities.audioBufferList(withNumberOfFrames: <#T##UInt32#>,
//                                     numberOfChannels: 1,
//                                     interleaved: true)
    
    // if you don't need a custom format, consider using EZAudioUtilities.m4AFormat
    let format = EZAudioUtilities.aiffFormat(withNumberOfChannels: 1,
                                             sampleRate: 44800)
    self.ezRecorder = EZRecorder.init(url: audioOutputURL,
                                      clientFormat: format,
                                      fileType: .AIFF)
  }
  
  private func writeStream() {
    let ptr = TPCircularBufferTail(buffer, availableWriteBytesPtr)
    
    // ensure we have non 0 bytes to write - which should always be true, but you may want to refactor things
    guard availableWriteBytesPtr.pointee > 0 else { return }
    let framesToWrite = availableWriteBytesPtr.pointee / 4 // sizeof(float)
    let audioBuffer =  AudioBuffer(mNumberChannels: 1,
                                   mDataByteSize: UInt32(availableWriteBytesPtr.pointee),
                                   mData: ptr)
    let audioBufferList = AudioBufferList(mNumberBuffers: 1, mBuffers: audioBuffer)
    
    self.ezRecorder?.appendData(from: &audioBufferList,
                                withBufferSize: UInt32(framesToWrite))
    
    TPCircularBufferConsume(buffer, framesToWrite * 4)
  }
}

extension StreamDelegateInstance: StreamDelegate {
  
  func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case .hasBytesAvailable:
      // if the input stream has bytes available
      // return the actual number of bytes placed in the buffer;
      guard let ptr = TPCircularBufferHead(buffer, availableReadBytesPtr) else {
        print("couldn't get buffer ptr")
        break;
      }
      let bytedsToRead = min(Int(availableReadBytesPtr.pointee), StreamDelegateInstance.MaxReadSize)
      let mutablePtr = ptr.bindMemory(to: UInt8.self, capacity: Int(bytedsToRead))
      let bytesRead = self.inputStream?.read(mutablePtr,
                                             maxLength: bytedsToRead) ?? 0
      if bytesRead < 0 {
        //Stream error occured
        print(self.inputStream?.streamError! ?? "No bytes read")
        break
      } else if bytesRead == 0 {
        //EOF
        break
      }
      
      TPCircularBufferProduce(buffer, Int32(bytesRead))
      
      DispatchQueue.main.async { [weak self] in
        self?.writeStream()
      }
    case .endEncountered:
      print("endEncountered")
      if self.inputStream != nil {
        self.inputStream?.delegate = nil
        self.inputStream?.remove(from: .current, forMode: .default)
        self.inputStream?.close()
        self.inputStream = nil
      }
    case .errorOccurred:
      print("errorOccurred")
    case .hasSpaceAvailable:
      print("hasSpaceAvailable")
    case .openCompleted:
      print("openCompleted")
    default:
      break
    }
  }
}

mani
  • 3,068
  • 2
  • 14
  • 25
  • Hello @mani, thank you so much for you answer. I had not used circular buffers before. the following line `let bytesRead = self.inputStream?.read(ptr, maxLength: bytedsToRead` was generating an error `Cannot convert value of type 'UnsafeMutableRawPointer' to expected argument type 'UnsafeMutablePointer'` so I created a new variable `let mutablePtr = ptr.bindMemory(to: UInt8.self, capacity: 1)` and used that to `let bytesRead = self.inputStream?.read(mutablePtr, maxLength: bytedsToRead`. Which built successfully however ended up with the same ASSERTION FAILURE as before. Any suggestions. – NSCoder Oct 28 '22 at 14:54
  • @NSCoder Can you post a stack trace to your question? i.e. the trace of all the calls before the assertion failure happens. – mani Oct 28 '22 at 14:59
  • @NSCoder Also updated the answer. when calling `bindMemory` we need to specify capacity of the buffer – mani Oct 28 '22 at 15:06
  • @NSCoder Updated answer - tried to match formats and use aiff format. It works locally for me, but am not sending data over a multipeer connection. If above continues to not work, it would be great to get a sample project incl. multipeer connectivity to test this. Have a look at `microphone.audioStreamBasicDescription()` to see what format the microphone is receiving data in. – mani Oct 28 '22 at 21:33
  • Still not working btw – NSCoder Oct 29 '22 at 22:29
  • @NSCoder looking into it – mani Oct 30 '22 at 12:02