RemoteIOでのオーディオ再生

iPhone用のアプリも公開できて一段落つきまして、さらにNDAも緩和されたという事で、iPhoneのオーディオプログラミングネタを書いていきたいと思います。

iPhoneで音を再生する方法はいくつかあります。短いサンプルを再生するならSystemSoundService。BGMで使うような長い曲を再生するならAudioQueueService。レイテンシーを低く再生するならAudioUnit。あと、OpenALを使うという手もあります。

「Touch the Wave」で使っているのはAudioUnitです。SystemSoundは短すぎて役に立ちませんし、AudioQueueではリアルタイムに音を変化させるような事や逆再生が出来ないですし(たぶん)、OpenALはよくわからないって感じでしたので。

何はなくともとりあえずAudioUnitを使うクラスのサンプルコードです。このクラスを生成すればAudioUnitが開始されて、解放すれば停止します。シンプルにするためにエラー処理はまったく記述していません。

※最終修正 2011/10/24

//
//  YKAudioOutput.h
//
#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>
@interface YKAudioOutput : NSObject {
AudioUnit outputUnit;
}
@end
//
//  YKAudioOutput.m
//
#import "YKAudioOutput.h"
@implementation YKAudioOutput
static OSStatus OutputCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData)
{
    OSStatus err = noErr;
    for (NSInteger i = 0; i < ioData->mNumberBuffers; i++) {
        //2009/6/28 OS3.0対応
        SInt16 *ptr = ioData->mBuffers[i].mData;
        for (NSInteger j = 0; j < inNumberFrames; j++) {
            UInt32 channels = ioData->mBuffers[i].mNumberChannels;
            for (NSInteger k = 0; k < channels; k++) {
                ptr[j * channels + k] = sin(M_PI / inNumberFrames * j * 50) * INT16_MAX;
            }
        }
    }
    return err;
}
- (void)setupOutputUnit
{
    AudioComponent component;
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    
    component = AudioComponentFindNext(NULL, &desc);
    AudioComponentInstanceNew(component, &outputUnit);
    AudioUnitInitialize(outputUnit);
    AURenderCallbackStruct callback;
    callback.inputProc = OutputCallback;
    callback.inputProcRefCon = self;
    
    AudioUnitSetProperty(outputUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Global,
                         0,
                         &callback,
                         sizeof(AURenderCallbackStruct));
    
    AudioStreamBasicDescription outputFormat;
    UInt32 size = sizeof(AudioStreamBasicDescription);
    
    outputFormat.mSampleRate = 44100;
    outputFormat.mFormatID = kAudioFormatLinearPCM;
    outputFormat.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
    outputFormat.mBitsPerChannel = 16;
    outputFormat.mChannelsPerFrame = 2;
    outputFormat.mFramesPerPacket = 1;
    outputFormat.mBytesPerFrame = outputFormat.mBitsPerChannel / 8 * outputFormat.mChannelsPerFrame;
    outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
    AudioUnitSetProperty(outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormat, size);
    AudioOutputUnitStart(outputUnit);
}
- (void)dispose
{
    AudioOutputUnitStop(outputUnit);
    AudioUnitUninitialize(outputUnit);
    AudioComponentInstanceDispose(outputUnit);
    outputUnit = NULL;
}
- (id) init
{
    self = [super init];
    if (self != nil) {
        [self setupOutputUnit];
    }
    return self;
}
- (void) dealloc
{
    [self dispose];
    [super dealloc];
}
@end

まず、AudioUnitを使うからといってAudioUnitFrameworkをインポートしてもエラーが出ます。AudioUnitだけをつかうときでもAudioUnitFrameworkはインポートせず、AudioToolbox.Frameworkだけをインポートすれば良いようです。

コードを見ていきますと、setupOutputUnitメソッドがAudioUnitのセットアップしている部分になります。順番としては、

・AudioComponentDescriptionに呼び出すAudioUnitの情報を記述する
・AudioComponentNextでAudioComponentを取得する
・AudioComponentInstanceNewでAudioUnitを取得する
・AudioUnitInitializeでAudioUnitを初期化する
・AURenderCallbackStructでコールバックの情報を記述する
・AudioUnitSetPropertyでコールバックの設定をする
・AudioUnitStartでRemoteIOをスタートする

