オーディオファイル その2 AudioFile

今回は、Audio File APIを使ったオーディオファイルの読み書きの方法を見て行きたいと思います。前回のExtendedAudioFileはフォーマットを指定すれば勝手に変換して読み書きしてくれましたが、Audio Fileではほぼそのまんまデータが読み込まれますので、変換が必要であれば自前で実装するか、AudioToolboxにあるAudio Converterを使うということになります。

オーディオファイルの読み込み

オーディオファイルの読み込みをする時は、

1 オーディオファイルを開く
2 オーディオファイルのフォーマットを取得する
3 読み込むバッファを確保する
4 バッファへデータを読み込む(必要な分だけ繰り返す)
5 終わったらオーディオファイルを閉じる

というのが基本的な流れになります。

オーディオをファイルを開く関数は以下のものがあります。

//CFURLでオーディオファイルを開く
extern OSStatus	
AudioFileOpenURL (CFURLRef inFileRef, 
                  SInt8 inPermissions, 
                  AudioFileTypeID inFileTypeHint,
                  AudioFileID *outAudioFile)

//FSRefでオーディオファイルを開く
extern OSStatus	
AudioFileOpen (const struct FSRef *inFileRef, 
                SInt8 inPermissions, 
                AudioFileTypeID inFileTypeHint,
                AudioFileID *outAudioFile)

inFileRefには開きたいオーディオのファイルパスを渡します。inFileTypeHintには開こうとするファイルの種類(AIFFなど)が分かっていれば指定、何を読み込むか分かんなければ0を入れてもいいですし、間違っていても問題ありません。ヒントっていうくらいですから合ってれば効率的に開けるって位のものだと思います。outAudioFileは開いたオーディオファイルのIDが返ってきますので、開いたファイルに対して何か行う時にはこのAudioFileIDを使います。inPermissionsにはファイルを開く上でのアクセス権を設定します。以下のような定数が定義されています。

enum {
  fsCurPerm                     = 0x00, /* open access permissions in ioPermssn */
  fsRdPerm                      = 0x01,
  fsWrPerm                      = 0x02,
  fsRdWrPerm                    = 0x03,
  fsRdWrShPerm                  = 0x04
};

読み込みで開くならfsRdPermというところでしょうか。

オーディオファイルのプロパティ(情報)の取得・設定を行うのは、Core Audioでおなじみのパターンの以下の関数です。

//オーディオファイルの情報を取得する
extern OSStatus
AudioFileGetProperty(AudioFileID inAudioFile,
                     AudioFilePropertyID inPropertyID,
                     UInt32 *ioDataSize,
                     void *outPropertyData)

//オーディオファイルの情報を設定する
extern OSStatus
AudioFileSetProperty(AudioFileID inAudioFile,
                     AudioFilePropertyID inPropertyID,
                     UInt32 inDataSize,
                     const void *inPropertyData)

inAudioFileにAudioFileIDを、プロパティの種類をinPropertyIDに、ioDataSizeには値を入れるプロパティのサイズを、outPropertyDataにはプロパティ取得・設定先を渡します。

プロパティは以下のようなものがあります。

enum
{
    kAudioFilePropertyFileFormat      = 'ffmt', //ファイルタイプ
    kAudioFilePropertyDataFormat            = 'dfmt', //フォーマット
    kAudioFilePropertyIsOptimized           = 'optm',
    kAudioFilePropertyMagicCookieData       = 'mgic',
    kAudioFilePropertyAudioDataByteCount    = 'bcnt', //バイト単位での長さ
    kAudioFilePropertyAudioDataPacketCount  = 'pcnt', //パケット単位での長さ
    kAudioFilePropertyMaximumPacketSize     = 'psze',
    kAudioFilePropertyDataOffset            = 'doff',
    kAudioFilePropertyChannelLayout         = 'cmap',
    kAudioFilePropertyDeferSizeUpdates      = 'dszu',
    kAudioFilePropertyDataFormatName        = 'fnme',
    kAudioFilePropertyMarkerList            = 'mkls',
    kAudioFilePropertyRegionList            = 'rgls',
    kAudioFilePropertyPacketToFrame         = 'pkfr',
    kAudioFilePropertyFrameToPacket         = 'frpk',
    kAudioFilePropertyChunkIDs              = 'chid',
    kAudioFilePropertyInfoDictionary        = 'info',
    kAudioFilePropertyPacketTableInfo       = 'pnfo',
    kAudioFilePropertyFormatList            = 'flst',
    kAudioFilePropertyPacketSizeUpperBound  = 'pkub',
    kAudioFilePropertyReserveDuration       = 'rsrv',
    kAudioFilePropertyEstimatedDuration     = 'edur',
    kAudioFilePropertyBitRate               = 'brat'
};

