swiftaudioavaudioplayer

How to compress AVAudioPCMBuffer to Data for streaming and playback the compressed Data


I am trying to record my microphone, compress the recorded audio buffer, transfer the compressed buffer as bytes to another device, decompress the received bytes into a new buffer and finally playback that buffer.

The following code I use to record the microphone, and to test whether the compression and decompression works without issues (thus leaving out the data transfer part):

    // ... code to setup audioEngines for recording and playback

    tappableInputNode?.installTap(onBus: bus, bufferSize: 1024, format: format!) { (buffer, when) in
      guard let compressedData = compressPCMBuffer(buffer) else {
        print("Failed to compress buffer")
        return
      }

      guard let decompressedBuffer = decompressDataToPCMBuffer(compressedData) else {
        print("Failed to decompress data")
        return
      }

      let renderTime =
        playerNode.playerTime(
          forNodeTime: playerNode.lastRenderTime ?? AVAudioTime(hostTime: mach_absolute_time()))
        ?? AVAudioTime(hostTime: mach_absolute_time())

      let delayInSeconds = 0.1
      let futureHostTime = renderTime.hostTime + UInt64(delayInSeconds)
      let scheduledTime = AVAudioTime(hostTime: futureHostTime)

      playerNode.scheduleBuffer(decompressedBuffer, at: scheduledTime, completionCallbackType: .dataPlayedBack) { (type) in
        print("Buffer played back")
      }
      playerNode.play()
    }

When playing the initial buffer provided by the tap, everything works fine. But when I try to playback the decompressed Buffer (like in the code above), the only thing I hear is a fast clicking sound.

Recording of the audio output

For compressing the AVAudioPCMBuffer I wrote the following function:

public func compressPCMBuffer(_ buffer: AVAudioPCMBuffer) -> Data? {
  var error: NSError?
  var opusDesc = AudioStreamBasicDescription()
  opusDesc.mFormatID = kAudioFormatOpus
  opusDesc.mChannelsPerFrame = buffer.format.channelCount
  opusDesc.mSampleRate = buffer.format.sampleRate
  guard let audioFormat = AVAudioFormat(streamDescription: &opusDesc) else {
    print("Failed to create AVAudioFormat")
    return nil
  }
  guard let converter = AVAudioConverter(from: buffer.format, to: audioFormat) else {
    print("Failed to initialize AVAudioConverter")
    return nil
  }

  let outputBuffer = AVAudioCompressedBuffer(
    format: audioFormat, packetCapacity: 1, maximumPacketSize: 512
  )

  let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
    outStatus.pointee = .haveData
    return buffer
  }

  let status = converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)
  if status == .error || error != nil {
    print("Audio conversion failed: \(error?.localizedDescription ?? "Unknown error")")
    return nil
  }

  let packetCount = Int(outputBuffer.packetCount)
  if packetCount == 0 {
    print("No packets were produced during conversion")
    return nil
  }

  let data = outputBuffer.toData()
  
  return data
}

The returned data seems to be 160 - 180 bytes of size, ideal for my use case.

Then, for decompressing the Data to an AVAudioPCMBuffer I have this function:

public func decompressDataToPCMBuffer(_ rawData: Data) -> AVAudioPCMBuffer? {
  var opusDesc = AudioStreamBasicDescription()
  opusDesc.mFormatID = kAudioFormatOpus
  opusDesc.mChannelsPerFrame = 1
  opusDesc.mSampleRate = 48000
  guard let opusFormat = AVAudioFormat(streamDescription: &opusDesc) else {
    print("Failed to create Opus AVAudioFormat")
    return nil
  }

  let inputBuffer = AVAudioCompressedBuffer(
    format: opusFormat, packetCapacity: 1, maximumPacketSize: 512
  )
  inputBuffer.packetCount = 1
  inputBuffer.byteLength = UInt32(rawData.count)
  rawData.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) in
    let rawPointer = rawBufferPointer.baseAddress!
    inputBuffer.audioBufferList.pointee.mBuffers.mData!.copyMemory(
      from: rawPointer, byteCount: rawData.count
    )
  }

  guard
    let pcmFormat = AVAudioFormat(
      commonFormat: .pcmFormatInt16, sampleRate: opusDesc.mSampleRate,
      channels: AVAudioChannelCount(opusDesc.mChannelsPerFrame), interleaved: false
    )
  else {
    print("Failed to create PCM format")
    return nil
  }

  guard let converter = AVAudioConverter(from: opusFormat, to: pcmFormat) else {
    print("Failed to create AVAudioConverter")
    return nil
  }

  guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: pcmFormat, frameCapacity: 4800) else {
    print("Failed to create PCM buffer")
    return nil
  }

  var error: NSError? = nil
  let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
    outStatus.pointee = .haveData
    return inputBuffer
  }

  converter.convert(to: pcmBuffer, error: &error, withInputFrom: inputBlock)
  if let error = error {
    print("Error during conversion: \(error.localizedDescription)")
    return nil
  }

  return pcmBuffer
}

Both functions run correctly without any warnings or errors, but the played audio is corrupted / malformed, with the clicking sound I described earlier.

I have also tried other formats, like AAC, but I get the exact same clicking sound.

Any help is appreciated. Thank you!


Solution

  • I was unable to create my own compression / decompression functions. However, I was able to get it to work using a third party package. https://github.com/alta/swift-opus