という流れになっています。Macでのときと違って関数の名前にAudioとかついていますが、使い方は変わりません。iPhoneでのアウトプットユニットはRemoteIOと名前がついてまして、サブタイプにkAudioUnitSubType_RemoteIOを指定します。RemoteIOがスタートしたらコールバックが定期的に呼び出されますので、そこでオーディオデータを渡します。

iPhone OS 2.2.1までのRemoteIOのデフォルトのフォーマットは8.24の固定小数点でしたが、OS 3.0からは16bit整数のインターリーブドに変更されたようです。ただ、あくまでデフォルトが変更されただけですので、OSのバージョンがなんだろうが、自分でフォーマットを設定しておけば互換性は問題なく保たれます。ミキサーのユニットなんかは変わらず8.24みたいですので注意が必要です。

iOS 5ではさらにデフォルトが32ビットfloatに変更されているようです。AudioUnitSetPropertyでフォーマットを設定するように変更しました。(2011/10/24)

RemoteIOでのオーディオ再生」への16件のフィードバック

  1. akichan

    いつも大変興味深く拝見させて頂いております。
    for (NSInteger i = 0; i mNumberBuffers; i++) {
    memset(ioData->mBuffers[i].mData,
    0,
    inNumberFrames * sizeof(AudioUnitSampleType));
    }
    の部分をsin()関数を使用して正弦波が出力されるようにかえてみたのですが、どうやっても矩形波(正弦波のレベルを上げて音が割れた状態)しか出力されないのです。(設定した周波数では出力されています。)
    また、0.0001をインクリメントし1以上になったら-1にするようなノコギリ波にかえてみても定期的にインパルスが出力されるだけです。
    何か他に必要なのでしょうか?
    ioData->mBuffers[i].mDataに渡すデータとしては、32bit固定小数点、24bit固定小数点、32bit浮動小数点等試しましたが全て同じ結果でした。
    お手数かとは存じますがご回答頂ければ幸いです。

    返信
  2. akichan

    さっそくすみません。
    24bit固定小数点と書いてありましたね。失礼しました。
    また、ioData->mBuffers[i].mDataのアドレスをポインタに入れ使っていたのですが、そのポインタの型をfloat型にしていたのが原因でした。
    自己解決で申し訳ありません。今後のご活躍も期待しております。

    返信
  3. Yasoshima

    せっかくなんで、サイン波を鳴らすように変更してみました。
    最初の頃はMacの感覚でiPhoneでもfloatを使おうとしていたのですが、パフォーマンス的にも不利になるので、iPhoneは割り切って整数で通して扱った方が良さそうです。
    (あれ、Integerって固定小数点じゃなくて整数ですよね?)

    返信
  4. Yasoshima

    あ、すみません、RemoteIOは固定小数点てことでしたね。自分的にちょっと勘違いしていましたので、また書き直しました。

    返信
  5. やま

    ちょっとお尋ねしたいのですが、
    再生スピードの制御(ゆっくりとか速くとか)はできるんでしょうか?
    kAudioUnitSubType_Varispeedやらでできるのかと思ったのですが、
    うまく動いてくれませんでした。
    AudioUnitSetParameterにはパラメータでこれを指定したのですが、値としては何を渡せばよいのでしょう???

    返信
  6. Yasoshima

    コメントいただきましてありがとうございます。
    VarispeedはMacでのみでiPhoneでは使えません。
    AudioUnitの機能で再生スピードを変えるという事でしたら、AUConverterを作ってRemoteIOの前に繋ぎ、そのAUConverterのイン側とアウト側のサンプリング周波数を変えるという事になると思います。RemoteIOだけでもできるような気がしますが試してません。
    なお、kAudioUnitSubType_VarispeedというのはAudioUnitの種類になりますので、ParameterにセットするのではなくVarispeedのAudioUnitを作成するという使い方になります。
    あと、拙作のTouch the Waveのような連続的なスピード変化をさせたいという事でしたら、自分自身で実装しないと実現できないと思います。

    返信
  7. Maruyama

    はじまして、丸山と申します。
    いつも開発で、本サイトを参考にさせていただいております。
    一つ質問があるのですが、32kHz/16bitの楽曲を再生するのにAudioUnitのRemoteIOを利用しようと試みているのですが、どういうわけかASBDで設定したmSampleRateの値が44.1kHzのままで、変更できません。そのため、再生速度が速くなってしまい困っております。
    もしや、RemoteIOのサンプリングレートは44.1kHzで固定されているのでしょうか?
    本件ご存知でしたらご指南いただけると幸いです。

    返信
  8. Yasoshima

    コメントいただきまして、ありがとうございます。
    こちらでも試してみましたが、たしかにRemoteIOのサンプリング周波数を変えようとしても変わらないみたいですね。AudioSessionのkAudioSessionProperty_PreferredHardwareSampleRateで設定すれば変更できます。AudioUnitでやるなら、AUConverterかMixerをRemoteIOの前に取り付けて、そちらのインプットを変更という感じでしょうか。

    返信
  9. pton

    はじめまして。大変参考にさせていただいております。
    この記事を参考に音を出すアプリを作成しているのですが、iOS5に変更したところ音が出ないようになってしまいました。
    オーディオフォーマットが変更された(http://d.hatena.ne.jp/nokiya/20111019/1318992221)所までたどりつき、最大値とインターリーブの変更を試みましたがうまくいっておりません(下記)。
    ptr[j + k*inNumberFrames] = sin(M_PI / inNumberFrames * j * 50);
    解決法をご存知でしたらご教授願えれば幸いです。

    返信
  10. Yasoshima

    ご指摘ありがとうございます。RemoteIOのフォーマットを変更するように、コードを修正しました。

    返信
  11. pton

    こんなに早くご回答いただけるとは思っておりませんでした。大変参考になります。設定の変更も試していたのですが、AudioUnitSetPropertyの使い方がを勘違いしていたようです。重ね重ね、ありがとうございました。

    返信
  12. fuu

    初めまして。参考にさせていただいております。
    知っていたら、教えていただきたいのですが
    iPhone 4Sで44.1kHzサンプリングで再生したときに
    22.5kHzを再生しようと思うと、再生されないのです。
    ほかにiPod touch 第4世代なども試しましたが
    iPhone 4Sのみが22.5kHzで再生できなくなってしまいます。
    対処法をご存知でしたら、ご教授お願いいたします。

    返信
  13. Yasoshima

    fuu様、コメントいただきありがとうございます。ただ、頂いている情報が少なく、お答えできかねる状態です。
    特に、「44.1kHzサンプリングで再生したときに 22.5kHzを再生」という部分で、実際にどのような方法で再生しようとしているかが分かれば検証できると思いますので、具体的に情報を頂けますでしょうか?
    また、OSのバージョンがiPhone 4Sだけ違っているという事はありませんでしょうか?今一度、ご確認ください。

    返信
  14. fuu

    Yasoshima様、ご返答ありがとうございます。
    OSは、両方とも最新のiOS5.01となります。
    以下は、検証用にYasoshima様のコードから作っています。
    OutputCallback内で以下のようにして信号を生成しております。
    for (NSInteger i = 0; i mNumberBuffers; i++)
    {
    SInt16 *ptr = ioData->mBuffers[i].mData;
    for (NSInteger j = 0; j mBuffers[i].mNumberChannels;
    for (NSInteger k = 0; k

    返信
  15. Yasoshima

    fuu様。こちらで検証した印象としましては、デバイスごとのDAの性能の差ではないでしょうか。
    確認したところ、音楽専用のデバイスではないiPhone4Sで20kHzくらいまでは出ていますし、十分な性能ではないかと思います。
    あと、AudioUnitのサンプリング周波数を88.2kに設定しても、変換されて結局44.1kで再生されてしまうはずですので、この場合の検証方法としては意味が無いと思います。

    返信

コメントを残す

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