オーディオファイル その1 ExtendedAudioFile

Core AudioのAudioToolbox.Frameworkにはオーディオファイルの読み書きに2通り方法が用意されています。<AudioToolbox/AudioFile.h>での、ほぼ生のデータを直接扱う方法と、<AudioToolbox/ExtendedAudioFile.h>での、オーディオファイルの読み書きにコンバーターを組み合わせてあるものを使う方法です。

圧縮ファイルを扱う場合や、リニアPCMでサンプリング周波数を変換して扱いたい場合には、ExtendedAudioFile.hを使ったほうが楽だと思います。逆にリニアPCMでフォーマットの変換が全く必要ないときには、AudioFile.hのバイトデータでの読み書きを使うとパフォーマンス的に有利かもしれません。

今回は、ExtendedAudioFileの使い方を見ていきたいと思います。まず、読み込みを行うときにはオーディオファイルを開いてExtAudioFileRefというオブジェクトを取得します。それを行うのが以下の関数です。できるだけObjective-Cを使いたい自分としては、URLで開く方がおすすめです。

//FSRefでパスを指定して開く
extern OSStatus
ExtAudioFileOpen(const FSRef *inFSRef, ExtAudioFileRef *outExtAudioFile)

//CFURLRefでパスを指定して開く
extern OSStatus
ExtAudioFileOpenURL(CFURLRef inURL, ExtAudioFileRef *outExtAudioFile)

取得したExtAudioFileRefを使ってオーディオデータ読み込みを行うのが以下のExtAudioFileRead関数です。

extern OSStatus
ExtAudioFileRead(ExtAudioFileRef inExtAudioFile, 
                 UInt32 *ioNumberFrames,
                 AudioBufferList *ioData)

読み込みたいフレーム数とAudioBufferListを渡すと読み込まれます。ファイルの最後の方の読み込み時などで、渡したフレーム数より読み込まれたデータが少なければ、ioNumberFramesとioData内のmDataByteSizeが書き換えられます。

また、読み込み位置は読み込んだ分だけ勝手に進んでくれるので、頭からシーケンシャルに読み込む場合はただ繰り返し関数を呼ぶだけで大丈夫です。ファイルの任意の位置から読み込み始めたいときは、ExtAudioFileSeek関数を使って、読み込み位置を移動させます。

extern OSStatus
ExtAudioFileSeek(ExtAudioFileRef inExtAudioFile,
                 SInt64 inFrameOffset)

inFrameOffsetにはオーディオファイル側のフレーム数を指定します。サンプリング周波数を変換して読み込んでいると変換後のレートで指定してしまいがちなので注意が必要です。

オーディオファイルの書き込みを行うには以下の関数でオーディオファイルを作成し、ExtAudioFileRefを取得します。

//FSRefとファイルネームでオーディオファイルを作成する
extern OSStatus
ExtAudioFileCreateNew(const FSRef *inParentDir,
                      CFStringRef inFileName, 
                      AudioFileTypeID inFileType,
                      const AudioStreamBasicDescription *inStreamDesc,
                      const AudioChannelLayout *inChannelLayout,
                      ExtAudioFileRef *outExtAudioFile)

//CFURLRefでオーディオファイルを作成する
extern OSStatus
ExtAudioFileCreateWithURL(CFURLRef inURL,
                          AudioFileTypeID inFileType,
                          const AudioStreamBasicDescription *inStreamDesc,
                          const AudioChannelLayout *inChannelLayout,
                          UInt32 inFlags,
                          ExtAudioFileRef *outExtAudioFile)

inFileTypeIDにはWAVやAIFFなどのオーディオフォーマットの種類を、inStreamDescにはオーディオファイルのフォーマットを
渡します。ChannelLayoutは必要なければNULLでかまいません。URLの方にあるinFlagsでは上書きするかなどの設定が出来ます。

オーディオファイルに書き込みを行うのは以下の関数です。

extern OSStatus
ExtAudioFileWrite(ExtAudioFileRef inExtAudioFile,
                  UInt32 inNumberFrames,
                  const AudioBufferList *ioData)

