0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Swift5でAudio Queue Servicesを使って音声ファイルを再生する

Last updated at Posted at 2021-02-02

CoreAudiのAPIで最もハイレベル(ハードウェアから遠い)AudioQueueServicesを使っての音声ファイルの再生は、Swift5でそのまま動くサンプルや記事があまり見つけられなかったため結構苦しみながら勉強しましたが、いろいろ試行錯誤の末に理解を深めることが出来ましたので、記録のために記事を書いてみます。不足や理解違いがあればご指摘頂ければありがたいです。

参考文献

タコさんブログ iPhone Core Audioプログラミング 永野哲久著 Learning Core Audio by Chris Adamson & Kevin Avila

環境

  • Xcode 12.2
  • Swift 5.3.1

事前準備とベースとなる考え方

  • import AudioToolboxが必要です。
  • kNumberBuffer変数に3を代入する。バッファの数は3が良いようです。
  • AudioPlayerというクラスを作り、このインスタンスをAudioService内で使ったり、AudioQueueCallback関数に渡したり、必要なデータを一括でやりとりしやすいようにします。
  • AudioPlayerクラスのプロパティが何をするためのものなのかを、順を追って噛み締めて行くと、理解がしやすいように思いました。
  • AudioFileGetPropertyなどCore Audioで用意されている関数の「設定方法」に慣れていくことがキモ。
  • 「ファイルを読む」とは、どのメモリの番地からどのメモリの番地までの情報を拾うか、ということが分かると以降の話が理解しやすいと思います。
  • 音声ファイルには大きく分けてCBR(固定ビットレート)とVBR(可変ビットレート)があり、以下のように整理すると分かりやすい
    • 1秒間に何個標本化するか、がサンプル・レート(例:44100 = 1秒間に44100個のデータ(サンプル)がある)。
    • 各サンプルの数値(振れ幅)の大きさをビット深度(bit depths)という(例:16bitなど)。
    • サンプル・レートとビット深度を掛けたものをビット・レート(bit rate)という。
    • ある一点の時間(サンプル)上に存在するデータをフレーム(Frame)という。モノラルの場合は1フレームにサンプルはひとつ。ステレオの場合は右チャンネルと左チャンネルのふたつ。
    • LPCMなどはビット・レートが常に一定 == CBR (Constant Bit Rate)。
    • CBRの場合は、1パケットには必ず1フレームなので、ファイル上でパケットいくつ分進むとどのサンプルが得られるかが、とても計算しやすい。
    • mp3などは独自の計算方法によりビット・レートが変化することにより、より小さなファイルサイズを実現している == VBR (Variable Bit Rate)。1パケットのフレーム数はいろいろ。
    • そのため、VBRの場合の音声データは、AudioStreamPacketDescriptionというパケットに何フレームあるかを説明しているものがないと、ファイルを読むことが出来ない。
  • Bundleにファイルを追加する方法はこちらの記事などを参考にしてください。
AudioService.swift
import Foundation
import AudioToolbox

let kNumberBuffer = 3

class AudioPlayer {
    var mAudioFile: AudioFileID? // 1
    var mDataFormat = AudioStreamBasicDescription() // 2
    var mQueue: AudioQueueRef? // 3
    var mBuffers: Array<AudioQueueBufferRef?> = Array<AudioQueueBufferRef?>(repeating: nil, count: kNumberBuffers) // 4
    var mBufferByteSize: UInt32 = 0 // 4
    var mNumPacketsToRead: UInt32 = 0 // 4
    var mPacketDescs: UnsafeMutablePointer<AudioStreamPacketDescription>? // 5
    var mCurrentPacket: Int64 = 0 // 6
    var mIsRunning: Bool = false // 6
}
  • 以下のコードはすべてAudioServiceクラス内に記述して行きます。

1: ファイルを開いてAudioFileIDを入手する

```AudioService.swift

var audioPlayer = AudioPlayer() // AudioPlayerクラスのインスタンスを作る

let filePath = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first?.path

func openFile() {
let url = NSURL(fileURLWithPath: filePath!) // pathからNSURLを生成

AudioFileOpenURL(url,
                 AudioFilePermissions.readPermission,
                 0, //拡張子がないファイルなどの場合、ヒントを与えることも可能ここでは0とし、いろいろなファイルタイプに対応している。
                 &audioPlayer.mAudioFile) // 音声ファイルオブジェクトAudioFileIDが格納される
}

}

<h3>2: AudioStreamBasicDescriptionを入手する</h3>
<ul>
<li>AudioFileGetProperty関数を使用してaudioPlayerインスタンスにASBD情報を格納すます。これらの関数は元々C言語で書かれているAPIなので、予めプロパティのサイズを確認しないと使えません。そこに慣れるとそれほど取っ付きにくくはないのかな?と感じました。</li>
</ul>
```AudioService.swift
func getASBD() {
    var propertySize: UInt32 = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
    AudioFileGetProperty(audioPlayer.mAudioFile!, //音声ファイルオブジェクト
                         kAudioFilePropertyDataFormat, //ASBDを読みとることをここで指定
                         &propertySize, //プロパティサイズをここで指定
                         &audioPlayer.mDataFormat) //ここにASBDが格納される
    }

