AVAudioEngine

iOS8からAVAudioEngineという新たなオーディオの機能がAVFoundationに追加されました。AVAudioEngineというのはAUGraphのObjective-C版のような感じのものです。オーディオのプレイヤーやエフェクトやミキサーなどがNodeと呼ばれる部品で用意されていて、それらを繋いで音を鳴らす事が出来ます。

AUGraphよりもかなり簡単に使えるようになっているのですが、その反面、AUGraphを使うときのように細かい設定が出来るわけではなかったり、AUGraphで出来た事が出来なかったりする部分もあるので、いままでAUGraphを使っていた人にとっては物足りないかもしれません。でも、そんなに複雑な事をやらないのであれば、簡単にエフェクトをかけられたりするので便利だと思います。

AVAudioEngineに関してはWWDC2014でも解説されていますので、ちゃんとしたApple公式の解説を聞きたい・読みたいという方はそちらをご覧ください。

#501 – What’s New in Core Audio
#502 – AVAudioEngine in Practice

他だと、@shu223さんのiOS8-SamplerというgitリポジトリにもAVAudioEngineのサンプルコードが入っているようですので、参考にされると良いと思います。
https://github.com/shu223/iOS8-Sampler

サンプルコード

では簡単に、AVAudioEngineでオーディオファイルを鳴らして見たいと思います。Xcode 6でSingle View Applicationなどで適当なiOSのプロジェクトを作ったら、ViewController.mを以下のコードに差し替えてください。

//
//  ViewController.m
//
#import "ViewController.h"
#import <avfoundation/AVFoundation.h>
@interface ViewController ()
@property (nonatomic) AVAudioEngine *audioEngine;
@property (nonatomic) AVAudioUnitReverb *reverbNode;
@property (nonatomic) AVAudioPlayerNode *playerNode;
@property (nonatomic) AVAudioFile *audioFile;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    if (![self setup]) {
        NSLog(@"AudioEngine Setup Failed.");
    }
    [self play:nil];
}
- (BOOL)setup
{
    NSError *error = nil;
    // AudioEngineの生成
    self.audioEngine = [[AVAudioEngine alloc] init];
    // オーディオファイルの生成
    NSURL *fileURL = [[NSBundle mainBundle] URLForAuxiliaryExecutable:@"Rhythm.wav"];
    self.audioFile = [[AVAudioFile alloc] initForReading:fileURL error:&error];
    if (error) {
        return NO;
    }
    // オーディオプレイヤーノードの生成
    self.playerNode = [[AVAudioPlayerNode alloc] init];
    // リバーブエフェクトノードの生成
    self.reverbNode = [[AVAudioUnitReverb alloc] init];
    [self.reverbNode loadFactoryPreset:AVAudioUnitReverbPresetMediumHall];
    self.reverbNode.wetDryMix = 50.0f;
    // ノードをAudioEngineへ追加
    [self.audioEngine attachNode:self.playerNode];
    [self.audioEngine attachNode:self.reverbNode];
    // ノードを接続
    [self.audioEngine connect:self.playerNode
                           to:self.reverbNode
                       format:self.audioFile.processingFormat];
    [self.audioEngine connect:self.reverbNode
                           to:self.audioEngine.mainMixerNode
                       format:self.audioFile.processingFormat];
    // AudioEngineの開始
    if (![self.audioEngine startAndReturnError:&error]) {
        return NO;
    }
    return YES;
}
- (IBAction)play:(id)sender
{
    // オーディオプレイヤーノードの開始
    [self.playerNode play];
    // オーディオファイルを再生
    [self.playerNode scheduleFile:self.audioFile
                           atTime:nil
                completionHandler:nil];
}
@end

また、何かオーディオファイルをXcodeのプロジェクトに追加して”Rhythm.wav”と名前を付けてください。逆に、コード上の”Rhythm.wav”の部分を、用意したオーディオのファイルの名前に書き換えてもかまいません。拡張子はwavにしていますが、mp3でも何でも、iOSで普通にならせるオーディオのフォーマットであれば何でも構いません。

オーディオファイルを追加したら、ビルドして実行すると1回オーディオファイルが再生されると思います。UIButtonとかをplay:メソッドに接続すればタップして何度も鳴らす事が出来ます。

解説

上記のコードでは以下の図のような構成でAudioEngineをセットアップしています。ただ音を鳴らすだけではAVAudioEngineを使う意味が無いので、オーディオファイルの音にリバーブをかけて再生しています。

