AVAssetReaderとAVAssetWriter

iOS4.0のAVAssetExportSessionでiPod Libraryからファイルの書き出しができるようになったことは以前のエントリーに書きましたが、さらにiOS4.1からは、AVAssetReaderによって事前にコピーすること無く、直接iPodLibraryのオーディオファイルのデータを生のデータで読み込むことが出来るようになっています。また、AVAssetWriterという、オーディオファイルを作ることの出来るクラスも追加されています。

AVAssetExportSessionがMP3などを読み込むのにかなり使い勝手の悪いものだったのですが、AVAssetReaderはExtAudioFile的に簡単に好きなフォーマットに変換していろんなファイルが読み込めるので、かなり良さげな感じです。

ちなみに、ここではオーディオファイルだけを扱いますが、ビデオの読み込みや書き出しも出来るはずですので、興味のある方はいろいろ調べてみることをお勧めします。

AVAssetReaderはiPod Libraryから直接リアルタイム再生できたりするのが一番の利点だと思うのですが、ここではあえてAVAssetReaderとAVAssetWriterを使って、ExportSession的にファイルの書き出しをやってみたいと思います。自分も情報の少ない中しらべて成功したという感じでもありますので、おかしなところがありましたらご指摘いただけると助かります。

以下が、サンプルソースです。MPMediaPickerControllerなどでMPMediaItem取得してこのメソッドに渡すとアプリのドキュメントフォルダに書き出されます。セットアップしている途中で引っかかりそうなところはエラー処理的にreturn NOで終わるようにしてあります。

必要なフレームワークは、AVFoundation.FrameworkとCoreMedia.Framework、あとMPMediaItemの取得でMediaPlayer.Frameworkといったところです。

- (BOOL)exportItem:(MPMediaItem *)item
{
    NSError *error = nil;
    
    NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
                                  [NSNumber numberWithFloat:44100.0],AVSampleRateKey,
                                  [NSNumber numberWithInt:2],AVNumberOfChannelsKey,
                                  [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
                                  [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                                  [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
                                  [NSNumber numberWithBool:0], AVLinearPCMIsBigEndianKey,
                                  [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
                                  [NSData data], AVChannelLayoutKey, nil];
    
    //読み込み側のセットアップ
    
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *URLAsset = [AVURLAsset URLAssetWithURL:url options:nil];
    if (!URLAsset) return NO;
    
    AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:URLAsset error:&error];
    if (error) return NO;
    
    NSArray *tracks = [URLAsset tracksWithMediaType:AVMediaTypeAudio];
    if (![tracks count]) return NO;
    
    AVAssetReaderAudioMixOutput *audioMixOutput = [AVAssetReaderAudioMixOutput
                                                   assetReaderAudioMixOutputWithAudioTracks:tracks
                                                   audioSettings:audioSetting];
    
    if (![assetReader canAddOutput:audioMixOutput]) return NO;
    
    [assetReader addOutput:audioMixOutput];
    
    if (![assetReader startReading]) return NO;
    
    
    //書き込み側のセットアップ
    
    NSString *title = [item valueForProperty:MPMediaItemPropertyTitle];
    NSArray *docDirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docDir = [docDirs objectAtIndex:0];
    NSString *outPath = [[docDir stringByAppendingPathComponent:title]
                         stringByAppendingPathExtension:@"wav"];
    
    NSURL *outURL = [NSURL fileURLWithPath:outPath];
    AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outURL
                                                          fileType:AVFileTypeWAVE
                                                             error:&error];
    if (error) return NO;
    
    AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
                                                                              outputSettings:audioSetting];
    assetWriterInput.expectsMediaDataInRealTime = NO;
    
    if (![assetWriter canAddInput:assetWriterInput]) return NO;
    
    [assetWriter addInput:assetWriterInput];
    
    if (![assetWriter startWriting]) return NO;
    
    
    
    //コピー処理
    
    [assetReader retain];
    [assetWriter retain];
    
    [assetWriter startSessionAtSourceTime:kCMTimeZero];
    
    dispatch_queue_t queue = dispatch_queue_create("assetWriterQueue", NULL);
    
    [assetWriterInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{
        
        NSLog(@"start");
        
        while (1)
        {
            if ([assetWriterInput isReadyForMoreMediaData]) {
                
                CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
                
                if (sampleBuffer) {
                    [assetWriterInput appendSampleBuffer:sampleBuffer];
                    CFRelease(sampleBuffer);
                } else {
                    [assetWriterInput markAsFinished];
                    break;
                }
            }
        }
        
        [assetWriter finishWriting];
        [assetReader release];
        [assetWriter release];
        
        NSLog(@"finish");
    }];
    
    dispatch_release(queue);
    
    return YES;
}

