Search code examples
iosswiftavfoundationcore-audioaudioqueueservices

Audio Queue Services Player in Swift isn't calling callback


I've been playing around with Audio Queue Services for about a week and I've written a swift version of from the Apple Audio Queue Services Guide. I'm recording in Linear PCM and saving to disk with this method:

AudioFileCreateWithURL(url, kAudioFileWAVEType, &format,
                                          AudioFileFlags.dontPageAlignAudioData.union(.eraseFile), &audioFileID)

My AudioQueueOutputCallback isn't being called even though I can verify that my bufferSize is seemingly large enough and that it's getting passed actual data. I'm not getting any OSStatus errors and it seems like everything should work. Theres very little in the way of Swift written AudioServiceQueues and should I get this working I'd be happy to open the rest of my code.

Any and all suggestions welcome!

class SVNPlayer: SVNPlayback {

  var state: PlayerState!

  private let callback: AudioQueueOutputCallback = { aqData, inAQ, inBuffer in

    guard let userData = aqData else { return }
    let audioPlayer = Unmanaged<SVNPlayer>.fromOpaque(userData).takeUnretainedValue()

    guard audioPlayer.state.isRunning,
      let queue = audioPlayer.state.mQueue else { return }

    var buffer = inBuffer.pointee // dereference pointers

    var numBytesReadFromFile: UInt32 = 0
    var numPackets = audioPlayer.state.mNumPacketsToRead
    var mPacketDescIsNil = audioPlayer.state.mPacketDesc == nil // determine if the packetDesc

    if mPacketDescIsNil {
      audioPlayer.state.mPacketDesc = AudioStreamPacketDescription(mStartOffset: 0, mVariableFramesInPacket: 0, mDataByteSize: 0)
    }

    AudioFileReadPacketData(audioPlayer.state.mAudioFile, false, &numBytesReadFromFile, // read the packet at the saved file
      &audioPlayer.state.mPacketDesc!, audioPlayer.state.mCurrentPacket,
      &numPackets, buffer.mAudioData)

    if numPackets > 0 {
      buffer.mAudioDataByteSize = numBytesReadFromFile
      AudioQueueEnqueueBuffer(queue, inBuffer, mPacketDescIsNil ? numPackets : 0,
                              &audioPlayer.state.mPacketDesc!)
      audioPlayer.state.mCurrentPacket += Int64(numPackets)
    } else {
      AudioQueueStop(queue, false)
      audioPlayer.state.isRunning = false
    }
  }

  init(inputPath: String, audioFormat: AudioStreamBasicDescription, numberOfBuffers: Int) throws {
    super.init()
    var format = audioFormat
    let pointer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) // get an unmananged reference to self

    guard let audioFileUrl = CFURLCreateFromFileSystemRepresentation(nil,
                                                                     inputPath,
                                                                     CFIndex(strlen(inputPath)), false) else {
                                                                      throw MixerError.playerInputPath }

    var audioFileID: AudioFileID?

    try osStatus { AudioFileOpenURL(audioFileUrl, AudioFilePermissions.readPermission, 0, &audioFileID) }

    guard audioFileID != nil else { throw MixerError.playerInputPath }

    state = PlayerState(mDataFormat: audioFormat, // setup the player state with mostly initial values
      mQueue: nil,
      mAudioFile: audioFileID!,
      bufferByteSize: 0,
      mCurrentPacket: 0,
      mNumPacketsToRead: 0,
      isRunning: false,
      mPacketDesc: nil,
      onError: nil)

    var dataFormatSize = UInt32(MemoryLayout<AudioStreamBasicDescription>.stride)

    try osStatus { AudioFileGetProperty(audioFileID!, kAudioFilePropertyDataFormat, &dataFormatSize, &state.mDataFormat) }

    var queue: AudioQueueRef?

    try osStatus { AudioQueueNewOutput(&format, callback, pointer, CFRunLoopGetCurrent(), CFRunLoopMode.commonModes.rawValue, 0, &queue) } // setup output queue

