[iOS 8] AVFoundationのAVAudioPlayerNodeで音楽ファイルを再生してみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回の記事では、iOS 8のオーディオ関連フレームワークの新機能について大まかに解説しました。今回は、iOS 8でAVFoundationフレームワークに新規追加されたAVAudioEngineAVAudioPlayerNodeを使用した実装を解説していきます。(AVAudioEngineなどについての説明は前回の記事を御覧ください。)

目次

今回作るアプリについて

ios-8-avaudioplayernode-00

今回は、再生ボタン、ボリューム、パン(音の定位)の3つのコントロールだけを備えた簡易的な音楽再生アプリの実装例について解説します。

ios-8-avaudioplayernode-03

今回の実装のNodeの構成はこんな感じです(Mixer NodeやOutput NodeはAVAudioEngineが内部的に持つNodeになります)

実装 (ファイルから再生する)

以下が実装例です。

「Nodeを作成して、Engineに装着し、Engineをスタートさせる」という流れになります。

@import AVFoundation;

@interface FilePlayerViewController ()

@property (nonatomic, strong) AVAudioEngine *engine;
@property (nonatomic, strong) AVAudioPlayerNode *audioPlayerNode;
@property (nonatomic, strong) AVAudioFile *audioFile;

@end

@implementation FilePlayerViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.engine = [AVAudioEngine new];
    
    // [1] AVAudioFile オブジェクトを準備する
    NSString *path = [[NSBundle mainBundle] pathForResource:@"loop.m4a" ofType:nil];
    self.audioFile = [[AVAudioFile alloc] initForReading:[NSURL fileURLWithPath:path]
                                                   error:nil];
    
    // [2] AVAudioPlayerNode オブジェクトを準備する
    self.audioPlayerNode = [AVAudioPlayerNode new];
    [self.engine attachNode:self.audioPlayerNode];
    
    // [3] Node 同士を繋ぐ
    AVAudioMixerNode *mixerNode = [self.engine mainMixerNode];
    [self.engine connect:self.audioPlayerNode
                      to:mixerNode
                  format:self.audioFile.processingFormat];
    
    // [4] Engine の処理を開始する
    NSError *error;
    [self.engine startAndReturnError:&error];
    if (error) {
        NSLog(@"error:%@", error);
    }
}

#pragma mark - private methods

- (void)play
{
    // [5] オーディオファイルの再生をスケジュールする
    [self.audioPlayerNode scheduleFile:self.audioFile
                                atTime:nil
                     completionHandler:nil];
    
    // [6] 再生する
    [self.audioPlayerNode play];
}

- (IBAction)didTapPlayButton:(id)sender
{
    if (self.audioPlayerNode.isPlaying) {
        [self.audioPlayerNode stop];
    } else {
        [self play];
    }
}

- (IBAction)didChangeVolumeSliderValue:(id)sender
{
    float value = ((UISlider *)sender).value;
    
    // [7] ボリュームを操作する
    self.audioPlayerNode.volume = value;
}

- (IBAction)didChangePanSliderValue:(id)sender
{
    float value = ((UISlider *)sender).value;
    
    // [8] パンを操作する
    self.audioPlayerNode.pan = value;
}

@end

[1] AVAudioFileオブジェクトを準備する

まずは、AVAudioFileを作成します。これはオーディオファイルを表すオブジェクトであり、ファイルのフォーマット情報などももっています。

[2] AVAudioPlayerNode オブジェクトを準備する

AVAudioPlayerNodeを作成して、AVAudioEngineに装着します。AVAudioPlayerNodeはファイルやバッファからのオーディオ再生を行うためのNodeです。

[3] Node 同士を繋ぐ

[2]で作成したAVAudioPlayerNodeとAVAudioEngineが内部に持つミキサーNodeを接続します。これでファイルを再生するためのNodeは全て接続完了になります。

[4] Engine の処理を開始する

Audio Engine の処理を開始します。Nodeの接続等に問題がなければこれで準備完了です。

[5] オーディオファイルの再生をスケジュールする

AVAudioPlayerNodeのscheduleFile:atTime:completionHandler:メソッドを使用してオーディオファイルの再生をスケジュールします。

- (void)scheduleFile:(AVAudioFile *)file
              atTime:(AVAudioTime *)when
   completionHandler:(AVAudioNodeCompletionHandler)completionHandler

