タコさんブログ

プログラミングメモと小言

Swift で Audio Queue Services を使って再生する

Audio Queue Services Programming Guide - Playing Audio を参考にSwiftでAudio Queue Serviceを使用する。

環境

準備

AudioToolbox をインポートする。

import AudioToolbox

Audio Queue Service を使用する手順

  1. 状態・フォーマット・パス情報を管理する構造体を定義する
  2. 実際の再生を行うオーディオキューコールバックを作成する
  3. オーディオキューバッファのサイズを決定するコードを書く
  4. プレイバックするオーディオファイルを開き、オーディオデータのフォーマットを取得する
  5. プレイバックオーディオキューを作成し、プレイバックに設定する
  6. オーディオキューバッファを生成し、エンキューする
  7. リソースの解放

状態・フォーマット・パス情報を管理するクラス(構造体)を定義

オーディオフォーマットとオーディオキューの情報を管理するクラスを定義する。 状態の更新を行うのでクラス(参照型)で定義する。

let kNumberBuffers = 3

class AQPlayerState {
  var mDataFormat: AudioStreamBasicDescription
  var mQueue: AudioQueueRef
  var mBuffers: [AudioQueueBufferRef]
  var mAudioFile: AudioFileID
  var bufferByteSize: UInt32
  var mCurrentPacket: Int64
  var mNumPacketsToRead: UInt32
  var mPacketDescs: UnsafeMutablePointer<AudioStreamPacketDescription>
  var mIsRunning: Bool
  // 見やすさのためinit内で初期化する
  init() {
      mDataFormat = AudioStreamBasicDescription()
      mQueue = nil
      mBuffers = [AudioQueueBufferRef](count: kNumberBuffers, repeatedValue: nil)
      mAudioFile = nil
      bufferByteSize = 0
      mCurrentPacket = 0
      mNumPacketsToRead = 0
      mPacketDescs = nil
      mIsRunning = false
  }
}

オーディオキューコールバックの作成

このコールバック関数は主に以下の3つのことをする

  1. 指定された分のデータをオーディオファイルから読み込み、それをオーディオキューバッファに入れる
  2. バッファキューにオーディオバッファキューをエンキューする
  3. 読み込むデータがない場合にオーディオキューに停止するように伝える

以下がサンプルに示されている基本的なコールバック関数になる。

func HandleOutputBuffer(aqData: UnsafeMutablePointer<Void>, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) {
  // AQPlayerStateにキャスト
  let aqdata = UnsafeMutablePointer<AQPlayerState>(aqData).memory
  // 再生中でない場合は何もしない
  guard aqdata.mIsRunning else {
      return
  }
  // AudioFileReadPackets deprecated の変更により
  // numBytesReadFromFile を設定する
  var numBytesReadFromFile = aqdata.bufferByteSize
  var numPackets = aqdata.mNumPacketsToRead
  // オーディオファイルからデータを読み込む
  AudioFileReadPacketData(aqdata.mAudioFile,
                          false,
                          &numBytesReadFromFile,
                          aqdata.mPacketDescs,
                          aqdata.mCurrentPacket,
                          &numPackets,
                          inBuffer.memory.mAudioData)
  if (numPackets > 0) {
      inBuffer.memory.mAudioDataByteSize = numBytesReadFromFile
      // オーディオキューバッファをエンキューする
      AudioQueueEnqueueBuffer(aqdata.mQueue,
                              inBuffer,
                              (aqdata.mPacketDescs != nil ? numPackets : 0),
                              aqdata.mPacketDescs)
      aqdata.mCurrentPacket += Int64(numPackets)
  } else {
      // オーディオキューを停止する
      AudioQueueStop(aqdata.mQueue, false)
      aqdata.mIsRunning = false
  }
}

再生するオーディオキューバッファのサイズを決定する

func DeriveBufferSize(ASBDesc: AudioStreamBasicDescription, maxPacketSize: UInt32, seconds: Float64, outBufferSize: UnsafeMutablePointer<UInt32>, outNumPacketsToRead: UnsafeMutablePointer<UInt32>) {
  let maxBufferSize: UInt32 = 0x50000 // 320 KB
  let minBufferSize: UInt32 = 0x4000  // 16 KB

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

  if outBufferSize.memory > maxBufferSize && outBufferSize.memory > maxPacketSize {
      outBufferSize.memory = maxBufferSize
  } else {
      if outBufferSize.memory < minBufferSize {
          outBufferSize.memory = minBufferSize
      }
  }

  outNumPacketsToRead.memory = outBufferSize.memory / maxPacketSize
}