全体の手順としては

① MPMediaItemから読み込みたい音楽のAVURLAssetを取得
② 読み込む時のフォーマットをNSDictionaryで作成
③ AVAssetReaderを作成
④ AVAssetReaderAudioMixOutputを作成してAVAssetReaderに取り付け
⑤ AVAssetReaderのstartReadingで読み出しの準備

⑥ 書き出し先のNSURLを作成
⑦ 書き出すファイルのフォーマットをNSDictionaryで作成(今回は読み込み側と共通)
⑧ AVAssetWriterを作成
⑨ AVAssetWriterInputを作成してAVAssetWriterに取り付け
⑩ AVAssetWriterのstartWritingで書き込みの準備
 
⑪ AVAssetWriterのstartSessionAtSourceTimeで書き出しセッションの開始
⑫ AVAssetWriteInputのrequestMediaDataWhenReadyOnQueue:usingBlock:で書き出し開始

⑬ AVAssetReaderAudioMixOutputのcopySampleBufferでCMSampleBufferを取得
⑭ AVAssetWriterInputのappendSampleBuffer:で書き込み
⑮ 終わるまで⑬と⑭を繰り返し
⑯ 終わったらAVAssetWriterInputのmarkAsFinishedとAVAssetWriteのfinishWritingでファイルを閉じる

となっています。

ちょっと解説をしていきますと、最初にNSDictionaryで作っているのが書き出したいWAVEファイルのフォーマットです。AVAssetReaderで読み込むときのフォーマットであればもうちょっと無くても大丈夫なパラメータもあるのですが、AVAssetWriterで書き出すときは結構細かく全部指定しないと怒られます。AVChannelLayoutKeyなんかは指定したくなくても空のNSDataを渡してあげないといけません。今回は読み込みも書き出しも同じフォーマットを使っています。

AVAssetReaderやAVAssetWriterは、直接読み込んだり書き出したりという構造にはなっていないらしく、AVAssetReaderにはAVAssetReaderAudioMixOutputを、AVAssetWriterにはAVAssetWriteInputをそれぞれ取り付けて、それ経由で読み込みや書き出しを行わなければいけないようです。AVPlayerの様にお手軽ではないのですが、CMSampleBufferという生のオーディオデータを保持しているものが使えるので、そこからAudioBufferListを取得してあれこれ自由にデータを扱うことが出来ます。

AVAssetWriterのデータ書き込みは今回で一番調べるのに苦労したところで、最後のコピー処理のところでrequestMediaDataWhenReadyOnQueue:usingBlock:というメソッドを使っています。マイクからの入力をちょっとずつ書き込むなんて言う場合には直接appendSampleBufferで書き込んじゃっても大丈夫かもしれませんが、今回のようにオフラインで1曲分ガンガン連続で書き込む場合には、試すと頭の数秒程度しか書き込まれていない状態で終わってしまいます。AVAssetWriterInputに渡したデータがファイルに書き出しされないうちに、AVAssetWriterInputの持ってるバッファを超えてデータを渡そうとしても、isReadyForMoreMediaDataでNOが返ってきて書き込ませてくれません。なので、isReadyForMoreMediaDataがYESになるまで待ってから次のデータを書き込むということをdispatchを使ってバックグラウンドでやっているという感じです。