3: AudioQueueを作成し、コールバック関数とひもづける

```AudioService.swift func createAudioQueue() { AudioQueueNewOutput(&audioPlayer.mDataFormat, // ASBD AudioQueueCallback, // 音声データをbufferに入れ、queueに入れる(エンキューという)ためのコールバック関数の名前 &audioPlayer, // AudioPlayerインスタンスのポインタ(メモリ上のアドレス情報) CFRunLoopGetCurrent(), CFRunLoopMode.commonModes.rawValue, 0, &audioPlayer.mQueue) //ここにAudioQueueが格納される }
<h3>4: バッファのサイズとバッファの中のパケット数を計算する</h3>
<ul>
</ul>
```AudioService.swift
func setPlaybackAudioQueueSize() {
    var maxPacketSize: UInt32 = 0 // パケットの最大値を得るための変数。(ファイルのエンコード・タイプによって異なる)
    var propertySize: UInt32 = UInt32(MemoryLayout.size(ofValue: maxPacketSize)) //プロパティのサイズを確認
        
    AudioFileGetProperty(audioPlayer.mAudioFile!, //元のAudioFileID
                         kAudioFilePropertyPacketSizeUpperBound, //パケットサイズの上限を得るためのパラメータ
                         &propertySize, //プロパティサイズを指定
                         &maxPacketSize) //ここにパケットサイズの上限を格納
    // 下記に作成した関数に値を渡す
    DeriveBufferSize(ASBDesc: audioPlayer.mDataFormat,
                     maxPacketSize: maxPacketSize,
                     seconds: 0.5,
                     outBufferSize: &audioPlayer.mBufferByteSize,
                     outNumPacketsToRead: &audioPlayer.mNumPacketsToRead)
}
    
func DeriveBufferSize(ASBDesc: AudioStreamBasicDescription,
                      maxPacketSize: UInt32,
                      seconds: Float64,
                      outBufferSize: UnsafeMutablePointer<UInt32>,
                      outNumPacketsToRead: UnsafeMutablePointer<UInt32>) {
        
    let maxBufferSize: UInt32 = 320000 // バッファサイズの上限を320KBを指定して、下記計算が上手く行かない場合のセーフティ・ネットとする。
    let minBufferSize: UInt32 = 16000 // 同様に、下限を160KBと指定する
        
    if ASBDesc.mFramesPerPacket != 0 { // この値が0でなければCBR
        // 時間あたりのサンプル数、バッファの大きさを計算するのは簡単
        let numPacketForTime = ASBDesc.mSampleRate / Float64(ASBDesc.mFramesPerPacket) * seconds
        outBufferSize.pointee = UInt32(numPacketForTime) * maxPacketSize
    } else {
        // この値が0だとVBR。任意の「充分なサイズ」を決めなくてはいけない。ここでは、maxBufferサイズとmaxPacketSizeの「大きい方」を代入する。
        outBufferSize.pointee = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize
    }
    // もしも、計算された値がmaxBufferSizeよりもmaxPacketSizeよりも大きい場合は、maxBufferSizeが代入されるようにしている。同様にminBufferSizeよりも小さい場合はminBufferSizeが代入される。
    if outBufferSize.pointee > maxBufferSize && outBufferSize.pointee > maxPacketSize {
        outBufferSize.pointee = maxBufferSize
    } else {
        if outBufferSize.pointee < minBufferSize {
            outBufferSize.pointee = minBufferSize
        }
    }
    // 計算で得られたoutBufferSizeをmaxPacketSizeで割ることで、バッファの中にいくつパケットがあるかが分かる。
    outNumPacketsToRead.pointee = outBufferSize.pointee / maxPacketSize    
}

5: AudioStreamPacketDescriptionにメモリを割り当て、MagicCookieをAudioQueueにコピーする