フォーマットとかファイルの長さの取得・設定あたりが主な使い道だと思います。ちょっと名前のつけられ方がややこしいですが、FileFormatがAIFFなどのファイルタイプで、DataFormatがAudioStreamBasicDescriptionで表されるフォーマットになります。

オーディオファイルからのデータの読み込みを行うのは、以下の関数です。

//バイト単位でデータを読み込む
extern OSStatus	
AudioFileReadBytes (AudioFileID inAudioFile,
                    Boolean     inUseCache,
                    SInt64      inStartingByte, 
                    UInt32      *ioNumBytes, 
                    void        *outBuffer)

//パケット単位でデータを読み込む
extern OSStatus
AudioFileReadPackets (AudioFileID                  inAudioFile, 
                      Boolean                      inUseCache,
                      UInt32                       *outNumBytes,
                      AudioStreamPacketDescription *outPacketDescriptions,
                      SInt64                       inStartingPacket, 
                      UInt32                       *ioNumPackets, 
                      void                         *outBuffer)

AudioFileReadBytesがバイトデータでの読み込みで、AudioFileReadPacketsがパケット単位での読み込みになります。ExtendedAudioFileでは読み込んだ分だけ読み込み位置が進んでくれましたが、こちらは毎度位置を自分で進めてinStartingByteに指定します。また、オーディオデータの読み込み先もAudioBufferListではなく普通のメモリ領域になります。

なお、読み込むオーディオデータのバイトオーダーは、オーディオファイルのフォーマットの種類に関わらず、ネイティブなエンディアンに変換された状態で読み込まれるようです。

MP3とかAACとかの圧縮フォーマットを読み込んだり、サンプリング周波数を変換して読み込む場合は、パケット単位で読み込んでデコーダに渡したりする感じになりますが、Tiger以降ではExtendedAudioFileがあるので、特殊な事をするのでなければ、ExtendedAudioFileを使った方が良いと思います。

ちなみに、ExtendedAudioFileの読み込み速度と、バイト単位での読み込み速度が10倍くらい差があると前回書いていましたが、改めて調べたらそんなには差がありませんでした。自分の計りそこないだったかも知れませんし、パケット単位での読み込みが改善されたのかもしれません。

読み込みや書き込みが終わったオーディオファイルを閉じるのは、AudioFileClose関数です。

extern OSStatus
AudioFileClose	(AudioFileID inAudioFile)

AudioFileIDを渡してファイルを閉じます。少なくとも書き込み時にはちゃんと閉じておかないとファイルが読めません。読み込みの時でも、開きっぱなしにしておけるファイルの数に上限があるようなので、必要なければ閉じておくようにしておいた方が良いと思います。

オーディオファイルの書き込み

オーディオファイルを書き込むには、

1 オーディオファイルのフォーマットを作成する
2 フォーマットやファイルパスを指定してオーディオファイルを作成(または上書き)する
3 書き込むデータを用意する
4 データをオーディオファイルに渡して書き込む(必要な分だけ繰り返す)
5 書き込みが終わったらファイルを閉じる 

といった流れになります。

オーディオファイルを作成・上書きしてAudioFileIDを取得するには以下の関数を使います。

//URLでファイルパスを指定してオーディオファイルを作成・上書きする
extern OSStatus	
AudioFileCreateWithURL (CFURLRef                          inFileRef,
                        AudioFileTypeID                   inFileType,
                        const AudioStreamBasicDescription *inFormat,
                        UInt32                            inFlags,
                        AudioFileID                       *outAudioFile)

//FSRefでファイルパスを指定してオーディオファイルを作成する
extern OSStatus	
AudioFileCreate (const struct FSRef                *inParentRef, 
                 CFStringRef                       inFileName,
                 AudioFileTypeID                   inFileType,
                 const AudioStreamBasicDescription *inFormat,
                 UInt32                            inFlags,
                 struct FSRef                      *outNewFileRef,
                 AudioFileID                       *outAudioFile)

//FSRefでファイルパスを指定してオーディオファイルを上書きする
extern OSStatus	
AudioFileInitialize (const struct FSRef                *inFileRef,
                     AudioFileTypeID                   inFileType,
                     const AudioStreamBasicDescription *inFormat,
                     UInt32                            inFlags,
                     AudioFileID                       *outAudioFile)

FSRefでファイルパスを指定する方は、新規作成と上書きでAudioFileCreateとAudioFileInitializeという二つの関数に分かれていますが、URLで指定する方は新規作成も上書きもAudioFileCreateWithURL関数ひとつでまかなえます。

