4

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)?
}
aBikis
  • 323
  • 4
  • 16

1 Answers1

1

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.

Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • Hmm I'm not sure I understand. Do you mean after allocating each buffer I should call the callback instead of calling `EnqueueBuffer`? Wouldn't that mean that it would only be called for the number of buffers I have? I gave it a go and it doesn't seem to do anything on AudioQueueStart. Any other suggestions? Thanks! – aBikis May 25 '17 at 18:19
  • 1
    That's right, the callback calls `EnqueueBuffer` anyway, but if that's not helping can you post a link to a working code sample? – Rhythmic Fistman May 25 '17 at 21:17
  • Wow really? That would be incredible. I've actually updated the source code a bit. I've been able to play audio via the C code I've been mocking this swift file off of and I think I have it enqueuing correctly however there seems to be an issue reading from the packet data. I've included all relevant files and the problematic line is in `SVNPlayer` line 30. Thank you so much! https://gist.github.com/bikisDesign/57c58355c2cb4498595dab52f0ff0be8 – aBikis May 26 '17 at 21:27
  • or maybe I should repost in a separate question? – aBikis May 26 '17 at 21:31
  • 1
    Yes, let's debug `AudioFileReadPacketData` in a new question. You know `AudioQueue` is one of the grumpier APIs and that you could probably replace all this code with a dozen lines or so of `AVAudioEngine` code? – Rhythmic Fistman May 26 '17 at 23:02
  • hmm `AVAudioEngine` looks way higher level and easier to interface with but the primary reason why I was working with `AudioQueue` is that I'm working on an app that needs to continuously record, cropping audio at a certain duration (first in first out). Do you think I can do that level of customization in `AVAudioEngine`? Little hesitant to learn a new API with the deadline I have coming up. – aBikis May 26 '17 at 23:36
  • I think it could do it, but if your only problem is with `AudioFileReadPacketData`, let's just fix that. – Rhythmic Fistman May 26 '17 at 23:46
  • I've posted the new question here. Sorry it took me so long I was trying to work it out on my own. https://stackoverflow.com/questions/44316380/audiofilereadpacketdata-returns-50-when-passed-valid-file – aBikis Jun 01 '17 at 20:16