卒研のプログラムを書いている途中で試行錯誤した部分だったのでメモついでに。
誰かの役に立つかは分かりません…
追記: 2019/11/28
なんとなくこの記事を思い出してもう一度調べてみたら、どうやらAVAudioPlayerNodeBufferOptions
なるものがあったみたいです...試してはいませんが、これ使った方が楽そうですね
まあ、社会人になってより調べる力もついたということで...(ごめんなさい)
一応自分の格闘の記録として、記事は残しておきます。
AVAudioPlayer
でよくない?
確かに、AVAudioPlayer
を使えばAVAudioPlayerDelegate
のfunc audioPlayerDidFinishPlaying()
を用いることでループ処理を行うことができます。
しかし、私は
-
AVAudioUnitEQ
を使いたい - 3次元空間で音を鳴らしたい
と思っていたため、AVAudioPlayerNode
を使わざるを得ませんでした。
AVAudioPlayerNode
を用いて実装する方法
「仕方ないか…まあ名前似てるし使い方も似たようなもんだろう」と思っていたのも束の間、
なんとAVAudioPlayerNode
にはデリゲートメソッドがありません。困りました。
もう避けては通れないと思い、ちゃんとApple公式のリファレンスを読みました。
へっぽこ大学生がざっくり調べた感じだと、以下の2通りが候補として挙げられました。
-
func scheduleBuffer()
を使う -
func scheduleFile()
を使う
見た感じ前者が楽そうだったので、最初はそっちで実装しました。
scheduleBuffer(_:at:options:completionHandler:)
名前からも分かる通り、ファイルをバッファに読み込んでから使うメソッドです。
引数のoptions
に.loops
を指定すればループ実装完了です。簡単ですね。
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
を指定すると、再生終了のタイミングでハンドラが呼び出されます。
ということはハンドラのとこに再生処理を書けば勝ちでは?ということで最終的に以下のようなコードになりました。
/*
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