当然ですが、読み込みと違ってinFileTypeにはちゃんとファイルタイプを指定します。また、それに合わせたフォーマットをinFormatに渡さないとエラーが返ってきてファイルを作成する事が出来ません。

inFlagsに渡すフラグは以下のようなものが用意されています。

enum {
	kAudioFileFlags_EraseFile = 1,
	kAudioFileFlags_DontPageAlignAudioData = 2
};

0を指定すれば、既にファイルが存在していた場合はエラーが返って上書きされません。EraseFileなら上書きされます。DontPageAlignAudioDataは、上書き可能に加えて、データに余計なスペースをつけないって感じだと思いますが、何がどう差が出るかは良くわかりません。

オーディオファイルへ実際に書き込みを行うには以下の関数を使います。

//バイト単位で書き込む
extern OSStatus	
AudioFileWriteBytes (AudioFileID inAudioFile,  
                     Boolean     inUseCache,
                     SInt64      inStartingByte, 
                     UInt32      *ioNumBytes, 
                     const void  *inBuffer)

//パケット単位で書き込む
extern OSStatus	
AudioFileWritePackets (AudioFileID                        inAudioFile,  
                       Boolean                            inUseCache,
                       UInt32                             inNumBytes,
                       const AudioStreamPacketDescription *inPacketDescriptions,
                       SInt64                             inStartingPacket, 
                       UInt32                             *ioNumPackets, 
                       const void                         *inBuffer)

読み込みと同じような感じで、バイト単位とパケット単位の二つの書き込み方法があります。inStartingByteで毎度位置を指定しなくてはいけなかったり、ioNumBytesに実際に書き込まれた分だけの値が返ってくるのも同じです。

ただ、読み込み時のデータのバイトオーダーは勝手にネイティブに変換されていましたが、書き込み時にはちゃんとフォーマットに合わせたエンディアンに変換しておかなければいけません。

また、inStartingByteで書き込み位置を、まだ何も書き込んでいない位置に飛ばして指定すると、その間は無音が勝手に入ります。

書き込み用の関数はこれくらいで、プロパティの設定・取得や、オーディオファイルを閉じるのは読み込みと共通です。

サンプルコード(オーディオファイルのコピー)

オーディオファイルの読み込みと書き込みを行うサンプルとして、オーディオファイルのコピーを行ってみたいと思います。ファイルタイプやフォーマットはリニアPCMに限定して、全く変えずコピーするようにしています。

FoundationToolを新規作成して、AudioToolbox.Frameworkをプロジェクトに追加し、以下のコードのようにmain関数を記述します。inPathは適当に書いてあるだけなので、何かコピー元となるリニアPCMのオーディオファイルを指定してみてください。

