nana開発者ブログ

音楽SNSアプリ"nana"開発チームのブログです。

MTAudioProcessingTapを使って、AVPlayerで再生中のオーディオデータを取得する

こんにちは!音声信号処理エンジニアの kaede-san です。

私事ですが、先日、新型コロナウイルスのワクチンを打ちまして、ここ数日副反応に見舞われており、更新が遅くなってしまいました🙇‍♀️

今回は、MTAudioProcessingTapを使って、AVPlayerでオーディオデータを取得する方法を紹介します!

MTAudioProcessingTapとは

SwiftではMTAudioProcessingTap、Objective-C(厳密にはC言語)ではMTAudioProcessingTapRefとして定義されています。

公式ドキュメントでは以下のように説明が書かれています。

You can use the audio tap to access the track’s audio data before it is played, read, or exported.

簡単に訳すと「オーディオデータを再生・読み取り・エクスポート前に読み取ることができるよ!」とのことです。

AVAudioEngineを使ったことある方はご存知かと思いますが、AVAudioEngineにはinstallTapという機能がありますね!その機能と考え方はほぼ同じです。

また、内部的にAudioUnitを繋いで、再生される音源にリバーブやイコライザーなどのエフェクトをかけて加工することもできます。

なぜこの技術を採用したか

目的は、「nanaの再生画面でビジュアライザーを表示すること」です。

ビジュアライザーとは、音楽に合わせてうねうねうようよ動くアレのことです。「オーディオスペクトラム」と呼ばれることもあります。

みなさんが一番よく目にしているのは、音楽の周波数に合わせて動くビジュアライザーではないでしょうか。

nanaでも、周波数に応じてうねうね動くようになっています。

nana上で動くビジュアライザー

音楽を流して、リアルタイムでビジュアライザーを動かすには、その音楽のオーディオデータを取り出す必要があります。

iOS版nanaでは、音源の再生にAVPlayerを使っており、AVPlayerでオーディオデータを取り出す方法について調べたところ、MTAudioProcessingTapが使えそう(というか、これしか方法がなさそう)、ということが分かりました。

しかし、公式のサンプルコード(下記参照)以外、参考にできそうな情報やコードがなく、サンプルコードを参考に実装してみた結果、見事想定通りにオーディオのデータを取り出すことができ、採用に至りました。

活用例

nanaではビジュアライザー用途でMTAudioProcessingTapを使っていますが、それ以外にも様々な用途があります。

基本的な用途だと、

  • レベルメーター(音量の可視化)
  • SFSpeechRecognizerと連携させて音声認識
  • AudioUnitを使って再生される音を加工する(方法は後述)

などが挙げられます。

応用的には、オーディオデータを解析してできることはなんでもできるので、例えば楽曲のキー・コード・音程の検出など、いろんなことに使えそうですね!

実装例

実装では、こちらの公式のサンプルコードが大変参考になりました。

ただしかなり古く、中身はObjective-Cで書かれています。古すぎてそのままではプロジェクトのビルドに失敗するので、storyboardのビルドバージョンを変えたり、適切なmovieURLを設定してあげたりする必要があります。

https://developer.apple.com/library/archive/samplecode/AudioTapProcessor/Introduction/Intro.html

developer.apple.com

AVAudioEngineではAVAudioPCMBufferが用意されていますが、AVPlayerではこのMTAudioProcessingTapを使って、AudioBufferListを取り出すことができます。

呼び出し元のAVPlayerクラス

上のサンプルコードはObjective-Cですが、Swiftで書くとこんな感じにすっきり書けます。

※実行環境: Xcode 12.5・Swift 5.4

let player = AVPlayer(url: soundURL)
// ビジュアライザーの準備
if let track = player.currentItem?.asset.tracks(withMediaType: .audio).first {
    visualizer.audioAssetTrack = track
    player.currentItem?.audioMix = visualizer.audioMix
}
player.play()

ビジュアライザーの音声解析クラス

基本的にはサンプルコードの通りになります。(Objective-Cで書いた場合の話です。)

MTAudioProcessingTapでは、初期化・ファイナライズ・準備・処理中・終了時に動くコールバックをそれぞれ設定してあげる必要があります。

static void tap_InitCallback(MTAudioProcessingTapRef tap, void *clientInfo, void **tapStorageOut);
static void tap_FinalizeCallback(MTAudioProcessingTapRef tap);
static void tap_PrepareCallback(MTAudioProcessingTapRef tap, CMItemCount maxFrames, const AudioStreamBasicDescription *processingFormat);
static void tap_ProcessCallback(MTAudioProcessingTapRef tap, CMItemCount numberFrames, MTAudioProcessingTapFlags flags, AudioBufferList *bufferListInOut, CMItemCount *numberFramesOut, MTAudioProcessingTapFlags *flagsOut);
static void tap_UnprepareCallback(MTAudioProcessingTapRef tap);

また、上記5つのコールバック間でデータを受け渡すために、下記のような構造体を準備します。

