ニッチなケースだとは思うのですが、
- デバイスA(マスター)に対して、デバイスBを同期再生させたい
- ダンスパフォーマンスなので遅延はNG
- デバイスBのアプリは、デバイスAの再生の途中で起動されるかもしれない
- つまり、同期タイミングが曲の頭だけでは不十分
- ライブパフォーマンスなので一発勝負
- デバイスBは100台ぐらい
という要件の案件があり、曲中にもちょこちょこと同期タイミング(※)を用意して、デバイスBではその度に再生位置を変える(シークする)という方法を取ろうとしました。
※同期方法には Audio Watermark (音響透かし)を使用。この話はまた後日書きます。
で、やってみると、デバイスAとデバイスBの再生音がたまにずれる。常にずれているなら同期方法自体の問題と考えられますが、「たまに」ずれるというのは、AVAudioPlayer の currentTime プロパティを使用したシーク時に遅延が生じているのかなと。(もちろん prepareToPlay とかはやっています)
ググってみると、「AVAudioPlayer も昔はレイテンシがひどかったけど、今はだいぶ改善された」みたいなコメントもあり、「AVAudioPlayerは遅い」という確証はイマイチ持てず。
でもまぁ高レベルAPIなわけだし、もうちょっと低レベルなAPIを使っていて低レイテンシを売りにしているライブラリとか当たってみよう、といろいろ調べてみると、OpenALをラップしてるいくつかのライブラリは、「低レイテンシ」をうたってはいるものの、長めの楽曲については結局 AVAudioPlayer を内部で使ってたり。。(例:ObjectAL)
EZAudio
で、たどり着いたのが、EZAudio という OSS。
https://github.com/syedhali/EZAudio
良さそうと思った点は、
- 内部では Audio Unit を使用
- 最近もわりと活発にメンテされている
- オーディオ系 OSS は古いことが多いので。。
- スター数が多い
- すなわちユーザー数が多くてある程度枯れてそう(希望的観測)
- API は AVAudioPlayer 並に簡単、とまではいかないが Audio Unit を直接使うよりは簡単
- 波形描画機能もライブラリに含まれていて、OpenGLを用いた高速描画もサポート
- CoreGraphicsベースも選択可能
実装方法
EZOutputDataSource プロトコルへの準拠を宣言しておく。
再生準備
ファイル読み込み等。
@property (nonatomic, strong) EZAudioFile *audioFile;
self.audioFile = [EZAudioFile audioFileWithURL:filePathURL]; self.audioFile.audioFileDelegate = self; [[EZOutput sharedOutput] setAudioStreamBasicDescription:self.audioFile.clientFormat];
EZOutputDataSource実装
- (void) output:(EZOutput *)output shouldFillAudioBufferList:(AudioBufferList *)audioBufferList withNumberOfFrames:(UInt32)frames { if (self.audioFile) { UInt32 bufferSize; [self.audioFile readFrames:frames audioBufferList:audioBufferList bufferSize:&bufferSize eof:&_eof]; } } - (AudioStreamBasicDescription)outputHasAudioStreamBasicDescription:(EZOutput *)output { return self.audioFile.clientFormat; }
再生開始
if (![[EZOutput sharedOutput] isPlaying]) { [EZOutput sharedOutput].outputDataSource = self; [[EZOutput sharedOutput] startPlayback]; }
再生停止
if ([[EZOutput sharedOutput] isPlaying]) { [EZOutput sharedOutput].outputDataSource = nil; [[EZOutput sharedOutput] stopPlayback]; }
シーク
NSTimeIntervalでシーク位置を指定したい場合はこんな感じ。
SInt64 frame = self.audioFile.totalFrames * newTime / self.audioFile.totalDuration; [self.audioFile seekToFrame:frame];
UISlider の位置でシークさせたい場合は付属のサンプルまんまでOK。
使ってみた所感
実際これに入れ替えてみると、「同期再生のずれ」は大幅に軽減されました。ちゃんと計測してないのですが。
関連記事