mp3ファイルからID3タグを読み込むサンプル

今度はAVFoundation Frameworkを使ってみた。
試したOSバージョンは10.7.2(Lion)

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

#import "AVMetadataItemAdditions.h"

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

    NSURL *url = [NSURL fileURLWithPath:@"/var/tmp/sample.mp3"];
    AVURLAsset *asset = [AVURLAsset assetWithURL:url];
    if (![asset.availableMetadataFormats containsObject:AVMetadataFormatID3Metadata]) {
        NSLog(@"*** ID3 tags not found in %@", url);
        exit(-1);
    }
    NSArray *origMetadata = [asset metadataForFormat:AVMetadataFormatID3Metadata];
    for (AVMetadataItem *item in origMetadata) {
        NSString *key = [item keyAsString];
        id value = item.value;

        NSLog(@"value for tag:%@ is:%@", key, value);
    }
    [pool drain];
    
    return 0;
}

アセットってのが音楽や動画を扱う単位になるみたい。\オープンディール!/


AVMetadataItemクラスにkeyAsStringなんてメソッド無いよね*1。実はカテゴリで追加してます。AVMetadataItemのkeyはなぜか数値を返すので文字列にデコードしている。

//  AVMetadataItemAdditions.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVMetadataItem.h>

@interface AVMetadataItem (KeyAsStringAdditions)
- (NSString *)keyAsString;
@end
//  AVMetadataItemAdditions.m
// cf:http://developer.apple.com/library/mac/#samplecode/avmetadataeditor/Introduction/Intro.html

#import "AVMetadataItemAdditions.h"

@implementation AVMetadataItem (KeyAsStringAdditions)
/*
 Get a string from a 4cc                 
 */    
static NSString *
stringForOSType(OSType theOSType)
{
    size_t len = sizeof(OSType);
    long addr = (unsigned long)&theOSType;
    char cstring[5];
    
    len = (theOSType >> 24) == 0 ? len - 1 : len;
    len = (theOSType >> 16) == 0 ? len - 1 : len;
    len = (theOSType >>  8) == 0 ? len - 1 : len;
    len = (theOSType >>  0) == 0 ? len - 1 : len;
    
    addr += (4 - len);
    
    theOSType = EndianU32_NtoB(theOSType);        // strings are big endian    
    
    strncpy(cstring, (char *)addr, len);
    cstring[len] = 0;
    
    return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding];
}
- (NSString *)keyAsString
{
    NSString *keyAsString = nil;
    if ([[self key] isKindOfClass:[NSString class]]) {
        keyAsString  = (NSString *)[self key];
    }
    else if ([[self key] isKindOfClass:[NSNumber class]]) {
        NSNumber *keyAsNumber = (NSNumber *)[self key];
        keyAsString = stringForOSType([keyAsNumber unsignedIntValue]);
    }
    else if ([[self key] isKindOfClass:[NSObject class]]) {
        keyAsString = [(NSObject *)[self key] description];
    }
    return keyAsString;
}
@end

処理の内容は「avmetadataeditor」という公式のサンプルコードから拝借しました。
出力例:

2012-01-08 04:33:19.465 AVFrameworkSample[4761:707] value for tag:TT2 is:Beautiful Trick
2012-01-08 04:33:19.483 AVFrameworkSample[4761:707] value for tag:TP1 is:Vivienne
2012-01-08 04:33:19.487 AVFrameworkSample[4761:707] value for tag:TCM is:ZUN
2012-01-08 04:33:19.487 AVFrameworkSample[4761:707] value for tag:TAL is:Flower Flag
2012-01-08 04:33:19.488 AVFrameworkSample[4761:707] value for tag:TRK is:5/9
2012-01-08 04:33:19.489 AVFrameworkSample[4761:707] value for tag:TPA is:1/1
2012-01-08 04:33:19.489 AVFrameworkSample[4761:707] value for tag:TYE is:2011
2012-01-08 04:33:19.490 AVFrameworkSample[4761:707] value for tag:TCO is:Soundtrack
2012-01-08 04:33:19.490 AVFrameworkSample[4761:707] value for tag:TCP is:1
2012-01-08 04:33:19.491 AVFrameworkSample[4761:707] value for tag:COM is:{
    identifier = iTunPGAP;
    language = eng;
    text = 0;
}
2012-01-08 04:33:19.492 AVFrameworkSample[4761:707] value for tag:TEN is:iTunes v7.6.2.9
2012-01-08 04:33:19.496 AVFrameworkSample[4761:707] value for tag:COM is:{
    identifier = iTunNORM;
    language = eng;
    text = " 0000167E 0000165A 00008A00 0000A583 00037A68 0003F12F 00008B81 00008BA8 0003B6B6 00032E2F";
}
2012-01-08 04:33:19.497 AVFrameworkSample[4761:707] value for tag:COM is:{
    identifier = iTunSMPB;
    language = eng;
    text = " 00000000 00000210 00000AE0 0000000000D32210 00000000 0072DA73 00000000 00000000 00000000 00000000 00000000 00000000";
}
2012-01-08 04:33:19.546 AVFrameworkSample[4761:707] value for tag:COM is:{
    identifier = "iTunes_CDDB_IDs";
    language = eng;
    text = "9+89347F9EE1B6AC6A6FA37AEBEA836D79+31558448";
}