今回は引数whenにnilを指定しているので(また、他に再生がスケジュールされてもいないので)、「すぐに再生する」という処理がスケジュールされます。時間を指定することで指定時間後に再生することもできます。

[6] 再生する

[5]でスケジュールされた処理が実行されます。

[7] ボリュームを操作する

AVAudioPlayerNodeのvolumeプロパティの値を変更することでボリュームを操作します。

[8] パンを操作する

AVAudioPlayerNodeのpanプロパティの値を変更することでパン(音の定位)を操作します。

実装 (バッファから再生する)

ios-8-avaudioplayernode-01

AVAudioPlayerNodeはオーディオバッファからの再生にも対応しています。そこで「実装 (ファイルから再生する)」で説明したコードを修正してオーディオバッファから再生するようにしてみましょう。また、画面にスイッチを追加し、スイッチがONになっている場合はループ再生するようにします。

変更する部分は以下の通りです。

[9] オーディオバッファを用意する

viewDidLoadメソッドの12〜15行目にコードを追加します。オーディオファイルからAVAudioPCMBufferオブジェクトを作成しています。作成したAVAudioPCMBufferオブジェクトはplayメソッドのほうで使用します。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.engine = [AVAudioEngine new];
    
    NSString *path = [[NSBundle mainBundle] pathForResource:@"loop.m4a" ofType:nil];
    self.audioFile = [[AVAudioFile alloc] initForReading:[NSURL fileURLWithPath:path]
                                                   error:nil];
    
    // [9] オーディオバッファを用意する
    AVAudioFormat *audioFormat = self.audioFile.processingFormat;
    AVAudioFrameCount length = (AVAudioFrameCount)self.audioFile.length;
    self.audioPCMBuffer = [[AVAudioPCMBuffer alloc]initWithPCMFormat:audioFormat frameCapacity:length];
    [self.audioFile readIntoBuffer:self.audioPCMBuffer error:nil];
    //
    
    self.audioPlayerNode = [AVAudioPlayerNode new];
    [self.engine attachNode:self.audioPlayerNode];
    
    AVAudioMixerNode *mixerNode = [self.engine mainMixerNode];
    [self.engine connect:self.audioPlayerNode
                      to:mixerNode
                  format:self.audioFile.processingFormat];
    
    NSError *error;
    [self.engine startAndReturnError:&error];
    if (error) {
        NSLog(@"error:%@", error);
    }
}

[10] オーディオバッファの再生をスケジュールする

オーディオバッファからの再生なので、AVAudioPlayerNodeのscheduleBuffer:atTime:options:completionHandler:メソッドを使用して再生をスケジュールします。

- (void)scheduleBuffer:(AVAudioPCMBuffer *)buffer
                atTime:(AVAudioTime *)when
               options:(AVAudioPlayerNodeBufferOptions)options
     completionHandler:(AVAudioNodeCompletionHandler)completionHandler

画面上のスイッチのON/OFFによって、AVAudioPlayerNodeBufferOptions型のoptions引数の値だけ異なるものを指定しています。

内容
AVAudioPlayerNodeBufferLoops バッファを無限にループ
AVAudioPlayerNodeBufferInterrupts 再生中のバッファを中断させる
AVAudioPlayerNodeBufferInterruptsAtLoop 再生中のバッファの1ループが終了するまで待つ
- (void)play
{
    // [10] オーディオバッファの再生をスケジュールする
    if (self.loopSwitch.isOn) {
        [self.audioPlayerNode scheduleBuffer:self.audioPCMBuffer
                                      atTime:nil
                                     options:AVAudioPlayerNodeBufferLoops
                           completionHandler:nil];
    } else {
        [self.audioPlayerNode scheduleBuffer:self.audioPCMBuffer
                                      atTime:nil
                                     options:AVAudioPlayerNodeBufferInterrupts
                           completionHandler:nil];
    }
    
    // Start playback
    [self.audioPlayerNode play];
}

まとめ

本記事では、AVFoundationのAVAudioEngineAVAudioPlayerNodeを使用して簡易的な音楽再生アプリを実装してみました。 今回作成したサンプルプロジェクトは以下のリポジトリで公開していますので動かしてみてください。

参考