20140614AVAudioEngine.001

セットアップの全体の流れは、「AudioEngineを生成」→「Nodeを生成」→「必要があればNodeのプロパティなどを設定(だいたい後でも可)」→「Node同士を接続」→「AudioEngineをスタート」という感じです。Nodeの接続がうまくいってない場合はAudioEngineをスタートする(startAndReturnError:メソッドを呼ぶ)時点でエラーが返ってきます。

クラス

AVFoundationにAVAudioEngine関連で追加されたクラスには以下のものがあります。

  • AVAudioEngine(オーディオグラフ全体の管理)
  • AVAudioNodeとそのサブクラス(グラフ内のオーディオの部品)
  • AVAudioBuffer・AVAudioPCMBuffer(オーディオのバッファ)
  • AVAudioFile(オーディオファイル)
  • AVAudioFormat(オーディオのフォーマット。AudioStreamBasicDescription+αの情報)
  • AVAudioTime(オーディオ時間。再生するタイミングなど)
  • AVAudioChannelLayout、他
  • 以前からAVFoundationにあったクラスと結構名前が似通ったものがあるので注意してください(AVAudioPlayerとAVAudioPlayerNodeや、AVAudioMixとAVAudioMixingなど)。あとAudioUnitEffectとそのサブクラスはAVAudioNodeのサブクラスではあるのですが、クラス名にNodeと付いていないのでちょっと分かりにくいかもしれません。

    ノード同士の接続

    Node同士を接続するにはconnect:to:format:メソッドを使います。「接続元のNode」と「接続先のNode」と「接続間のフォーマット」を引数に渡します。フォーマットに関しては、今回みたいにオーディオファイルを鳴らすならAVAudioFileのprocessingFormatにいい感じのフォーマットが入っているのでそれを渡せばOKです。そんなのが無いという場合はAVAudioFormatのinitStandardFormat〜という初期化メソッドがあるのでそれで生成すれば、いい感じのやつを作ってくれます。AVAudioEngineではAudioStreamBasicDescriptionの全要素を自分で埋める場面というのはほとんど無いと思います。

    なお、セットアップ図にある「AVAudioMixerNode」と「AVAudioOutputNode」というのは最初から「mainMixerNode」と「outputNode」というプロパティでAVAudioEngineに用意されていて、なおかつmainMixerNodeとoutputNodeは最初から接続もされています。最初からAVAudioEngineに用意されているものはほかにも、inputNodeやmusicSequenceなどがあります。

    また、サンプルでは使っていませんがNodeのBusを指定して接続するメソッド(connect:to:fromBus:toBus:format:)もあって、ミキサーに複数接続する場合は別々のBusを指定して接続すれば同時に複数のNodeを繋げて鳴らす事が出来ます。

    オーディオプレイヤー

    オーディオファイルの再生には「AVAudioPlayerNode」というNodeを使っています。これはAVFoundationのAVAudioPlayerとは使い勝手が違って、AudioUnitのAudioFilePlayerがベースになっています。AVAudioPlayerのようにオーディオファイルのオブジェクトがあってそれを再生するというのではなく、プレイヤーがあって、そこにいろいろなオーディオファイルの鳴らしたいタイミングを登録していくという感じです。サンプルではatTimeのところの引数がnilなのですぐに鳴りますが、ここにAVAudioTimeで時間を指定すれば何秒後に鳴らすといったことが可能です。また、複数登録しておく事も出来ますが、ひとつのプレイヤーからは同時にひとつの音源しか鳴らせないので、複数のオーディオファイルを同時に鳴らしたいという場合はプレイヤーを複数作る必要があります。

    AVAudioPlayerNodeではオーディオファイルを直接ならす以外にも、AVAudioPCMBufferにオーディオデータを書き込んでおいて、そこから再生する事も出来ます。その場合にはループ再生をするオプションも指定できます。

    ボリューム

    AVAudioEngineの特徴であるのですが、ミキサーのインプット側のボリュームの設定はミキサーには無く、接続元の音源であるNode側にあります。ミキサーでボリューム調整ができるNodeはAVAudioMixingというプロトコルに準拠していてvolumeプロパティを持っています。ミキサーがインプットに接続されているNodeを辿っていって、AVAudioMixingに準拠しているNodeがあったらそのvolumeを見てレベル調整をしているようです。

    まとめ

    AVAudioEngineの利点は、サンプルのコードを見てもらうとわかるとおり、ちょっとエフェクトをかけたいというときにはすごく少ないコード量で実現できるというところです。Swiftからも特に制限無くAVAudioEngineの機能にアクセスできるので、Swift推進派な人には良いかもしれません。

    逆に欠点としては、ProcessingTap的なものが無くて独自の処理をオーディオグラフの途中に挟み込めないということと、SplitterやConverterのNodeがなくて接続の自由度が低いということです。この辺りの機能が追加されるなら、完全にAUGraphからAVAudioEngineに乗り換えられると思うので、今後に期待したいですね。

    AVAudioEngine」への9件のフィードバック

    1. びっく

      初めまして、びっくと申します。Swiftでプログラミングの勉強を開始し、はじめての耳コピアプリを作ろうとしており、貴ブログを参考にさせていただいております。
      突然のコメントで恐れ入りますが、AVAudioEngineでのオーディオ再生で詰まっているところがあり、ご質問をしたく投稿いたしました。AVAudioEngineをつかって曲を再生する際に、「現在再生時間の取得・表示」と、「任意の時間から再生を開始する」にはどうすれば良いか、なにかヒントか解説をいただけませんでしょうか?
      現在、AVAudioPlayerNodeを使って、オーディオファイルを直接ならす(AVAudioFile)、また、バッファからならす(AVAudioPCMBuffer)ことができるようになりました。また、ピッチの変更、再生速度の変更、ディレイやディストーションをかけることもできるようになりました。さらに、曲の総時間は、総フレーム数(.length)を、サンプルレート(44100)で割って、秒で算出できるようになりました。その後、いろいろと調べているのですが、質問させていただいた内容が実装できません。
      突然の勝手なお願いで恐れ入ります。ご対応いただけるととても嬉しいです。
      不適切であれば、お手数ではございますが、コメントの削除をおねがいいたします。

      返信
      1. yaso_san 投稿作成者

        「任意の時間から再生を開始する」に関してですが、AVAudioPlayerNodeに「scheduleSegment:startingFrame:frameCount:atTime:completionHandler:」というメソッドがありますのでそちらを使えば可能です。
        「現在再生時間の取得・表示」は、AVAudioPlayerNodeに「playerTimeForNodeTime:」というメソッドがありますので、引数にnodeのlastRenderTimeを渡せば、ほぼ再生時間が取れると思います。ただ、これはあくまでPlayerNodeで再生を開始してからの時間ですので、オーディオファイルを途中から再生したときに元のオーディオファイル内の時間を知りたい場合には、その差分は自分で計算する必要があります。

        返信
        1. びっく

          ご返信ありがとうございます。初心者だというのは言い訳にはなりませんが、どうしてよいかわからず困っておりました。いただいたアドバイスをもとに、試行錯誤しトライしてみます。本当にありがとうございます!

          返信
    2. U

      はじめまして、Uと申します。
      為になる記事をありがとうございます。
      アプリ制作初心者として、本当にいつも助かっています…。
      失礼とは思うのですが、一点、質問させて下さい。
      node接続後、pleyerNode playにて再生することができますが、
      その再生される音声(リバーブ等がかかった音声)を、
      ファイルに出力する場合にはどのようにすれば良いのでしょうか?
      avAudioFileの initForWriting にてパスとsettingsを指定しておき、、それからどうすれば??
      と困っております。
      初歩的な内容で申し訳ありません。
      不適切な内容でしたら、お手数ですが、削除のほどよろしくお願い致します。

      返信
      1. yaso_san 投稿作成者

        AVAudioNodeのinstallTapOnBus:〜を使うとNodeの出力をBlockで取得できるようになりますので、その中でAudioFileに書き込めば良いと思います。

        返信
    3. U

      ご返信、ありがとうございます!
      いろいろと格闘しましたが、機能させることができました。
      ありがとうございました!

      返信
      1. Carl

        iPhone XS Maxで試しているのですが、出力がモノラルで片方(カメラの近くの方)しか鳴りません。以前にAudioUnitを直接設定していた時は左右から出ていました。ステレオにするにはどうしたらいいのでしょうか?

        返信
        1. yuki 投稿作成者

          AudioUnitからAVAudioEngineに変えたからといってステレオで鳴らないということないと思います。問題の起きる最小限のコードをGitHubか何かで見れるようにしていただければ、何かお答えできるかもしれません。

          返信
    4. ピンバック: [はてブ] AVAudioEngine | Objective-Audio | WEBで何かつくるよ

    コメントを残す

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です