再生するオーディオファイルを開く

let filePath = "music file path"
let url: CFURL = NSURL(fileURLWithPath: filePath)
let result = AudioFileOpenURL(url, .ReadPermission, 0, &aqData.mAudioFile)
assert(result == noErr, "AudioFileOpenURL Error: \(result)")

オーディオデータのフォーマットを取得

let result = AudioFileGetProperty(aqData.mAudioFile,
                                  kAudioFilePropertyDataFormat,
                                  &dataFormatSize,
                                  &aqData.mDataFormat)
assert(result == noErr, "AudioFileGetProperty Error: \(result)")

プレイバックオーディオキューの生成

let result = AudioQueueNewOutput(&aqData.mDataFormat,
                                 HandleOutputBuffer,
                                 &aqData,
                                 CFRunLoopGetCurrent(),
                                 kCFRunLoopCommonModes,
                                 0,  // アップルに予約されている。0でなければならない
                                 &aqData.mQueue)

assert(result == noErr, "AudioQueueNewOutput Error: \(result)")

プレイバックオーディオキューのサイズを設定

var maxPacketSize: UInt32 = 0
var propertySize = UInt32(sizeof(UInt32.self))
let result = AudioFileGetProperty(aqData.mAudioFile,
                                  kAudioFilePropertyPacketSizeUpperBound,
                                  &propertySize,
                                  &maxPacketSize)

DeriveBufferSize(aqData.mDataFormat, maxPacketSize: maxPacketSize, seconds: 0.5, outBufferSize: &aqData.bufferByteSize, outNumPacketsToRead: &aqData.mNumPacketsToRead)

Packet Description配列のメモリを割りあてる

// Allocating Memory for a Packet Descriptions Array
let isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 ||
                  aqData.mDataFormat.mFramesPerPacket == 0

if isFormatVBR {
    let size = Int(aqData.mNumPacketsToRead * UInt32(sizeof(AudioStreamPacketDescription.self)))
    aqData.mPacketDescs = UnsafeMutablePointer<AudioStreamPacketDescription>.alloc(size)
} else {
    aqData.mPacketDescs = nil
}

プレイバックオーディオキューのMagic Cookieを設定

var cookieSize = UInt32(sizeof(UInt32.self))

let result = AudioFileGetPropertyInfo(aqData.mAudioFile,
                                      kAudioFilePropertyMagicCookieData,
                                      &cookieSize,
                                      nil)

if result != noErr {
    let magicCookie = UnsafeMutablePointer<CChar>.alloc(Int(cookieSize))
    AudioFileGetProperty(aqData.mAudioFile,
                         kAudioFilePropertyMagicCookieData,
                         &cookieSize,
                         magicCookie)

    AudioQueueSetProperty(aqData.mQueue,
                          kAudioQueueProperty_MagicCookie,
                          magicCookie,
                          cookieSize)
    free(magicCookie)
}

最初に再生するオーディオキューバッファを用意する

aqData.mIsRunning = true
aqData.mCurrentPacket = 0
for i in (0..<kNumberBuffers) {
    AudioQueueAllocateBuffer(aqData.mQueue,
                             aqData.bufferByteSize,
                             &aqData.mBuffers[i])

    HandleOutputBuffer(&aqData,
                       inAQ: aqData.mQueue,
                       inBuffer: aqData.mBuffers[i])
}

オーディオキューを開始する

AudioQueueStart(aqData.mQueue, nil)

Command Lineで実行する場合は以下も必要。iOSの場合は不要。

repeat {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode,
                       0.25,
                       false)
} while aqData.mIsRunning

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, false)

再生後の処理

AudioQueueDispose(aqData.mQueue, true) // Dispose audio queue
AudioFileClose(aqData.mAudioFile)      // Close audio file
free(aqData.mPacketDescs)              // Release the packet description

参考URL