typedef struct AVAudioTapProcessorContext {
    Boolean supportedTapProcessingFormat;
    Boolean isNonInterleaved;
    Float64 sampleRate;
    AudioUnit audioUnit;
    Float64 sampleCount;
    void *self;
} AVAudioTapProcessorContext;

再生する音源にエフェクトをかけて加工したい場合は、加工用AudioUnitの準備とそのコールバック関数の設定をtap_PrepareCallback関数内で行います。

自前で作ったエフェクトをかけたり、自力で信号処理をする場合は、AU_RenderCallback関数内に処理を書くことができます。

加工しない場合は、AudioUnitやAU_RenderCallback関数の準備は要りません。

以下のコードでは、例として再生する音源にバンドパスフィルタをかけるエフェクトを設定しています。

static void tap_PrepareCallback(MTAudioProcessingTapRef tap, CMItemCount maxFrames, const AudioStreamBasicDescription *processingFormat)
{
    AVAudioTapProcessorContext *context = (AVAudioTapProcessorContext *)MTAudioProcessingTapGetStorage(tap);
    context->sampleRate = processingFormat->mSampleRate;
    context->supportedTapProcessingFormat = true;
    
    // ~~いろいろ省略~~
    
    // エフェクトの準備(バンドパスフィルタ)
    AudioUnit audioUnit;
    
    AudioComponentDescription audioComponentDescription
    audioComponentDescription.componentType = kAudioUnitType_Effect;
    audioComponentDescription.componentSubType = kAudioUnitSubType_BandPassFilter;
    audioComponentDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    audioComponentDescription.componentFlags = 0;
    audioComponentDescription.componentFlagsMask = 0;

    AudioComponent audioComponent = AudioComponentFindNext(NULL, &audioComponentDescription);
     if (audioComponent) {
        if (noErr == AudioComponentInstanceNew(audioComponent, &audioUnit)) {
            OSStatus status = noErr;
                // ~~いろいろ省略~~
                if (noErr == status) {
                    AURenderCallbackStruct renderCallbackStruct;
                    renderCallbackStruct.inputProc = AU_RenderCallback;
                    renderCallbackStruct.inputProcRefCon = (void *)tap;
                    status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &renderCallbackStruct, sizeof(AURenderCallbackStruct));
                }
                // ~~いろいろ省略~~
}

OSStatus AU_RenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)
{
    return MTAudioProcessingTapGetSourceAudio(inRefCon, inNumberFrames, ioData, NULL, NULL, NULL);
}

実際にオーディオデータを取り出して処理・解析するのはtap_ProcessCallback関数の部分になります。

nanaの場合、目的はビジュアライザー = 音声の周波数の可視化なので、この部分でFFT(Fast Fourier Transformation)しています。今回はAccelerate.frameworkのvDSPを使っています。

static void tap_ProcessCallback(MTAudioProcessingTapRef tap, CMItemCount numberFrames, MTAudioProcessingTapFlags flags, AudioBufferList *bufferListInOut, CMItemCount *numberFramesOut, MTAudioProcessingTapFlags *flagsOut)
{
    // ~~ いろいろ省略 ~~ 
    for (UInt32 i = 0; i < bufferListInOut->mNumberBuffers; i++) {
        // ここでオーディオデータが取り出せる
    }
}

課題

サンプルコードがObjective-Cだったこと、また当時呼び出し元のAVPlayer実装クラスもObjective-Cであり、Swiftを呼び出すのが難しそうだったことから、 ビジュアライザーの音声解析クラスのみObjective-Cで実装しています。

iOS版nanaのリポジトリでは、開発効率や将来性・メンテナーの不足などのさまざまな理由から、開発言語をSwiftに一本化することを図っており、Objective-CのコードをSwiftに書き換える、いわゆる「Swift化」を進めている最中です。

ところが、今回の実装によって、逆にObjective-Cのコードを増やしてしまいました😱

一方で、音の信号処理(特にリアルタイム処理)では、遅延や処理速度といった理由から、SwiftよりもObjective-CやC/C++で書いた方が結果として良い方向になることが多いと感じています。

今後、「Swift化」を試みる際は、遅延や処理速度などに影響が出ないということを確かめてから実装を進めていきたいと考えています。

注意

AVPlayerでストリーミング再生を行なっている時は、この方法でオーディオデータを取得することはできないようです。

nanaは再生にプログレッシブダウンロードを使用しているため、この方法を採用できました。

まとめ

今回はMTAudioProcessingTapを使って、AVPlayerで再生中のオーディオデータを取得する方法を紹介しました。

MTAudioProcessingTapを知った時、AVAudioEngine特有の技術だと思っていたことがAVPlayerでもできるんだ!と驚いたのを覚えています。

しかしAVAudioEngineのinstallTapほど導入は楽ではなく、またMTAudioProcessingTapの情報がほとんど出回っていないため、実装はちょっと大変でした…

どうしてもAVPlayer上でオーディオデータを取得したい!エフェクトをかけたい!という時に、この記事が少しでも実装の参考になれば嬉しいです!


nana musicは開発メンバーを募集しています!

気になっている方・興味のある方は、ぜひ一度ご連絡ください😉

www.wantedly.com