AVAudioPlayerNodeでのループ再生

卒研のプログラムを書いている途中で試行錯誤した部分だったのでメモついでに。
誰かの役に立つかは分かりません…

# 追記: 2019/11/28
なんとなくこの記事を思い出してもう一度調べてみたら、どうやら[`AVAudioPlayerNodeBufferOptions`](https://developer.apple.com/documentation/avfoundation/avaudioplayernodebufferoptions?language=objc)なるものがあったみたいです...試してはいませんが、これ使った方が楽そうですね
まあ、社会人になってより調べる力もついたということで...(ごめんなさい)
一応自分の格闘の記録として、記事は残しておきます。

# `AVAudioPlayer`でよくない?
確かに、`AVAudioPlayer`を使えば`AVAudioPlayerDelegate`の`func audioPlayerDidFinishPlaying()`を用いることでループ処理を行うことができます。
しかし、私は

* `AVAudioUnitEQ`を使いたい
* 3次元空間で音を鳴らしたい

と思っていたため、`AVAudioPlayerNode`を使わざるを得ませんでした。

#`AVAudioPlayerNode`を用いて実装する方法
「仕方ないか…まあ名前似てるし使い方も似たようなもんだろう」と思っていたのも束の間、
なんと`AVAudioPlayerNode`にはデリゲートメソッドがありません。困りました。

もう避けては通れないと思い、ちゃんとApple公式のリファレンスを読みました。
へっぽこ大学生がざっくり調べた感じだと、以下の2通りが候補として挙げられました。

* `func scheduleBuffer()`を使う
* `func scheduleFile()`を使う

見た感じ前者が楽そうだったので、最初はそっちで実装しました。

## `scheduleBuffer(_:at:options:completionHandler:)`
名前からも分かる通り、ファイルをバッファに読み込んでから使うメソッドです。
引数の`options`に`.loops`を指定すればループ実装完了です。簡単ですね。

```Swift
let player = AVAudioPlayerNode()
let url = Bundle.main.url(forResource: "Cello", withExtension: "wav") // 音源のパスを取得
let file = try! AVAudioFile(forReading: url) // ファイルを取得
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) // バッファを確保
try! file.read(into: buffer) // ファイルをバッファへ読み込む

player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
player.play()
```
「複雑なことしなくてよかった〜助かった」と思っていました。しかしそうもいきませんでした…

このメソッドを使うためには前提としてバッファを確保しておかなければなりません。つまりメモリを圧迫します。
数曲程度なら問題ありませんが、塵も積もればなんとやらです。
私の研究だと40もの楽曲を使うので40曲分のバッファを確保しようとしていました。(前提自体どうなんだというのはスルーさせてください)
めっちゃメモリ食います。するとなんということでしょう、バッファ確保の部分でアプリがクラッシュしました。(当たり前)

これはなんとかせざるを得ない…ということでバッファを捨てる決意をしました。

## `scheduleFile(_:at:completionCallbackType:completionHandler:)`
こちらはファイルをそのまま使う?メソッドです。
先ほどの`options`のように一発でループを実装できないので面倒です。コールバックとかよくわからんし。
もはや後には引けなかったので頑張って調べました。

`completionCallbackType`は`completionHandler`を呼ぶタイミングを指定する引数です。

* `.dataConsumed`
* `.dataRendered`
* `.dataPlayedBack`

の3種類があります。
一番下の`.dataPlayedBack`を指定すると、再生終了のタイミングでハンドラが呼び出されます。
ということはハンドラのとこに再生処理を書けば勝ちでは?ということで最終的に以下のようなコードになりました。

```Swift
/*
players -> [AVAudioPlayer]
files -> [AVAudioFiles]
*/

func allocateFileToPlayer(n: Int) { // ファイルの割り当て, ループ設定
    players[n].scheduleFile(files[n], at: nil, completionCallbackType: .dataPlayedBack, completionHandler: 
    { (AVAudioPlayerNodeCompletionCallbackType) -> Void in
        self.allocateFileToPlayer(n: n)
        self.players[n].play()
    })
}
    
func play() {
    // ファイルの割り当て
    for i in 0..<players.count {
        allocateFileToPlayer(n: i)
    }
    // 音を再生する
    for i in 0..<players.count {
        players[i].play()
    }
```

`func scheduleFile()`を使った場合、再生終了した後にもう一度再生させようとするともう一度プレイヤにファイルの割り当てをしなければなりません。
よってハンドラ部分にも`func scheduleFile()`を書く必要があります。
そこで、この部分を`func allocateFileToPlayer()`としてハンドラ部分で再帰的に呼び出すことで簡潔にしてみました。
クロージャとかコールバックとか色々難しかったですが、メモリも圧迫せず快適に動作してくれました。

# まとめ

* `func scheduleBuffer()`
    * メリット: 実装が簡単
    * デメリット: 曲数が増えるとメモリ圧迫しまくる
* `func scheduleFile()`
    * メリット: メモリを圧迫しづらい
    * デメリット: クロージャとかコールバックを理解する必要がある

こんな感じだと思います。ちゃんと実装できたし勉強にもなったので結果的には良かったかなと思います。
何か間違いなどあればご指導、ご鞭撻のほどよろしくお願いします。

# 参考文献
* AVAudioPlayerNode - AVFoundation, https://developer.apple.com/documentation/avfoundation/avaudioplayernode
* [Swift][AVAudioFoundation] AVAudioPlayer でオーディオデータ再生が終了したらCallbackする, https://qiita.com/programanx1/items/40a287b2d77d0bcbcd3f
* iOSでのコールバック処理の3つの書き方(Swift), http://sakebook.hatenablog.com/entry/2015/08/12/133756