プログラミング」カテゴリーアーカイブ

iOSDC JAPAN 2017

iOSDC JAPAN 2017の前夜祭で「Objective-C++を使ってMRCで快適に開発する」という発表をしてきました。

Objective-Cに関するCfpは少ないだろうから選ばれる可能性が高そうだったのと、マニアックな話題にすると前夜祭に選ばれて発表が終わった後にスッキリとした気持ちで本編を見ることができるだろうという見込みがピッタリはまって、前夜祭で発表することとなりました。

「すごく参考になりました!早速使ってみます!」みたいなコメントは僕の知る限りひとつもいただけなかったのですが(それは当然)、すでにObjective-C++を使っている方が少なからずいらっしゃることもわかりましたし、MRCで書くかどうかはさておいてObjective-C++はパフォーマンスを追求するような分野では細々と使われていくのではないかなという印象です。

発表の内容に関して興味のある方は、以下のスライドやサンプルコードをご参照ください。

スライド


https://speakerdeck.com/objectiveaudio/objective-c-plus-plus-woshi-tutemrcdekuai-shi-nikai-fa-suru

サンプルコード

https://github.com/objective-audio/objc_ptr_sample

来年も開催されるようでしたら、またマニアックなネタで前夜祭を狙っていきたいと思います。

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に乗り換えられると思うので、今後に期待したいですね。

    mediaServiceWereResetNotificationが呼ばれるタイミング

    AVAudioSessionのmediaServiceWereResetNotificationですが、たまたま確実に呼ばれるタイミングを見つけられたのでメモしておきます。

    1、AudioSessionのCategoryをAVAudioSessionCategoryMultiRouteにする。
    2、iPad本体にヘッドホンを接続し、Lightning端子に外部出力を接続する。
    3、ヘッドホンとLightningケーブルを同時に素早く抜き取る。

    LightningやiPadと書きましたが、たぶんDockケーブルやiPhoneでもMultiRouteな状態からケーブルを一気に引き抜けば同じく呼ばれると思います。呼ばれたときにはAudioSessionがリセットされていたりしますので、オーディオ関連のオブジェクトの再生成とAudioSessionの再セットアップを行いましょう。

    参考:Technical Q&A QA1749

    なかなか普通はMultiRouteを使ったりはしないと思いますのでこのパターンで遭遇する事は無いと思います。もし、他に呼ばれるタイミングをご存知な方がいらっしゃいましたら教えていただけるとうれしいです。

    Multi Route Audio

    iOS6からの機能なので今さら感がありますが、拙作のTouch the Waveでも導入したMulti Route Audioの使い方をまとめておこうと思います。

    Multi Route Audioとは

    USBオーディオインターフェースやHDMIなどの外部出力を接続したときに、ヘッドホンへ別系統の音を出力できる機能です。外部出力2chとヘッドホン2chの計4ch出力する事が出来ます。残念ながらマルチイン・アウトが搭載されているオーディオインターフェースをマルチで使えるという機能ではなく、USBオーディオインターフェースはイン・アウト2chのみで、それとは別にヘッドホンにも別の音をだせるというだけの機能です。

    ちなみにUSBオーディオインターフェースの繋げ方を知らない方に説明しておきますと、カメラコネクションキットまたはUSBカメラアダプタを使ってUSBオーディオインターフェースを接続すると、内蔵スピーカーではなくオーディオインターフェースから音を出す事が出来ます。市販されているたいていのUSBオーディオインターフェースは対応しているはずです。ただし、USB接続に対応しているのはiPadのみで、HDMI接続ならiPhone・iPad両方大丈夫です。また、バスパワーで動作するオーディオインターフェースはほぼ電源供給が足りないので、電源付きのUSBハブを介して接続する必要があります。

    入力が2系統受けられる機能に関してはiOS5から搭載されているようですが、出力に関してはiOS6からの新機能です。今回のエントリでは出力のみ扱います。なお、WWDC2012のSession 505でも解説されていますので、英語に抵抗無い方はApple純正の解説をご覧いただくのも良いと思います。

    音を出す前の準備として大まかに以下のような作業が必要になります。

    ① AudioSessionのCategoryをMultiRouteにする
    ② RouteChangeの通知を受け取る
    ③ 出力ポートの情報を取得する
    ④ 再生する音の出力チャンネルを設定する

    AudioSessionのCategoryをMultiRouteにして、外部オーディオデバイスとヘッドホンを両方接続した状態で初めてMulti Route Audioが使えるようになります。

    実際に音を出すのには、RemoteIOを使う方法と、AVAudioPlayerを使う方法の2つがあります。基本的にRemoteIOの方法を解説します。AVAudioPlayerは個人的に使わないので簡単に触れる程度にします。

    AudioSessionのCategoryをMultiRouteにする

    AVAudioSessionのCategoryにAVAudioSessionCategoryMultiRouteをセットし、通常オーディオを鳴らす前の準備と同じくAudioSessionをアクティブにしてください。

    NSError *error = nil;
       
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryMultiRoute error:&error];
    [[AVAudioSession sharedInstance] setActive:YES error:&error];
    

    RouteChangeの通知を受け取る

    通知センターからAVAudioSessionRouteChangeNotificationの通知を受け取るように登録します。これで、オーディオデバイスやヘッドホンを抜き差ししたときに通知が飛んできます。

    // 通知を登録する
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(routeChangeNotification:)
    name:AVAudioSessionRouteChangeNotification object:nil];
    
    // 通知を受け取る
    - (void)routeChangeNotification:(NSNotification *)note
    {
        NSDictionary *dict = note.userInfo;
        AVAudioSessionRouteChangeReason reason =
            [[dict objectForKey:AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
        
        // ... 通知を受け取った後の処理
    }
    

    出力ポートの情報を取得する

    通知を受け取った後や、一番最初にセットアップをするときに出力ポートの情報を取得します。

    // 現在接続されているオーディオルートの情報を取得する
    AVAudioSessionRouteDescription *routeDesc =
        [[AVAudioSession sharedInstance] currentRoute];
    // 出力の情報を取得する
    NSArray *outputs = routeDesc.outputs;
        
    for (AVAudioSessionPortDescription *portDesc in outputs) {
       NSString *portType = portDesc.portType; // AVAudioSessionPortHeadphonesなど
       NSString *portName = portDesc.portName;
       NSArray *channels = portDesc.channels; // AVAudioSessionChannelDescription
    }
    

    AudioSessionのcurrentRouteから現在のオーディオ接続状態をAVAudioSessionRouteDescriptionで取得できます。さらにそのAVAudioSessionRouteDescriptionのoutputsから現在の出力ポートの情報がAVAudioSessionPortDescriptionのNSArrayで取得できます。

    ポートというのは基本的に接続されている機器です。USB Audioであったり、ヘッドホンであったり内蔵スピーカーの事です。出力であればだいたい2chずつチャンネルを持っています。

    outputsが2つあればマルチで出力できるという事ですし、1つであれば1系統しか出力が無いという事です。AudioSessionでMultiRouteにCategoryがセットされていなければ、常に1つだけになります。CategoryがMultiRouteでかつマルチで出力できる接続があるときにoutputsが2つになります。

    AVAudioPlayerで出力を選択するには、AVAudioSessionPortDescriptionのchannelsで取得できるAVAudioSessionChannelDescriptionをchannelAssignmentsにセットすれば良いようです。

    RemoteIOのChannelMapを設定する

    出力ポートの情報を取得してMulti Routeの状態であることが確認できたら、RemoteIOからどの出力へアサインするかをChannelMapで設定します。

    UInt32 map[4] = {0, 1, -1, -1};//ひとつめのデバイスに出力
    // UInt32 map[4] = {-1, -1, 0, 1};//ふたつめのデバイスに出力
    // UInt32 map[4] = {0, 1, 0, 1};// 両方のデバイスに同じ音を出力
    UInt32 size = 4 * sizeof(UInt32);
        
    AudioUnitSetProperty(_audioUnit, kAudioOutputUnitProperty_ChannelMap, kAudioUnitScope_Output, 0, map, size);
    

    重要な注意点として、RemoteIOはひとつにつき2chしか出力する事が出来ません。マルチ出力でUSB Audioとヘッドホンの合計4chへバラバラの音を出力したい場合は、RemoteIOを2つ作成する必要があります。ただ、同じ音を複数の出力へ割り当てる事は出来ますので、外部出力とヘッドホン出力で左右を入れ替えるというだけだったり(そんなことはしないと思いますが…)、モノラルを2系統出力するという用途であればRemoteIOひとつでも大丈夫です。

    ChannelMapはUInt32型の配列で設定します。4chの出力がある状態ならば、要素数が4つの配列にRemoteIOのアウトのどのチャンネルを割り当てるのかを記述します。0と1にすればRemoteIOのそれぞれのチャンネルの音が出力されますが、-1にすれば出力されません。(符号無しの型なのに-1を記述するのはちょっと気持ち悪いですが、-1で設定するように指定されています)。なお、MultiRouteの状態でなければChannelMapの設定は無視されるようです。

    ChannelMap配列の出力の順番は、AudioSessionのcurrentRouteのoutputsの順番と一致しています。ただ、接続するオーディオデバイスによって順番が変わったりしますので、ちゃんとポートの中身を判断して設定したり、ユーザーに設定をしてもらう必要があります。USBでしか試していないと「本線(USB)・ヘッドホン」の順番で固定だと思ってしまいますが、HDMIだと「ヘッドホン・本線(HDMI)」となるので、注意が必要です。

    AudioQueueProcessingTap

    iOS6からAudioQueueServiceにProcessingTapという機能が追加されました。

    これを利用すると、Queueにバッファされた後のデータにエフェクトなど処理を挟み込む事ができるようになります。AudioQueueのピッチ処理はiOSでは機能していないと思いますが、AudioUnitを挟み込む事もできるので、VarispeedとかNewTimePitchとか使えば実現する事ができます。

    AudioQueueProcessingTapNew

    ProcessingTapをAudioQueueで使えるようにするのが以下の関数です。

    extern OSStatus
    AudioQueueProcessingTapNew(
            AudioQueueRef inAQ,
            AudioQueueProcessingTapCallback inCallback,
            void *inClientData,
            UInt32 inFlags,
            UInt32 *outMaxFrames,
            AudioStreamBasicDescription *   outProcessingFormat,
            AudioQueueProcessingTapRef *    outAQTap)
    

    AudioQueueProcessingTapNewでAudioQueueProcessingTapを作って、既に作ってあるAudioQueueに追加します。なお、ひとつのAudioQueueにつき、ひとつのProcessingTapしか割り当てられないようです。

    inAQにはProcessingTapを入れたいAudioQueueを指定します。

    inCallbackには処理をするコールバックの関数を指定します。引数の構成はAudioUnitのRenderCallbackと似たような感じです。

    inFlagsではCallbackの動作の仕方を指定する事ができます。使えるのは以下の3つです。

    kAudioQueueProcessingTap_PreEffects
    kAudioQueueProcessingTap_PostEffects
    kAudioQueueProcessingTap_Siphon
    

    PreEffectsとPostEffectsは必ずどちらかを指定しないといけません。ここでのEffectというのはMacでのみ使えるAudioQueueのピッチ処理の事のようで、その前後のどちらかを選択できるようです。iOSだと、今の所どちらを選択しても動作的にはおそらく変わりません。

    Siphonをフラグに足しておくと、CallbackのioDataにバッファからのデータが入った状態で来ます。フラグを入れなければ、AudioQueueProcessingTapGetSourceAudioという関数を使ってバッファからデータを読み込まなければいけません。ProcessingTapのコールバックの中だけでゴリゴリ自分で処理をするならSiphoneを入れる、AudioUnitを使って入力側のコールバックにデータが欲しい場合やスピードを変更したい場合はSiphonを入れないでAudioQueueProcessingTapGetSourceAudioを呼ぶ、という事になると思います。

    outMaxFramesやoutProcessingFormatには、ProcessingTapコールバックでの最大のフレーム数や、フォーマットが返ってきます。たぶん自分でフォーマットを指定したりとかはできません。オーディオファイルをリニアPCMに解凍した状態のフォーマットが返ってくると思います。AudioUnitのエフェクトを使いたい場合は、AUConverterなどを使ってoutProcessingFormatに変換したり、outMaxFramesのフレーム数をmaximumPerFramesに設定する必要があると思います。

    AudioQueueにProcessingTapを追加する簡単なコードは以下のような感じです。

    //
    // インスタンス変数とか(AudioQueueは既にセットアップ済みを想定)
    // AudioQueueRef _queue;
    // AudioQueueProcessingTapRef _processingTap;
    //
    static void Callback(
            void *inClientData,
            AudioQueueProcessingTapRef inAQTap,
            UInt32 inNumberFrames,
            AudioTimeStamp *ioTimeStamp,
            UInt32 *ioFlags,
            UInt32 *outNumberFrames,
            AudioBufferList *ioData)
    {
        AudioQueueProcessingTapGetSourceAudio(
                inAQTap,
                inNumberFrames,
                ioTimeStamp,
                ioFlags,
                outNumberFrames,
                ioData);
        // ここで何か処理をする
    }
    - (void)setupAudioQueueProcessingTap
    {
        UInt32 processingMaxFrames;
        AudioStreamBasicDescription processingFormat;
        BOOL isPost = YES;
        BOOL isSiphon = NO;
        UInt32 flags = isPost ? kAudioQueueProcessingTap_PostEffects :
                           kAudioQueueProcessingTap_PreEffects;
        if (isSiphon) flags |= kAudioQueueProcessingTap_Siphon;
        AudioQueueProcessingTapNew(
                _queue,
                Callback,
                NULL,
                flags,
                &processingMaxFrames,
                &processingFormat,
                &_processingTap);
    }
    

    このコードではエフェクトが何もかからないスルー状態です。Siphonをフラグに入れていないのでAudioQueueProcessingTapGetSourceAudioを呼んでいます。何か処理をしたい場合は、GetSourceAudioの後にioDataの中身をいじってください。もしSiphonをいれたらAudioQueueProcessingTapGetSourceAudioの行は全くなしでも音が鳴ります。

    AudioUnitのエフェクトを使う場合には、AudioUnitの入力側のコールバックでGetSourceAudioを呼ぶ事になります。

    あと、スピードを変更する場合、AudioUnitのVariSpeedを使うなら気にしなくてもいいのですが、自分でやる場合にはGetSourceAudioに渡すTimeStampのsampleTimeをちゃんと使ったフレーム分だけ毎コールバック進めないといけません。ProcessingTapに来ているTimeStampそのまま渡すと、ノーマルスピードで進んでいると判断してデータを取ってきてしまいます。

    iOS 5のオーディオ新機能

    世の中iOS 5といったらiCloudだSiriだと騒いでいますが、オーディオの新機能も実は結構あります。とりあえずどんなものがあるかだけ書いておきます。

    AudioUnitプラグイン

    AudioUnitのプラグインがかなり追加されてます。Macでは以前からあるもののサブセット的な感じです。

    【Generator】
    ・ScheduledSoundPlayer
    ・AudioFilePlayer

    【MusicDevice】
    ・Sampler

    音を再生するものはGeneratorとMusicDeviceですね。特にSamplerはSoundFont&MIDI対応のプレイバックシンセなので、SoundFontを作ってしまえばMIDI対応も簡単にできてしまいます。「なんとかピアノ」とか「シンギングなんとか」とか、AVAudioPlayerで無理矢理つくるんじゃなくて、これ使った方が良いです。AudioFilePlayerは、これはこれでいいんですが、使い方が独特ってのもありますし、個人的にはAudioQueueのAudioUnitプラグイン版を作ってほしいなぁと思います。

    【FormatConverter】
    ・VariSpeed
    ・iPodTimeOther

    僕は使ってませんが、VariSpeedつかえば再生スピードの変更も簡単です。AUConverterでも良いんじゃないの?と思うかもしれませんが、VariSpeedならスピードを変えたときにプツっとならずにスムーズに変わります。ただ、すごく速くとか、すごく遅くとか出来ませんので、「Touch the Wave」でやってるようなスクラッチを実現するのは難しいです。

    iPodTimeOtherというのは「Touch the Wave for iPad」でも早速使っています。iPodTimeよりもクオリティの上がったタイムストレッチです。

    【Effect】
    ・LowPassFilter
    ・HiPassFilter
    ・BandPassFilter
    ・HighShelfFilter
    ・LowShelfFilter
    ・NBandEQ
    ・ParametricEQ
    ・PeakLimiter
    ・DynamicProcessor
    ・Reverb2
    ・Distortion

    Effectは、半分くらいEQですが、結構増えました。リバーブの追加はうれしいです。

    プラグインがこれだけ用意されていると、ようやくAUGraphの便利さが活用できそうです。はっきりいって今までは、RemoteIOだけ使って全部自分で作んなくちゃいけませんでしたから。

    MusicPlayer

    MIDIシーケンサーです。SMFが読み込めます。中にSamplerを含んだAUGraphを持っているので、入れ替えてあげれば自分で用意した音も鳴らせます。もちろんMIDIメッセージだけ再生させて利用する事も出来ます。

    MIDINetworkSession

    MIDINetworkSessionがシミュレータに対応してます。実機につながなくてもMacだけでMIDIの送受信ができるので、デバッグが楽になるんじゃないでしょうか。あとiOS4だとMIDINetworkSessionってMIDI使い始めたら勝手にオンになっていたと思うのですが、iOS5だとちゃんとenabledをYESにしたりとかしておかないと使えないみたいです。

    Float32対応 (2011/10/23追加)

    iOS 5からAudioConverterがFloat32対応になっています。上記でも紹介したAudioUnitプラグインなどは、floatにしか対応していないようです。AUGraphなんかでつなぐ場合はあまり意識しなくても良いのですが、整数で用意したデータを渡すときにはAUConverterを前に挟み込む必要がありそうです。

    iOSのCore MIDI

    iOSの勉強会用にCoreMIDIを調べてみたりしたのはいいものの、まだ発表する機会がなさそうなのと、気がつけば3ヶ月もブログを更新していなかったので、ちょっと書いておこうかなと思います。とりあえずこのエントリは、CoreMIDI対応アプリを開発する前の準備の話です。(※2011/7/26に内容を変更しています)

    CoreMIDIはiPadだけ?

    iOS 4.3.3より前のOSだと、外部とMIDIデータの送受信をできるのはiPadだけだったのですが、iOS 4.3.3以降だとiPhoneでもWi-fi経由でiPadと同じく使えるようになったようです。また、iPadではMIDIインターフェースをCamera Connection Kitにつなげれば使えたのですが、最近YAMAHAなどからドックに直接つなげるタイプのMIDIインターフェースが発売されましたので、iPhoneでもそれらをつかえばMIDIの送受信を実機だけで出来るようになりました。

    デバッグはWi-Fiで!

    MIDIインターフェースをドックに付けた状態だと、Macとつなげる事が出来ないので非常にデバッグがめんどくさいです。なので開発中はドックにつながず、iPhone/iPad(実機)とMacをWi-Fiで接続してWi-Fi経由でMIDIを送受信するようにした方が良いと思います。MacにMIDIインターフェースをつなげるのも良いですが、何かMIDIを送受信出来るアプリケーションがあれば別にMIDIインターフェースがなくても問題ありません。むしろ、いろいろなMIDIメッセージを送信出来るMacアプリケーションを作ってデバッグに活用する方が良いんじゃないでしょうか。

    以下は、Macと実機をWi-Fi経由でMIDIの送受信をする接続方法を解説します。これはアプリの開発者に関わらず、MacのシーケンスソフトなどからiOSアプリの音をならしたい、でもケーブルでごちゃごちゃしたくないというユーザーの方にも参考になると思います。ですがWi-Fi経由だと、直接MIDIインターフェースをドックにつなげた場合と違って、それなりに発音の遅れや揺れがあると思いますのでご注意ください。

    Wi-Fi経由のCore MIDIの接続方法

    まず、実機になにかCoreMIDI対応アプリを入れておきましょう。何も持っていなければ無料の「Midi Monitor」あたりをダウンロードしておけば良いと思います(僕の環境ではしょっちゅう落ちるアプリですが…)。もちろん、自分のアプリを開発するときにはそれを立ち上げればいいので、それで。

    Macと実機を同じWi-Fiネットワークに接続してください。Wi-Fiアクセスポイントがない場所でも、MacでAd-Hocネットワークを作成して接続する方法もありますので、わからない方は適当にぐぐってください。

    Macの「Audio MIDI 設定」アプリケーションを起動して、「MIDI スタジオ」というウィンドウが開いていなければ、メニューのウィンドウの「MIDI ウィンドウを開く」を選択して開いてください。

    MIDIスタジオのなかにネットワークというアイコンがありますのでダブルクリックして「MIDIネットワーク設定」というウィンドウを開きます。

    CoreMIDI01.jpg

    ウィンドウ左上の「自分のセッション」の「+」をクリックして、ネットワークセッションを作ります。

    CoreMIDI03.jpg

    おそらく「セッション1」という名前のセッションが出来ると思いますので、そこの左のボタンにチェックを入れます。

    CoreMIDI04.jpg

    実機側で、CoreMIDI対応のアプリを起動してください。実機では、アプリでMIDIClientを作成する事で自動的にネットワークセッションが作成されます。ちゃんと実機側のネットワークセッションがMacで認識されていれば、ウィンドウ左真ん中の「ディレクトリ」に実機の名前が表示されています。もし、CoreMIDI対応アプリを起動しても名前が現れない場合は、一度実機を再起動してみてからアプリを起動しなおすと良いかもしれません。

    CoreMIDI05.jpg

    実機の名前が現れたら選択して、下の「接続」ボタンをクリックしてください。接続に成功すれば、右側の「構成」のところに実機の名前が表示されているはずです。

    CoreMIDI06.jpg

    MacのMIDIアプリケーションからMIDIを送受信する場合は、この時点でMacアプリケーションのMIDIインやアウトでネットワークセッションを選択出来るようになっていると思いますので、Audio MIDI 設定での設定は以上です。

    アプリケーションを使わずMIDIインターフェースと実機をつなげたい場合は、ウィンドウ右下のライブルーティングを使います。実機へMIDIメッセージを送りたいMIDIインターフェース(MIDI IN)を上のポップアップボタンで選択してください。逆に実機から送りたいMIDIインターフェース(MIDI OUT)は下のポップアップボタンで選択してください。(手元にUSB接続のMIDIキーボードしかなかったので、下のスクリーンショットではIN側だけ選択した状態です。)

    CoreMIDI07.jpg

    あとは実機アプリ側の設定ですが、MIDI INやOUTが選択出来るアプリであれば、「Network Session 1」というのがあるはずですので選択してください。そのような設定がないものはつながるものは全部つなげてしまう仕様になっていると思いますので、特に設定する必要はないと思います。

    と、今回はこの辺で。

    CMSampleBufferからオーディオデータを取り出す

    前回の続きというほどのものでもない内容になってしまいますが、CMSampleBufferからオーディオのデータを取り出す方法です。CMSampleBufferGetAudioBufferListWithRetainedBlockBufferという関数を使います。前回の内容も含めてDeveloper Forumsに書いてあったコードを参考に調べていたのですが、削るに削ってこれだけになりました。

    CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
    CMBlockBufferRef blockBuffer;
    AudioBufferList audioBufferList;
            
    CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
                                                            NULL,
                                                            &audioBufferList,
                                                            sizeof(audioBufferList),
                                                            NULL,
                                                            NULL,
                                                            0,
                                                            &blockBuffer);
    //
    //    ここでAudioBufferListから読み出す処理など
    //
    CFRelease(sampleBuffer);
    CFRelease(blockBuffer);
    

    とりあえずInterleavedなオーディオデータが読み出せればよいのであれば、上記のコードのようにAudioBufferListを作って、その中のデータを保持するCMBlockBufferをこの関数で取得するというだけで良いようです。引数がたくさんあって他にもいろいろ取得できるようですが、結構みんなNULLであっさり通ったので、あえてそれらを使って何かするのでなければいらないんじゃないでしょうか。

    AVAssetReaderとAVAssetWriter

    iOS4.0のAVAssetExportSessionでiPod Libraryからファイルの書き出しができるようになったことは以前のエントリーに書きましたが、さらにiOS4.1からは、AVAssetReaderによって事前にコピーすること無く、直接iPodLibraryのオーディオファイルのデータを生のデータで読み込むことが出来るようになっています。また、AVAssetWriterという、オーディオファイルを作ることの出来るクラスも追加されています。

    AVAssetExportSessionがMP3などを読み込むのにかなり使い勝手の悪いものだったのですが、AVAssetReaderはExtAudioFile的に簡単に好きなフォーマットに変換していろんなファイルが読み込めるので、かなり良さげな感じです。

    ちなみに、ここではオーディオファイルだけを扱いますが、ビデオの読み込みや書き出しも出来るはずですので、興味のある方はいろいろ調べてみることをお勧めします。

    AVAssetReaderはiPod Libraryから直接リアルタイム再生できたりするのが一番の利点だと思うのですが、ここではあえてAVAssetReaderとAVAssetWriterを使って、ExportSession的にファイルの書き出しをやってみたいと思います。自分も情報の少ない中しらべて成功したという感じでもありますので、おかしなところがありましたらご指摘いただけると助かります。

    以下が、サンプルソースです。MPMediaPickerControllerなどでMPMediaItem取得してこのメソッドに渡すとアプリのドキュメントフォルダに書き出されます。セットアップしている途中で引っかかりそうなところはエラー処理的にreturn NOで終わるようにしてあります。

    必要なフレームワークは、AVFoundation.FrameworkとCoreMedia.Framework、あとMPMediaItemの取得でMediaPlayer.Frameworkといったところです。

    - (BOOL)exportItem:(MPMediaItem *)item
    {
        NSError *error = nil;
        
        NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
                                      [NSNumber numberWithFloat:44100.0],AVSampleRateKey,
                                      [NSNumber numberWithInt:2],AVNumberOfChannelsKey,
                                      [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
                                      [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
                                      [NSNumber numberWithBool:0], AVLinearPCMIsBigEndianKey,
                                      [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
                                      [NSData data], AVChannelLayoutKey, nil];
        
        //読み込み側のセットアップ
        
        NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
        AVURLAsset *URLAsset = [AVURLAsset URLAssetWithURL:url options:nil];
        if (!URLAsset) return NO;
        
        AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:URLAsset error:&error];
        if (error) return NO;
        
        NSArray *tracks = [URLAsset tracksWithMediaType:AVMediaTypeAudio];
        if (![tracks count]) return NO;
        
        AVAssetReaderAudioMixOutput *audioMixOutput = [AVAssetReaderAudioMixOutput
                                                       assetReaderAudioMixOutputWithAudioTracks:tracks
                                                       audioSettings:audioSetting];
        
        if (![assetReader canAddOutput:audioMixOutput]) return NO;
        
        [assetReader addOutput:audioMixOutput];
        
        if (![assetReader startReading]) return NO;
        
        
        //書き込み側のセットアップ
        
        NSString *title = [item valueForProperty:MPMediaItemPropertyTitle];
        NSArray *docDirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *docDir = [docDirs objectAtIndex:0];
        NSString *outPath = [[docDir stringByAppendingPathComponent:title]
                             stringByAppendingPathExtension:@"wav"];
        
        NSURL *outURL = [NSURL fileURLWithPath:outPath];
        AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outURL
                                                              fileType:AVFileTypeWAVE
                                                                 error:&error];
        if (error) return NO;
        
        AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
                                                                                  outputSettings:audioSetting];
        assetWriterInput.expectsMediaDataInRealTime = NO;
        
        if (![assetWriter canAddInput:assetWriterInput]) return NO;
        
        [assetWriter addInput:assetWriterInput];
        
        if (![assetWriter startWriting]) return NO;
        
        
        
        //コピー処理
        
        [assetReader retain];
        [assetWriter retain];
        
        [assetWriter startSessionAtSourceTime:kCMTimeZero];
        
        dispatch_queue_t queue = dispatch_queue_create("assetWriterQueue", NULL);
        
        [assetWriterInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{
            
            NSLog(@"start");
            
            while (1)
            {
                if ([assetWriterInput isReadyForMoreMediaData]) {
                    
                    CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
                    
                    if (sampleBuffer) {
                        [assetWriterInput appendSampleBuffer:sampleBuffer];
                        CFRelease(sampleBuffer);
                    } else {
                        [assetWriterInput markAsFinished];
                        break;
                    }
                }
            }
            
            [assetWriter finishWriting];
            [assetReader release];
            [assetWriter release];
            
            NSLog(@"finish");
        }];
        
        dispatch_release(queue);
        
        return YES;
    }
    

    全体の手順としては

    ① MPMediaItemから読み込みたい音楽のAVURLAssetを取得
    ② 読み込む時のフォーマットをNSDictionaryで作成
    ③ AVAssetReaderを作成
    ④ AVAssetReaderAudioMixOutputを作成してAVAssetReaderに取り付け
    ⑤ AVAssetReaderのstartReadingで読み出しの準備

    ⑥ 書き出し先のNSURLを作成
    ⑦ 書き出すファイルのフォーマットをNSDictionaryで作成(今回は読み込み側と共通)
    ⑧ AVAssetWriterを作成
    ⑨ AVAssetWriterInputを作成してAVAssetWriterに取り付け
    ⑩ AVAssetWriterのstartWritingで書き込みの準備
     
    ⑪ AVAssetWriterのstartSessionAtSourceTimeで書き出しセッションの開始
    ⑫ AVAssetWriteInputのrequestMediaDataWhenReadyOnQueue:usingBlock:で書き出し開始

    ⑬ AVAssetReaderAudioMixOutputのcopySampleBufferでCMSampleBufferを取得
    ⑭ AVAssetWriterInputのappendSampleBuffer:で書き込み
    ⑮ 終わるまで⑬と⑭を繰り返し
    ⑯ 終わったらAVAssetWriterInputのmarkAsFinishedとAVAssetWriteのfinishWritingでファイルを閉じる

    となっています。

    ちょっと解説をしていきますと、最初にNSDictionaryで作っているのが書き出したいWAVEファイルのフォーマットです。AVAssetReaderで読み込むときのフォーマットであればもうちょっと無くても大丈夫なパラメータもあるのですが、AVAssetWriterで書き出すときは結構細かく全部指定しないと怒られます。AVChannelLayoutKeyなんかは指定したくなくても空のNSDataを渡してあげないといけません。今回は読み込みも書き出しも同じフォーマットを使っています。

    AVAssetReaderやAVAssetWriterは、直接読み込んだり書き出したりという構造にはなっていないらしく、AVAssetReaderにはAVAssetReaderAudioMixOutputを、AVAssetWriterにはAVAssetWriteInputをそれぞれ取り付けて、それ経由で読み込みや書き出しを行わなければいけないようです。AVPlayerの様にお手軽ではないのですが、CMSampleBufferという生のオーディオデータを保持しているものが使えるので、そこからAudioBufferListを取得してあれこれ自由にデータを扱うことが出来ます。

    AVAssetWriterのデータ書き込みは今回で一番調べるのに苦労したところで、最後のコピー処理のところでrequestMediaDataWhenReadyOnQueue:usingBlock:というメソッドを使っています。マイクからの入力をちょっとずつ書き込むなんて言う場合には直接appendSampleBufferで書き込んじゃっても大丈夫かもしれませんが、今回のようにオフラインで1曲分ガンガン連続で書き込む場合には、試すと頭の数秒程度しか書き込まれていない状態で終わってしまいます。AVAssetWriterInputに渡したデータがファイルに書き出しされないうちに、AVAssetWriterInputの持ってるバッファを超えてデータを渡そうとしても、isReadyForMoreMediaDataでNOが返ってきて書き込ませてくれません。なので、isReadyForMoreMediaDataがYESになるまで待ってから次のデータを書き込むということをdispatchを使ってバックグラウンドでやっているという感じです。

    単純なファイルコピーに関しては以上ですが、リアルタイム再生用などでCMSampleBufferから生のデータを取得する方法は、また次のエントリに書いてみようと思います。

    iPodライブラリからのファイル書き出し その2

    ※この記事の内容は、iOS4.2以降では正常に動作しない可能性があります。

    iPodライブラリからの書き出し、第二回です。

    前回、MP3からAACへの書き出しができないと書きましたが、おそらくAVAssetExportSessionがサポートしているファイルタイプにMP3がないからと思われます。supportedFileTypesメソッドで取得できるAVAssetExportSessionがサポートしているとおぼしきファイルタイプは以下のようなものです。

    "com.apple.quicktime-movie",
    "com.apple.m4a-audio",
    "public.mpeg-4",
    "com.apple.m4v-video",
    "public.3gpp",
    "org.3gpp.adaptive-multi-rate-audio",
    "com.microsoft.waveform-audio",
    "public.aiff-audio",
    "public.aifc-audio"
    

    オーディオで対応しているのは、「m4a」「3gpp」「wave」「aiff」「aifc」といったところのようです。

    MP3の書き出しの他にも、WAVやAIFFの非圧縮ファイルをそのまま使いたい場合にも、ちょっとめんどくさい方法を使わないといけないようです。僕があれこれ試して成功した方法をとりあえず記録しておきます。もし他にスマートな方法がありましたらご指摘いただけるとうれしいです。

    とりあえずコードです。前回のmediaPicker:didPickMediaItems:メソッドを以下のようにマルッと差し替えてください。あと、CoreMedia.Frameworkもインポートしてください。

    - (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
    {
        MPMediaItem *item = [mediaItemCollection.items lastObject];
        NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
        AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:nil];
        
        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]
                                               initWithAsset:urlAsset
                                               presetName:AVAssetExportPresetPassthrough];
        
        
        NSArray *tracks = [urlAsset tracksWithMediaType:AVMediaTypeAudio];
        AVAssetTrack *track = [tracks objectAtIndex:0];
        id desc = [track.formatDescriptions objectAtIndex:0];
        const AudioStreamBasicDescription *audioDesc = CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)desc);
        FourCharCode formatID = audioDesc->mFormatID;
        
        NSString *fileType = nil;
        NSString *ex = nil;
        
        switch (formatID) {
                
            case kAudioFormatLinearPCM:
            {
                UInt32 flags = audioDesc->mFormatFlags;
                if (flags & kAudioFormatFlagIsBigEndian) {
                    fileType = @"public.aiff-audio";
                    ex = @"aif";
                } else {
                    fileType = @"com.microsoft.waveform-audio";
                    ex = @"wav";
                }
            }
                break;
                
            case kAudioFormatMPEGLayer3:
                fileType = @"com.apple.quicktime-movie";
                ex = @"mp3";
                break;
                
            case kAudioFormatMPEG4AAC:
                fileType = @"com.apple.m4a-audio";
                ex = @"m4a";
                break;
                
            case kAudioFormatAppleLossless:
                fileType = @"com.apple.m4a-audio";
                ex = @"m4a";
                break;
                
            default:
                break;
        }
        
        exportSession.outputFileType = fileType;
        
        NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        NSString *filePath = [[docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]] stringByAppendingPathExtension:ex];
        exportSession.outputURL = [NSURL fileURLWithPath:filePath];
        
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            
            if (exportSession.status == AVAssetExportSessionStatusCompleted) {
                NSLog(@"export session completed");
            } else {
                NSLog(@"export session error");
            }
            
            [exportSession release];
        }];
    }
    

    では、ちょっと解説していきます。

    AVAssetExportSessionのpresetNameは前回のM4AではなくAVAssetExportPresetPassthroughにしています。これはiPodライブラリから変換をせずにコピーをするpresetのようです。ただし、この場合、outputFileTypeに指定すべきファイルタイプをM4Aの時のように絞ってくれませんので、自分で調べる必要があります。

    まず、AVURLAssetが持っているオーディオのAVAssetTrackを取得。さらにAVAssetTrackがもっているformatDescriptionsを取得。formatDescriptionsはNSArrayなのですが、その中に入ってるのはObjective-Cのオブジェクトではなくて、CMAudioFormatDescriptionRefというCarbon?なオブジェクトが入っています。このCMAudioFormatDescriptionRefからCMAudioFormatDescriptionGetStreamBasicDescription()という関数を使って、みなさんおなじみのAudioStreamBasicDescriptionが取得できますので、これを見て判断しています。

    wavやaiffのLinearPCMなファイルはどちらもFileIDがkAudioFormatLinearPCMですので、エンディアンで違いを判別しています。

    MP3はあれこれ試した結果、FileTypeを@”com.apple.quicktime-movie”にして、ファイル名に拡張子「.mp3」をつけておくと、ExtAudioFileで開くことができました。この挙動は試してみたらできちゃった的な感じなので、なにか正当な方法があるといいなと思っているのですが…。ちなみに、もしかしたらMP3以外もQuickTimeで共通でいけるんじゃないかと思ったのですが、MP3以外でもデータはコピーされるものの、ExtAudioFileでそのままOpenというわけにはいかないようです。

    あと、何かが取得できなかった時とかのエラー処理とかはいっさいやってませんのであしからずご了承ください。