単純なファイルコピーに関しては以上ですが、リアルタイム再生用などでCMSampleBufferから生のデータを取得する方法は、また次のエントリに書いてみようと思います。

AVAssetReaderとAVAssetWriter」への10件のフィードバック

  1. Another84

    Hi! Thank for great blog!
    One question : is this possible with kAudioFormatAppleLossless ?
    With this setting
    NSDictionary *audioSetting = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithFloat:44100.0],AVSampleRateKey,
    [NSNumber numberWithInt:2],AVNumberOfChannelsKey,
    [NSNumber numberWithInt:kAudioFormatAppleLossless], AVFormatIDKey,
    [NSData data], AVChannelLayoutKey, nil];
    i have this error “-[AVAssetReaderAudioMixOutput initWithAudioTracks:audioSettings:] AVAssetReaderOutput does not currently support compressed output'”
    is this possible to convert to m4a, not to wav ?

    返信
  2. RIKU

    いつも参考にさせていただいております。
    今制作しているアプリでは、マイクで録音して、書き出した音声ファイルの生のデータを取得してサーバーへ送信するアプリなんですが、AVAssetReader/AVAssetWriterで音声ファイルを読み込み、生のデータとして保存できるのでしょうか?書き出すときにNSDataタイプに格納することがふさわしいのでしょうか?ヘッダなしの生のオーディオデータが必要です。
    お手間をおかけしますが、ご指導をよろしくお願いします。

    返信
  3. Yasoshima

    マイクで録音するという事でしたらAVAssetReaderやAVAssetWriterは使う必要はないと思います。AVAudioRecorderで必要なフォーマットを指定してオーディオファイルに録音して、ヘッダが必要ないという事でしたら、ヘッダを飛ばしたところからファイルを読み込んで送信すれば良いと思います。

    返信
  4. si

    管理者様
    はじめまして。とても有用な記事が多く、参考にさせていただいています。
    私もiPodライブラリの曲を使って色々したいと考えていて、この記事のプログラムをテストしてみたのですが、どうしてもうまく動作しない部分があるので質問させてください。
    ビルドして実機で動作させた際、初回は正しくオーディオデータがコピーされログでも”finish”のメッセージが確認できるのですが、二回目以降exportItemを呼び出すと if (![assetWriter startWriting]) に引っかかってNOが返されてしまいます。実機からアプリを削除し、プロジェクトをクリーンして再度ビルド、転送すると正常に動作しますが、先ほど同様二回目以降は失敗してしまいます。
    何か解決策等ありますでしょうか?最近本格的にiOSアプリ開発を始めたのでもしかしたら初歩的な部分で躓いている可能性がありますが…何卒よろしくお願いします。
    ちなみにiOS5.0、Xcode4.2を使用しています。

    返信
  5. Yasoshima

    確認してみましたが、すでに存在するファイルは上書きできず、startWritingでエラーが出て止まってしまいます。上書きしないように書きだせば問題ないようです。ご確認ください。

    返信
  6. si

    先ほど質問させて頂いた者です。早速のお返事ありがとうございます。
    ご指摘の通り、テストの際同じ曲を選択していたことが原因のようでした。同じ名前で作りたい場合はNSFileManagerで確認・削除する必要がありそうですね…盲点でした。
    余談になりますが、その後ARCを有効にしていた事で[audioMixOutput copyNextSampleBuffer]が動かない問題が発生しましたが、ARCを無効にしてソース通りretain及びreleaseを記述することで解決しました。
    改めてこの度はありがとうございました。しばらくはオーディオ関係を色々やっていこうと思います。

    返信
  7. GTJ

    いつも参考にさせていただいています。
    質問です。
    dispatch_release(queue);
    を実行している時点ではまだqueueを使ってるような気がするのですが問題ないのでしょうか?

    返信
  8. ピンバック: How to: How do I record audio on iPhone with AVAudioRecorder? | SevenNet

  9. ピンバック: Answer: How do I record audio on iPhone with AVAudioRecorder? #programming #fix #solution | IT Info

コメントを残す

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