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

    METRONOME STAR、bigstopwatchのアップデート

    ホームページの移行中にエントリを増やしたくなかったので、リリースされてから少し時間が経っていますが、METRONOME STARbigstopwatchをアップデートしました。
    両方ともiOS4くらいの時から放っていて、4インチのディスプレイに対応していなかったので対応しました。ついでに対応OSをiOS7以降に上げています。
    METRONOME STARは、タップテンポの機能を追加しました。エディット画面のとき(メトロノーム音が停止中のとき)にテンポの文字あたりを連続でタップするとテンポが計れます。

    WordPressへ移行しました

    いままでObjective-AudioではMovableTypeを使っていたのですが、思う所ありましてWordpressに乗り換えました。

    パーマリンクを引き継いだりリダイレクトしたりと、リンクが切れないようにがんばったつもりですが、もしリンクが切れてしまった所がありましたらすみません。個別の記事に関しては無くなっているような事はないと思いますので、改めてお探しいただければ幸いです。

    今後ともどうぞよろしくお願いします。

    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)」となるので、注意が必要です。

    Unityでストップウォッチを作る その11 データの保存

    Unityでストップウォッチを作るシリーズ半年ぶりの更新で、最後になります。今回はアプリを終了しても継続して計測できるようにデータを保存したいと思います。

    Stopwatch.csに以下のコードを追加してください。ChangeState関数に関しては、最後に1行だけの追加になります。

    //
    // Stopwatch.csの一部
    //
    void Awake() {
        Load();
    }
    const string lastStopTimeKey = "LastStopTime";
    const string startDateTimeKey = "StartDateTime";
    const string stateKey = "State";
    void Save() {
        string lastStopTimeString = lastStopTimeSpan.Ticks.ToString();
        string dateTimeString = startDateTime.Ticks.ToString();
        PlayerPrefs.SetString(lastStopTimeKey, lastStopTimeString);
        PlayerPrefs.SetString(startDateTimeKey, dateTimeString);
        PlayerPrefs.SetInt(stateKey, (int)state);
    }
    void Load() {
        if (PlayerPrefs.HasKey(lastStopTimeKey) &&
            PlayerPrefs.HasKey(startDateTimeKey) &&
            PlayerPrefs.HasKey(stateKey)) {
            string lastStopTimeString = PlayerPrefs.GetString(lastStopTimeKey);
            if (!string.IsNullOrEmpty(lastStopTimeString)) {
                lastStopTimeSpan = new TimeSpan(long.Parse(lastStopTimeString));
            }
            string dateTimeString = PlayerPrefs.GetString(startDateTimeKey);
            if (!string.IsNullOrEmpty(dateTimeString)) {
                startDateTime = new DateTime(long.Parse(dateTimeString));
            }
            state = (StopwatchState)PlayerPrefs.GetInt(stateKey);
        }
    }
    void ChangeState(ref bool circleAnim) {
        ButtonType buttonType = ButtonType.Background;
        
        // ... 中略
        
            FlashBackground(0);
        }
        
        // 次の行を追加
        Save();
    }
    

    解説

    保存したいデータは3つだけですので、簡単に保存が出来るPlayerPrefsを使っています。ただし、PlayerPrefsに保存できる型は「int」「float」「string」の3つだけです。

    Save()関数でデータの保存をしています。今回のコードで保存しているのはStopwatchクラスの変数の「state」「lastStopTime」および「startDateTime」の3つです。stateはintで大丈夫なのですが、他の2つはTicksというlong(リファレンスを見ると64bit)の値を保存したいので、intを使うのはちょっと不安です。なので、一旦文字列に変換して、stringで保存をしています。

    Load()関数では保存したデータから読み込みをしています。long.Parseで文字列から数値に変換していますが、このParse関数は文字列から数値を探せなかったときに例外を発生させます。今回のコードのように何もしていない状態で例外が飛んでくると問題が起きますので、念のためにtryで囲って例外をキャッチしたら無視するなり、エラーダイアログを出すなりした方がよいと思います。

    サンプルプロジェクト

    これで最後という事で、プロジェクトをgithubにアップしてあります。
    BigStopWatchForUnity

    前へ

    Touch the Wave for iPad v2.0.1リリース

    Touch the Wave for iPadのv2.0.1がリリースされました。

    変更点は、以下になります。

    ・GAINが効かなくなる事がある問題を修正
    ・Multi Route Audioの問題を修正(HDMI接続時)

    Multi Route Audioの問題というのは、出力を2つにわけるときにSplitterを使っていたのですけど、HDMIをつなげたときにデバイスが48kHz固定になってしまい、RemoteIOでサンプリングレートの変換が入って倍速かつ変な音になっていたものです。なので、Splitter〜RemoteIOの間はデバイスのサンプリングレートと一緒にして、変換が必要な場合はSplitterの前に入れる事で解決しました。