extern OSStatus
ExtAudioFileWriteAsync(ExtAudioFileRef inExtAudioFile,
                       UInt32 inNumberFrames,
                       const AudioBufferList *ioData)

上の方のExtAudioFileWriteは普通にこの関数が呼ばれたタイミングで書き込まれますが、下のExtAudioFileWriteAsyncはバッファにデータがためこまれて非同期に書き込みが行われます。バッチ処理など立て続けに書き込みを行うときはWriteで、オーディオデバイスからの録音などコールバックを邪魔したくないときなどはWriteAsyncという使い分けになると思います。

オーディオファイルの読み書きが必要なくなったときにExtAudioFileRefを解放してファイルを閉じるのが以下のExtAudioFileDispose関数です。ExtAudioFileRefはそんなにたくさん同時に作っておけないようなので、使うものだけを残しておいて、いらないものはこまめに解放するようにしたほうが良いです。

extern OSStatus
ExtAudioFileDispose(ExtAudioFileRef inExtAudioFile)

ExtendedAudioFileでオーディオファイルの情報の取得や設定をするときは以下の関数で行います。Core Audioのときのように〜SetPropertyで設定、〜GetPropertyで取得というおなじみのパターンです。

extern OSStatus
ExtAudioFileGetProperty(ExtAudioFileRef	inExtAudioFile,
                        ExtAudioFilePropertyID inPropertyID,
                        UInt32 *ioPropertyDataSize,
                        void *outPropertyData)

extern OSStatus
ExtAudioFileSetProperty(ExtAudioFileRef	inExtAudioFile,
                        ExtAudioFilePropertyID inPropertyID,
                        UInt32 inPropertyDataSize,
                        const void *inPropertyData)

設定・取得出来るプロパティは以下のものが定義されています。

enum { // ExtAudioFilePropertyID
    kExtAudioFileProperty_FileDataFormat        = 'ffmt',
    kExtAudioFileProperty_FileChannelLayout     = 'fclo',
    kExtAudioFileProperty_ClientDataFormat      = 'cfmt',
    kExtAudioFileProperty_ClientChannelLayout   = 'cclo',
	
    // read-only:
    kExtAudioFileProperty_AudioConverter        = 'acnv',
    kExtAudioFileProperty_AudioFile             = 'afil',
    kExtAudioFileProperty_FileMaxPacketSize     = 'fmps',
    kExtAudioFileProperty_ClientMaxPacketSize   = 'cmps',
    kExtAudioFileProperty_FileLengthFrames      = '#frm',

    // writable:
    kExtAudioFileProperty_ConverterConfig       = 'accf',
    kExtAudioFileProperty_IOBufferSizeBytes     = 'iobs',
    kExtAudioFileProperty_IOBuffer              = 'iobf' 
};
typedef UInt32 ExtAudioFilePropertyID;

よく使いそうなものを見ていくと、FileDataFormatはオーディオファイル自体のフォーマットで、ClientDataFormatはコンバーターで変換された後のフォーマット、FileLengthFramesがオーディオファイル自体のフレーム数です。

ちなみに、読み込み時にFileDataFormatを取得しても、エンディアンはファイルのものではなく、すでにマック側のネイティブなエンディアンのようです。インテルマックでしか検証していないのですが、ビッグエンディアンにしたAIFFとかCAFとかのフォーマットを取得してもビッグエンディアンのフラグは立っていませんでした。

読み込み時の全体の流れとしては、オーディオファイルを開く、オーディオファイルのフォーマットを取得する、クライアントフォーマットを設定する、オーディオデータを読み込む、閉じる、といった感じです。

書き込みの場合は、オーディオファイルをフォーマットを指定して新規作成する、クライアントフォーマットを設定する、オーディオデータを書き込む、閉じる、という風になります。

といったところで、オーディオファイルの読み書きのサンプルとして、フォーマットを変換してファイルのコピーを行ってみたいと思います。FoundationToolを作成して、<CoreAudio/AudioToolbox.h>をインポートし、main関数を以下のように記述します。変換元のファイルパスは、適当に入れてあるので必要に応じて変更してください。WAVでもAIFFでもmp3でもAACでも、マックが標準でデコードできるコーデックなら何でも読み込めるはずです。実行して”complete”とログに表示されたら、変換元のファイルと同じフォルダにWAVの16bitの22.050kHzのオーディオファイルが出来ていると思います。