(※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 copyFrames = 1024;
    
    //その他の変数の宣言
    OSStatus err = noErr;
    UInt32 size;
    AudioFileID inID;
    AudioFileID outID;
    AudioFileTypeID fileType;
    AudioStreamBasicDescription format;
    Byte *ioBuffer = NULL;
    
    //読み込み側のオーディオファイルを開く(2008/6/26修正)
    //NSURL *inUrl = [NSURL URLWithString:inPath];
    NSURL *inUrl = [NSURL fileURLWithPath:inPath];
    err = AudioFileOpenURL((CFURLRef)inUrl, fsRdPerm, 0, &inID);
    if (err != noErr) goto catchErr;
    
    //オーディオファイルのフォーマットを取得する
    size = sizeof(format);
    err = AudioFileGetProperty(
        inID, kAudioFilePropertyDataFormat, &size, &format);
    if (err != noErr) goto catchErr;
    
    //リニアPCMでなければ終了する
    if (format.mFormatID != kAudioFormatLinearPCM) {
        NSLog(@"Not LinearPCM");
        goto catchErr;
    }
    
    //オーディオファイルのファイルタイプを取得する
    size = sizeof(fileType);
    err = AudioFileGetProperty(
        inID, kAudioFilePropertyFileFormat, &size, &fileType);
    if (err != noErr) goto catchErr;
    
    //ファイルタイプによってエンディアンを設定する
    switch (fileType) {
        case kAudioFileAIFFType:
            format.mFormatFlags |= kAudioFormatFlagIsBigEndian;
            break;
        case kAudioFileAIFCType:
            format.mFormatFlags |= kAudioFormatFlagIsBigEndian;
            break;
        case kAudioFileWAVEType:
            format.mFormatFlags &= ~kAudioFormatFlagIsBigEndian;
            break;
        default:
            NSLog(@"This file is not supported");
            goto catchErr;
            break;
    }

    NSLog(@"FileType = %@", NSFileTypeForHFSTypeCode(fileType));
    NSLog(@"Samplerate = %f", format.mSampleRate);
    NSLog(@"FormatID = %@", 
        NSFileTypeForHFSTypeCode(format.mFormatID));
    NSLog(@"FormatFlags = %4.4x", format.mFormatFlags);
    NSLog(@"BitsPerChannels = %u", format.mBitsPerChannel);
    NSLog(@"ChannelsPerFrame = %u", format.mChannelsPerFrame);
    NSLog(@"FramesPerPacket = %u", format.mFramesPerPacket);
    NSLog(@"BytesPerFrame = %u", format.mBytesPerFrame);
    NSLog(@"BytesPerPacket = %u", format.mBytesPerPacket);
    
    //書き出し側のオーディオファイルのパスを作成する(2008/6/26修正)
    NSString *extension = [inPath pathExtension];
    NSString *outPathWithoutExtension = 
        [[inPath stringByDeletingPathExtension] 
            stringByAppendingString:@"-export"];
    NSString *outPath = 
        [outPathWithoutExtension stringByAppendingPathExtension:extension];
    //NSURL *outUrl = [NSURL URLWithString:outPath];
    NSURL *outUrl = [NSURL fileURLWithPath:outPath];
    
    //書き出し側のオーディオファイルを作成する。上書きしない
    err = AudioFileCreateWithURL(
        (CFURLRef)outUrl, fileType, &format, 0, &outID);
    if (err != noErr) goto catchErr;
    
    //データをコピーする
    SInt64 startingByte = 0;
    UInt32 ioBufferBytes = copyFrames * format.mBytesPerFrame;
    ioBuffer = calloc(1, ioBufferBytes);
    BOOL isEnd = NO;
    
    //バイトスワップが必要か判断する
    NSUInteger swapBytes = 
        format.mBytesPerFrame / format.mChannelsPerFrame;
    BOOL isSwap = 
        ((format.mFormatFlags & kAudioFormatFlagIsBigEndian) != 
        kAudioFormatFlagsNativeEndian) && (swapBytes > 1);
    
    //オーディオデータをコピーする
    while (!isEnd) {
        
        //読み込むバイト数を設定する
        UInt32 readBytes = ioBufferBytes;
        
        //オーディオファイルからメモリに読み込む
        err = AudioFileReadBytes(
            inID, false, startingByte, &readBytes, ioBuffer);
        
        //errにeofErrが返ればファイルの最後なので終了
        if (err == eofErr) {
            isEnd = YES;
        } else if (err != noErr) {
            NSLog(@"readbytes err");
            goto catchErr;
        }
        
        //オーディオファイルのフォーマットがネイティブ
        //エンディアンと違う場合はバイトスワップする
        if (isSwap) {
            NSUInteger i, j;
            for (i = 0; i < (readBytes / swapBytes); i++) {
                for (j = 0; j < swapBytes / 2; j++) {
                    Byte *ptr = &(ioBuffer[i * swapBytes]);
                    Byte temp = ptr[j];
                    ptr[j] = ptr[swapBytes - j - 1];
                    ptr[swapBytes - j - 1] = temp;
                }
            }
        }
        
        //オーディオファイルに書き込む
        err = AudioFileWriteBytes(
            outID, false, startingByte, &re
adBytes, ioBuffer);
        if (err != noErr) {
            NSLog(@"writebytes err");
            goto catchErr;
        }
        
        //読み書きのスタート位置を進める
        startingByte += readBytes;
    }
    
    NSLog(@"complete");
    
catchErr:
    
    if (err != noErr) NSLog(@"err = %d", err);
    
    //バッファを解放する
    if (ioBuffer != NULL) free(ioBuffer);
    
    //オーディオファイルを閉じる
    AudioFileClose(outID);
    AudioFileClose(inID);
    
    [pool drain];
    return 0;
}

プログラムを実行すると、微妙なファイル容量のずれはありますが、中身的には全く同じオーディオデータのコピーが出来るはずです。ファイルタイプによって読み込みと書き込みでエンディアンが変わってしまうので、スワップするようにしています。まあ、でもやっぱりめんどくさいので、普通に書き込むだけだったらExtendedAudioFile使った方が良いのではないでしょうか。

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

  1. pebble

    kAudioFileFlags_DontPageAlignAudioDataですが、kAudioFileWAVETypeで出力した場合はFLLRチャンクヘッダが出力されなくなるようです。

    返信

コメントを残す

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