    guard queue != nil else { throw MixerError.playerOutputQueue }

    state.mQueue = queue // add to playerState

    var maxPacketSize = UInt32()
    var propertySize = UInt32(MemoryLayout<UInt32>.stride)

    try osStatus { AudioFileGetProperty(state.mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize) }

    deriveBufferSize(maxPacketSize: maxPacketSize, seconds: 0.5, outBufferSize: &state.bufferByteSize, outNumPacketsToRead: &state.mNumPacketsToRead)

    let isFormatVBR = state.mDataFormat.mBytesPerPacket == 0  || state.mDataFormat.mFramesPerPacket == 0

    if isFormatVBR { //Allocating Memory for a Packet Descriptions Array
      let size =  UInt32(MemoryLayout<AudioStreamPacketDescription>.stride)
      state.mPacketDesc = AudioStreamPacketDescription(mStartOffset: 0,
                                                       mVariableFramesInPacket: state.mNumPacketsToRead,
                                                       mDataByteSize: size)


    } // if CBR it stays set to null

    for _ in 0..<numberOfBuffers {  // Allocate and Prime Audio Queue Buffers
      let bufferRef = UnsafeMutablePointer<AudioQueueBufferRef?>.allocate(capacity: 1)
      let foo = state.mDataFormat.mBytesPerPacket * 1024 / UInt32(numberOfBuffers)
      try osStatus { AudioQueueAllocateBuffer(state.mQueue!, foo, bufferRef) } // allocate the buffer

      if let buffer = bufferRef.pointee {
        AudioQueueEnqueueBuffer(state.mQueue!, buffer, 0, nil)
      }
    }

    let gain: Float32 = 1.0  // Set an Audio Queue’s Playback Gain
    try osStatus { AudioQueueSetParameter(state.mQueue!, kAudioQueueParam_Volume, gain) }
  }

  func start() throws {
    state.isRunning = true // Start and Run an Audio Queue
    try osStatus { AudioQueueStart(state.mQueue!, nil) }
    while state.isRunning {
      CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 0.25, false)
    }
    CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 1.0, false)
    state.isRunning = false
  }

  func stop() throws {
    guard state.isRunning,
      let queue = state.mQueue else { return }
    try osStatus { AudioQueueStop(queue, true) }
    try osStatus { AudioQueueDispose(queue, true) }
    try osStatus { AudioFileClose(state.mAudioFile) }

    state.isRunning = false
  }


  private func deriveBufferSize(maxPacketSize: UInt32, seconds: Float64, outBufferSize: inout UInt32, outNumPacketsToRead: inout UInt32){
    let maxBufferSize = UInt32(0x50000)
    let minBufferSize = UInt32(0x4000)

    if state.mDataFormat.mFramesPerPacket != 0 {
      let numPacketsForTime: Float64 = state.mDataFormat.mSampleRate / Float64(state.mDataFormat.mFramesPerPacket) * seconds
      outBufferSize = UInt32(numPacketsForTime) * maxPacketSize
    } else {
      outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize
    }

    if outBufferSize > maxBufferSize && outBufferSize > maxPacketSize {
      outBufferSize = maxBufferSize

    } else if  outBufferSize < minBufferSize {
      outBufferSize = minBufferSize
    }

    outNumPacketsToRead = outBufferSize / maxPacketSize
  }
}

My player state struct is :

struct PlayerState: PlaybackState {
  var mDataFormat: AudioStreamBasicDescription
  var mQueue: AudioQueueRef?
  var mAudioFile: AudioFileID
  var bufferByteSize: UInt32
  var mCurrentPacket: Int64
  var mNumPacketsToRead: UInt32
  var isRunning: Bool
  var mPacketDesc: AudioStreamPacketDescription?
  var onError: ((Error) -> Void)?
}

Solution

  • Instead of enqueuing an empty buffer, try calling your callback so it enqueues a (hopefully) full buffer. I'm unsure about the runloop stuff, but I'm sure you know what you're doing.