(※2008/6/26変更 NSURLを取得するのにURLWithString:を使っていましたが、それだとスペースの含まれているファイルパスが開けないので、fileURLWithPath:に変更しました。)

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>


int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    //変換するファイル
    NSString *inPath = @"/ConvertFile/sample.aiff";

    //一度に変換するフレーム数
    UInt32 convertFrames = 1024;
	
    //変数の宣言
    OSStatus err = noErr;
    UInt32 size;
    ExtAudioFileRef inAudioFileRef = NULL;
    ExtAudioFileRef outAudioFileRef = NULL;
    AudioStreamBasicDescription inFileFormat, ioClientFormat, outFileFormat;
    void *ioData = NULL;
	
    //読み込み側のオーディオファイルを開く(2008/6/26修正)
    //NSURL *inUrl = [NSURL URLWithString:inPath];
    NSURL *inUrl = [NSURL fileURLWithPath:inPath];
    err = ExtAudioFileOpenURL((CFURLRef)inUrl, &inAudioFileRef);
    if (err != noErr) goto catchErr;
	
    //読み込み側のオーディオファイルからフォーマットを取得する
    //size = sizeof(ioClientFormat);(2009/12/4修正)
    size = sizeof(inFileFormat);
    err = ExtAudioFileGetProperty(
        inAudioFileRef, kExtAudioFileProperty_FileDataFormat,
         &size, &inFileFormat);
    if (err != noErr) goto catchErr;
	
    //書き出し側のオーディオファイルのパスを作成する(2008/6/26修正)
    NSString *outPath = 
        [[inPath stringByDeletingPathExtension] 
            stringByAppendingString:@"-export.wav"];
    //NSURL *outUrl = [NSURL URLWithString:outPath];
    NSURL *outUrl = [NSURL fileURLWithPath:outPath];
	
    //書き出し側のオーディオファイルのフォーマットを作成する
    outFileFormat.mSampleRate = 22050;
    outFileFormat.mFormatID = kAudioFormatLinearPCM;
    outFileFormat.mFormatFlags = 
        kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    outFileFormat.mBitsPerChannel = 16;
    outFileFormat.mChannelsPerFrame = inFileFormat.mChannelsPerFrame;
    outFileFormat.mFramesPerPacket = 1;
    outFileFormat.mBytesPerFrame = 
        outFileFormat.mBitsPerChannel / 8 * outFileFormat.mChannelsPerFrame;
    outFileFormat.mBytesPerPacket = 
        outFileFormat.mBytesPerFrame * outFileFormat.mFramesPerPacket;
	
    //書き出し側のオーディオファイルを作成する
    err = ExtAudioFileCreateWithURL(
        (CFURLRef)outUrl, kAudioFileWAVEType, &outFileFormat, 
        NULL, 0, &outAudioFileRef);
    if (err != noErr) goto catchErr;
	
    //読み書き両方のクライアントフォーマットを設定する
    ioClientFormat.mSampleRate = inFileFormat.mSampleRate;
    ioClientFormat.mFormatID = kAudioFormatLinearPCM;
    ioClientFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
    ioClientFormat.mBitsPerChannel = 32;
    ioClientFormat.mChannelsPerFrame = inFileFormat.mChannelsPerFrame;
    ioClientFormat.mFramesPerPacket = 1;
    ioClientFormat.mBytesPerFrame = 
        ioClientFormat.mBitsPerChannel / 8 * ioClientFormat.mChannelsPerFrame;
    ioClientFormat.mBytesPerPacket = 
        ioClientFormat.mBytesPerFrame * ioClientFormat.mFramesPerPacket;
	
    size = sizeof(ioClientFormat);
    err = ExtAudioFileSetProperty(
        outAudioFileRef, kExtAudioFileProperty_ClientDataFormat, 
        size, &ioClientFormat);
    if (err != noErr) goto catchErr;
	
    size = sizeof(ioClientFormat);
    err = ExtAudioFileSetProperty(
        inAudioFileRef, kExtAudioFileProperty_ClientDataFormat, 
        size, &ioClientFormat);
    if (err != noErr) goto catchErr;
	
    //オーディオデータの読み書きに使用するメモリ領域を確保する
    UInt32 allocByteSize = convertFrames * ioClientFormat.mBytesPerFrame;
    ioData = malloc(allocByteSize);
    if (!ioData) {
        err = 1002;
        goto catchErr;
    }
	
    //オーディオデータの読み書きに使用するAudioBufferListを作成する
    AudioBufferList ioList;
    ioList.mNumberBuffers = 1;
    ioList.mBuffers[0].mNumberChannels = ioClientFormat.mChannelsPerFrame;
    ioList.mBuffers[0].mDataByteSize = allocByteSize;
    ioList.mBuffers[0].mData = ioData;
	
    //オーディオデータをコピーする
    while (1) {
        //フレーム数とデータサイズを設定する
        UInt32 frames = convertFrames;
        ioList.mBuffers[0].mDataByteSize = allocByteSize;
		
        //読み込み側のオーディオファイルからオーディオデータを読み込む
        err = ExtAudioFileRead(inAudioFileRef, &frames, &ioList);
        if (err != noErr) goto catchErr;
		
        //最後まで読み込んだら終了
        if (frames == 0) break;
		
        //書き込み側のオーディオファイルへ書き込む
        err = ExtAudioFileWrite(outAudioFileRef, frames, &ioList);
        if (err != noErr) goto catchErr;
    }
	
    NSLog(@"complete");
	
	
