月別アーカイブ: 2010年9月

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から生のデータを取得する方法は、また次のエントリに書いてみようと思います。