```AudioService.swift func allocPacketDescription() { // mBytesPerPacketもしくはmFramesPerPacketの値が0であればVBRと判定 let isFormatVBR = audioPlayer.mDataFormat.mBytesPerPacket == 0 || audioPlayer.mDataFormat.mFramesPerPacket == 0 if isFormatVBR { // VBRと判定された場合はmPacketDescsにメモリを割り当てる let size = Int(audioPlayer.mNumPacketsToRead * UInt32(MemoryLayout.size)) audioPlayer.mPacketDescs = UnsafeMutablePointer.allocate(capacity: size) } else { // VBRでない場合は、値はnil audioPlayer.mPacketDescs = nil } }

func setMagicCookie() {
var cookieSize = UInt32(MemoryLayout.size)

AudioFileGetPropertyInfo(audioPlayer.mAudioFile!,
                         kAudioFilePropertyMagicCookieData,
                         &cookieSize,
                         nil)
if cookieSize > 0 { // もしcookieSizeが0でない場合
    // magicCookieにメモリを割り当て
    let magicCookie = UnsafeMutablePointer<CChar>.allocate(capacity: Int(cookieSize))
    AudioFileGetProperty(audioPlayer.mAudioFile!,
                         kAudioFilePropertyMagicCookieData,
                         &cookieSize,
                         magicCookie) // ここでmagicCookieを格納
        
    AudioQueueSetProperty(audioPlayer.mQueue!, // AudioQueueのプロパティとしてmagicCookieを設定する。
                          kAudioQueueProperty_MagicCookie,
                          magicCookie,
                          cookieSize)
    // magicCookieのメモリを開放する
    free(magicCookie)
}

}

<h3>6: バッファにメモリを割り当て、コールバックへデータを渡す</h3>
```AudioService.swift

func prepareToPlay() {
    audioPlayer.mIsRunning = true // 再生中フラグをtrueに
    audioPlayer.mCurrentPacket = 0 // 再生ポジションを0に初期化
        
    for i in 0..<kNumberBuffers { // バッファ数は3に
        AudioQueueAllocateBuffer(audioPlayer.mQueue!,
                                 audioPlayer.mBufferByteSize, //計算で得られたバッファサイズ
                                 &audioPlayer.mBuffers[i])
        // コールバック関数をコール
        AudioQueueCallback(inData: &audioPlayer,
                           inAQ: audioPlayer.mQueue!,
                           inBuffer: audioPlayer.mBuffers[i]!)
    }
}

最終: Callback関数の記述

  • AudioServiceクラスの外にAudioQueueCallback( )を記述する。
  • この関数が心臓部
```AudioService.swift func AudioQueueCallback(inData: UnsafeMutableRawPointer?, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) {
// AudioPlayer型にキャスト
let indata = inData!.assumingMemoryBound(to: AudioPlayer.self).pointee
// mIsRunningがfalseだとブレイクする
guard indata.mIsRunning else {
    return
}
// 分かりやすく、以下の変数に値を格納
var numBytesReadFromFile = indata.mBufferByteSize
var numPackets = indata.mNumPacketsToRead

AudioFileReadPacketData(indata.mAudioFile!, //元の音声ファイルのAudioFileIDオブジェクト
                        false, // キャッシュするか
                        &numBytesReadFromFile, // バッファの大きさ
                        indata.mPacketDescs, // VBRの場合、この情報を使う
                        indata.mCurrentPacket, // ファイル上の再生位置
                        &numPackets, // バッファにパケットがいくつあるか
                        inBuffer.pointee.mAudioData) // バッファに格納された音声データ

if (numPackets > 0) { // パケット数が0以上であれば
    inBuffer.pointee.mAudioDataByteSize = numBytesReadFromFile
    // AudioQueueにbufferをenqueueするユーティリティ関数
    AudioQueueEnqueueBuffer(indata.mQueue!,
                            inBuffer,
                            (indata.mPacketDescs != nil ? numPackets : 0),
                            indata.mPacketDescs)
    // 再生位置をインクリメント
    indata.mCurrentPacket += Int64(numPackets)
} else {
    // パケット数が0であればストップする
    AudioQueueStop(indata.mQueue!, false) // バッファ内のデータを全て再生してからストップするのでinImmediateをfalseに設定
    indata.mIsRunning = false
}

}

<h3>SwiftUIのButtonで再生</h3>
```AudioService.swift
init () { // インスタンス生成時にこれらの関数をコール
    openFile()
    getASBD()
    createAudioQueue()
    setPlaybackAudioQueueSize()
    allocPacketDescription()
    setMagicCookie()
    prepareToPlay()
}

func play() {
    AudioQueueStart(audioPlayer.mQueue!, nil)
}
ContentView.swift
import SwiftUI

struct ContentView: View {
    var audioService = AudioService()
    var body: some View {
        Button(action: {
            audioService.play()
        }, label: {
            Text("Play")
        })
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?