catchErr:
	
    if (err != noErr) NSLog(@"err = %ld", err);
	
    //解放する
    if (ioData) free(ioData);
    if (inAudioFileRef) ExtAudioFileDispose(inAudioFileRef);
    if (outAudioFileRef) ExtAudioFileDispose(outAudioFileRef);
	
    [pool drain];
    return 0;
}

オーディオファイル その1 ExtendedAudioFile」への7件のフィードバック

  1. S.kagami

    こちらのブログあまり見かけないCocoa向けのオーディオの取り扱いが書いてあり、大変役にたっています。ありがとうございます!
    ちょっと質問なのですが、このファイル読み込みの後に、例えば音に対してディレイをかけたりといった処理をする場合にどの変数に対して処理をすればいいのですか?
    もしお時間ありましたらお答えいただけると嬉しいです。

  2. Yasoshima

    このエントリのコード中でエフェクトをかけるならば、オーディオデータの読み込みと書き込みの間でioDataに対して処理を行います。ディレイをかける場合は、別のメモリ領域を確保して一旦そちらに読み込んだデータをioDataからコピーしておき、ディレイさせる時間分前のデータをioDataに書き込めばよろしいかと思います。

  3. da1

    //読み込み側のオーディオファイルからフォーマットを取得する
    size = sizeof(ioClientFormat);
    は、
    //読み込み側のオーディオファイルからフォーマットを取得する
    size = sizeof(inFileFormat);
    ですよね?
    ものによってエラーになっていたので、調べていたら見つけました。

  4. Yasoshima

    ありがとうございます、修正しました。size = sizeof(AudioStreamBasicDescription)にしておいたほうが間違いが無くていいかもしれませんねぇ。

  5. t.machida

    大変参考にさせていただきました。
    そこで質問なのですが、ファイルからデコードやファイルへのエンコード処理を
    行う場合にExtAudioFileが使えるようですが、
    ファイルではなく、メモリ上にあるバイトからのエンコードやデコードを行って、
    その結果をバイト列で得られるようなクラスなどはあるのでしょうか?
    ずっと探しているのですが、本当に見つからなくて困っています。。

  6. Yasoshima

    返事が遅くなってしまいました。Audio Converter Serviceを使えばできるはずです。使い方は「iPhone Core Audioプログラミング」とか参考にされると良いと思います。

  7. t.machida

    ありがとうございます!本当に助かりました。
    すみません、もう一つAudioSessionの記事にも質問させて頂きました。
    こちらもお時間のあるときに見て頂ければ幸いです。

コメントは停止中です。