LoginSignup
27
31

More than 3 years have passed since last update.

AUGraphを用いたリアルタイム・オーディオ処理

Last updated at Posted at 2016-02-26

初めての投稿になります。
普段は自身のブログでオーディオ関連の内容をたまに紹介したりしていましたが、今後はQiitaでも書いていこうと思っています。

まずはiOSに対していろいろ書いていきます。その中で、AUGraphを用いたリアルタイム・オーディオ処理についてはよく出てくることになりそうですので、最初にまとめておこうと思います。

[追記 2019/1/7]
AUGraphはiOS 12まででDeprecatedとなりました。本記事はそれをふまえてご参照ください。

ちなみに、今回の内容は、以下のブログにて以前書いた内容を整理したものになります。
http://www.loopsessions.com/blog/?p=17

アプリ内にリソースとしてオーディオファイルが含まれていることを前提とします。

最初に、オーディオ関連のクラスを定義します。

@interface SimpleAudioIO ()
{
    AudioStreamBasicDescription _outputFormat;

    AUGraph _graph;
    AudioUnit _remoteIOUnit;
    AudioUnit _converterUnit;
}

@property (readonly) ExtAudioFileRef extAudioFile;
@property (nonatomic, assign) UInt32 numberOfChannels;  // チャンネル数
@property (nonatomic, assign) SInt64 totalFrames;   // トータルフレーム数
@property (nonatomic, assign) SInt64 currentFrame;  // 現在のフレーム位置

@end

オーディオファイルの準備

オーディオファイルを読み込みはExtAudioFileを用います。Wavだけでなく、MP3といったリニアPCM以外のフォーマットも読み込むことができる便利なサービスです。

- (SInt64)initAudioFile:(NSURL *)fileURL
{
    OSStatus ret = noErr;

    // ExAudioFileの作成
    ret = ExtAudioFileOpenURL((CFURLRef)fileURL, &_extAudioFile);

    // ファイルフォーマットを取得
    AudioStreamBasicDescription inputFormat;
    UInt32 size = sizeof(AudioStreamBasicDescription);
    ret = ExtAudioFileGetProperty(_extAudioFile,
                                  kExtAudioFileProperty_FileDataFormat,
                                  &size,
                                  &inputFormat);

    // Audio Unit正準形のASBDにサンプリングレート、チャンネル数を設定
    _numberOfChannels = inputFormat.mChannelsPerFrame;
    _outputFormat = AUCanonicalASBD(inputFormat.mSampleRate, inputFormat.mChannelsPerFrame);

    // 読み込むフォーマットをAudio Unit正準形に設定
    ret = ExtAudioFileSetProperty(_extAudioFile,
                                  kExtAudioFileProperty_ClientDataFormat,
                                  sizeof(AudioStreamBasicDescription),
                                  &_outputFormat);

    // トータルフレーム数を取得しておく
    SInt64 fileLengthFrames = 0;
    size = sizeof(SInt64);
    ret = ExtAudioFileGetProperty(_extAudioFile,
                                  kExtAudioFileProperty_FileLengthFrames,
                                  &size,
                                  &fileLengthFrames);
    _totalFrames = fileLengthFrames;

    // 位置を0に移動
    ExtAudioFileSeek(_extAudioFile, 0);
    _currentFrame = 0;

    return fileLengthFrames;
}

ASBD(Audio Stream Basic Description)はCore Audioで用いるオーディオフォーマットの情報をまとめた構造体です。
iOS5以降からAudioUnitはFloat32型で処理することを推奨しているようで、それに合わせて設定した例が以下になります。

static AudioStreamBasicDescription AUCanonicalASBD(Float64 sampleRate, UInt32 channel)
{
    AudioStreamBasicDescription audioFormat;
    audioFormat.mSampleRate = sampleRate;
    audioFormat.mFormatID = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
    audioFormat.mChannelsPerFrame = channel;
    audioFormat.mBytesPerPacket = sizeof(Float32);
    audioFormat.mBytesPerFrame = sizeof(Float32);
    audioFormat.mFramesPerPacket = 1;
    audioFormat.mBitsPerChannel = 8 * sizeof(Float32);
    audioFormat.mReserved = 0;
    return audioFormat;
}

AUGraphの準備

Audio Unit Graphを定義して、Nodeの接続を行います。ここでは以下のように接続しています。

Callback -> AUConverter -> Remote IO

最初にコールバック関数が呼び出され、再生するためのオーディオバッファを都度取得することができます。

