Swift で Audio Queue Services を使って再生する
Audio Queue Services Programming Guide - Playing Audio を参考にSwiftでAudio Queue Serviceを使用する。
環境
準備
AudioToolbox をインポートする。
import AudioToolbox
Audio Queue Service を使用する手順
- 状態・フォーマット・パス情報を管理する構造体を定義する
- 実際の再生を行うオーディオキューコールバックを作成する
- オーディオキューバッファのサイズを決定するコードを書く
- プレイバックするオーディオファイルを開き、オーディオデータのフォーマットを取得する
- プレイバックオーディオキューを作成し、プレイバックに設定する
- オーディオキューバッファを生成し、エンキューする
- リソースの解放
状態・フォーマット・パス情報を管理するクラス(構造体)を定義
オーディオフォーマットとオーディオキューの情報を管理するクラスを定義する。 状態の更新を行うのでクラス(参照型)で定義する。
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つのことをする
- 指定された分のデータをオーディオファイルから読み込み、それをオーディオキューバッファに入れる
- バッファキューにオーディオバッファキューをエンキューする
- 読み込むデータがない場合にオーディオキューに停止するように伝える
以下がサンプルに示されている基本的なコールバック関数になる。
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