蛇足: mp3ファイルにID3タグを書き込むサンプル(をやってみたかった)

結局上手く行かなかったわ。何が悪いのか分からない。
既存のID3v2.2.0タグが付いている曲に「アルバムアーティスト」を追加するという想定でいく。
まず、AVMetadataItemのリストを作る。上のサンプルの要領で既存の曲から読み込んで、TP2が無ければ付ける。

    ....
    // 元の曲のAssetに付いているID3のメタデータ
    NSArray *origMetadata = [asset metadataForFormat:AVMetadataFormatID3Metadata];
    // 書き込む為のID3のメタデータの入れ物
    NSMutableArray *newMetadata = [NSMutableArray array];
    
    AVMutableMetadataItem *albumArtistMetadataItem = nil;
    for (AVMetadataItem *item in origMetadata) {
        AVMutableMetadataItem *newItem = [[item mutableCopy] autorelease];
        if ([[newItem keyAsString] isEqual:@"TP2"] ||
                [[newItem keyAsString] isEqual:AVMetadataID3MetadataKeyBand]) { // @"TPE2"
            // アルバムアーティストが既にあったら覚えておく
            albumArtistMetadataItem = newItem;
        }
        [newMetadata addObject:newItem];
    }
    // アルバムアーティストが無かったら追加
    if (!albumArtistMetadataItem) {
        albumArtistMetadataItem = [AVMutableMetadataItem metadataItem];
        albumArtistMetadataItem.key = @"TP2";
        albumArtistMetadataItem.keySpace = AVMetadataKeySpaceID3;
        [newMetadata addObject:albumArtistMetadataItem];
    }
    // アルバームアーティスト名を設定
    albumArtistMetadataItem.value = @"FELT";
    ....


タグを含めAVAssetの情報を書き込むには、AVAssetExportSessionってのを使うらしい。
MP3のファイルタイプが無いのでAVFileTypeQuickTimeMovieを指定する。
presetには、中のフォーマットを変換する必要がない場合はpresetName:AVAssetExportPresetPassthroughを指定する。

    ....
    // write tags (NOT WORKS!!)
    NSString *outputFileType = AVFileTypeQuickTimeMovie;
    NSURL *destinationUrl = [NSURL fileURLWithPath:@"/var/tmp/sample_out.mp3"];

    AVAssetExportSession *session =
        [AVAssetExportSession exportSessionWithAsset:asset presetName:AVAssetExportPresetPassthrough];
    if (![[session supportedFileTypes] containsObject:outputFileType]) {
        return -1;
    }
    [session setOutputFileType:outputFileType];
    [session setOutputURL:destinationUrl];    
    [session setMetadata:newMetadata]; // set new metadata
    ....

書き込みは非同期で行われるので、フォアグラウンドスレッドで書き込み終了まで待機する。
非同期処理を同期する為のNSRunLoopの使い方はココ参照。

    NSThread *foregroundThead = [NSThread currentThread];
    NSObject *dummy = [[NSObject new] autorelease];
    [session exportAsynchronouslyWithCompletionHandler:^{
        [dummy performSelector:@selector(self) onThread:foregroundThead withObject:nil waitUntilDone:NO];
    }];
    while ([session status] == AVAssetExportSessionStatusExporting) {
        [[NSRunLoop currentRunLoop] acceptInputForMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"waiting... progress:%f", [session progress]);
    }
    if (session.error) {
        NSLog(@"error: %@", session.error);
    }
    ....

exportAsynchronouslyWithCompletionHandlerに終了時に実行するBlockを渡せるので、そこから表のスレッドで何か実行する。ほんとに何でも良いので今回はダミーのNSObjectを作って-selfを呼んでいる。
ちなみにAVAssetExportSessionは既存のファイルを上書き出来ないみたいなので、再実行するときは出来たファイルを毎回消さないとエラーになる。


これで出力したファイルをiTunesなどで見ると……ID3タグが全く含まれていない。MP3の出力には対応していないと考えた方が良そう。

*1:_keyAsStringってメソッド見えてるけど……