- (OSStatus)initAUGraph
{
    OSStatus ret = noErr;

    // AUGraphの準備
    NewAUGraph(&_graph);
    AUGraphOpen(_graph);

    // AUNodeの作成
    AudioComponentDescription cd;

    cd.componentType = kAudioUnitType_FormatConverter;
    cd.componentSubType = kAudioUnitSubType_AUConverter;
    cd.componentManufacturer = kAudioUnitManufacturer_Apple;
    cd.componentFlags = 0;
    cd.componentFlagsMask = 0;
    AUNode converterNode;
    AUGraphAddNode(_graph, &cd, &converterNode);
    AUGraphNodeInfo(_graph, converterNode, NULL, &_converterUnit);

    cd.componentType = kAudioUnitType_Output;
    cd.componentSubType = kAudioUnitSubType_RemoteIO;
    cd.componentManufacturer = kAudioUnitManufacturer_Apple;
    cd.componentFlags = 0;
    cd.componentFlagsMask = 0;
    AUNode remoteIONode;
    AUGraphAddNode(_graph, &cd, &remoteIONode);
    AUGraphNodeInfo(_graph, remoteIONode, NULL, &_remoteIOUnit);

    // Callbackの作成
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = renderCallback;
    callbackStruct.inputProcRefCon = self;
    AUGraphSetNodeInputCallback(_graph,
                                converterNode,
                                0,  // bus number
                                &callbackStruct);

    // 各NodeをつなぐためのASBDの設定
    UInt32 size = sizeof(AudioStreamBasicDescription);
    // converter IO
    ret = AudioUnitSetProperty(_converterUnit,
                               kAudioUnitProperty_StreamFormat,
                               kAudioUnitScope_Input, 0,
                               &_outputFormat, size);
    ret = AudioUnitSetProperty(_converterUnit,
                               kAudioUnitProperty_StreamFormat,
                               kAudioUnitScope_Output, 0,
                               &_outputFormat, size);
    // remoteIO I
    ret = AudioUnitSetProperty(_remoteIOUnit,
                               kAudioUnitProperty_StreamFormat,
                               kAudioUnitScope_Input, 0,
                               &_outputFormat, size);

    // Nodeの接続
    // AUConverter -> Remote IO
    ret = AUGraphConnectNodeInput(_graph, converterNode, 0, remoteIONode, 0);

    // コンソールに現在のAUGraph内の状況を出力(デバッグ)
    CAShow(_graph);

    // AUGraphを初期化
    ret = AUGraphInitialize(_graph);

    return ret;
}

ちなみに、上記サンプルでは簡略化のためエラー処理を省略していますが、実際の実装ではエラー処理を入れておくことをお勧めします。(GitHubのサンプルにはエラー処理は入れてあります)
この部分は正しく定義していないと(ひっそりと)関数の実行時にエラーを返していて、アプリが落ちることはないものの音が出ない、といった症状によくなりがちですので、注意が必要です。

コールバック処理

再生中に以下のコールバック関数が都度呼び出されます。
以下では、オーディオファイルのデータを読み込み、バッファにコピーしています。ここでようやくタイトルの「リアルタイム・オーディオ処理」ですが、以下の例では、取得した実データに対して1.0を掛けているだけですので何の変化も起きません。この部分は自由に変更などして試していただけたらと思います。

OSStatus renderCallback(void *inRefCon,
                        AudioUnitRenderActionFlags *ioActionFlags,
                        const AudioTimeStamp *inTimeStamp,
                        UInt32 inBusNumber,
                        UInt32 inNumberFrames,
                        AudioBufferList *ioData)
{
    OSStatus ret = noErr;
    SimpleAudioIO *def = (SimpleAudioIO *)inRefCon;

    UInt32 ioNumberFrames = inNumberFrames;
    // オーディオファイルのデータを読み込み、バッファ(ioData)にコピー
    ret = ExtAudioFileRead(def.extAudioFile, &ioNumberFrames, ioData);
    if (ret) {
        return ret;
    }

    // 実データにアクセス
    Float32 *outL = (Float32 *)ioData->mBuffers[0].mData;
    for (int i = 0; i < ioNumberFrames; i++) {
        outL[i] *= 1.0;
    }
    if (def.numberOfChannels > 1) {
        Float32 *outR = (Float32 *)ioData->mBuffers[1].mData;
        for (int i = 0; i < ioNumberFrames; i++) {
            outR[i] *= 1.0;
        }
    }

    return ret;
}

掛ける値をUI側で調整できるようにすることで音量調整の機能になるかと思います。ただ、音量を調整する場合はkAudioUnitSubType_MultiChannelMixerを使用する方が良いと思います。(ここでは省略しました)

再生開始/停止

これでようやく再生する準備が整いました。

- (void)start
{
    if (_graph) {
        Boolean isRunning = false;
        OSStatus ret = AUGraphIsRunning(_graph, &isRunning);
        if (ret == noErr && !isRunning) {
            ret = AUGraphStart(_graph);
        }
    }
}

- (void)stop
{
    if (_graph) {
        Boolean isRunning = false;
        OSStatus ret = AUGraphIsRunning(_graph, &isRunning);
        if (ret == noErr && isRunning) {
            ret = AUGraphStop(_graph);
        }
    }
}

初回から長くなってしまいましたが、これらをベースとして、AUGraphを用いたいろいろな処理について説明していければと思っています。

今回のサンプルプログラムは以下にあります。(SimpleAudioIO)
https://github.com/JunichiMinamino/AudioObjCSample

27
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
31