BigStopWatch HD v1.0.0 公開されました

BigStopWatchのiPad対応&ちょっと機能アップ版「BigStopWatch HD」がリリースされました。今回は有料になってます。ダウンロードはこちらから。

bswhdpadl.png

一番大きな変更点は、LAP/SPLITをリストで表示するところです。iPadの大画面なら、やっぱりリスト表示で見れたほうがいいなぁと。で、そのLAP/SPLITリストが履歴で残って後から見れます。iTunesのファイル共有機能を使えばCSVファイルが抜き出せます。

そのかわり画面全体がスタート・ストップボタンではなくなり、機能ごとに画面が区分けされたボタンになっています。あと設定は全く無くしまして、カウントダウンの秒数は時間表示部分をドラッグして変更出来ます。

基本的には単純にiPad版を作ろうと思って作っていたのですが、なんとなくiPhone版で同じ機能を使えるようにしておきたいなぁと思いまして、Universalになっています。iPhone版は小さい画面に詰め込んだような感じになっていますので、スクリーンショットを見てイマイチだなぁと思う方は、無料版のBigStopWatchを引き続きお使いください。機能的な違いはラップの履歴が残るだけです。

ちょっと開発者的な話になりますが、今回のHD版もフルOpenGLです。ラップリストのテーブルビューもどきも自分で作ってます。

といっても、ただ同じものという訳ではなく、テーブルの一番下まで移動するボタンをフッター部分に付けてあります。いつもiPhone使ってて一番ストレスたまるのが、Safariとかで縦に長いページを延々と下までスクロールしていかなくちゃいけないってところだったので。一番上に戻るのはステータスバーをタップで出来ますが。

あと、Universalにして改めて感じたのは、iPadよりもRetina iPhoneの方がメモリ的にもパフォーマンス的にも厳しいってことですね。

BigStopWatch v2.4.2 公開されました

BigStopWatchのv2.4.2が公開されました。ダウンロードはこちらから。

端末によりコンマ以下2桁の数字の部分に焼き付きが起きてしまう現象があったので修正しました。

おそらくほとんどのiPhoneやiPod touchでは起きない現象だと思いますが、AppStoreのレビューでユーザーさんからの指摘がありましたので修正版を出しました。ちなみに、逆にiPadではこの現象が起きる端末が結構多いと思います。現在製作中のiPad版を作り始めたときから悩まされていた現象です。

どうやらこのアプリのように、違う画像を60fpsで1フレームごとにチカチカと切り替えて表示させていると、うっすらと焼き付いたように画像が残ってしまうようです。メインの時間表示だけ30fpsに落として対処しています。

Touch the Wave 2 v1.2リリース

Touch the Wave 2のv1.2がリリースされました。ダウンロードはこちらから。

今回は、ほぼ単純にバグフィックスです。前バージョンをiOS4.2で動作させた場合、波形のスクロール方向が逆になったり、iPodライブラリから変換の必要の無い曲をインポートしようとすると失敗してしまうという不具合が出ていましたので修正しました。もし、そのような不具合が出るようでしたらアップデートをお願いします。

ただ、変換の必要の無い曲をインポートしようとすると失敗することから、iPodライブラリからの曲の読み込みは必ず時間がかかるようになってしまいました。この辺はご了承ください。

プログラミング的なことを書きますと、前バージョンはAVAssetExportSessionを使って読み込んでいたのを、AVAssetReaderに変えました。あまり深追いはしていないのですが、どうやら以前の記事「iPodライブラリからのファイル書き出し その2」で書いていたテクニックが使えなくなってしまったようです。もし参考にされている方がいらっしゃいましたらお気をつけ下さい。

そんな感じで、とりあえず今回はバグフィックスなんですが、現在、別バージョンのTouch the Waveを構想中です。

今のバージョンは、iPodライブラリからの読み込み機能が無い時点で開発したものなので、一旦オーディオファイルをアプリ内にコピーして再生するというなっていますが、今度はライブラリから選んだらわりとすぐ再生出来るようにしようかと、ちょこちょこ研究しています。

そうなるとアプリの構造をいろいろ変えたいので、Touch the Wave 2とは別アプリとして出そうかなと思っています。Touch the Waveシリーズ3本目ですね。完成したら今よりかなりシンプルなものになるんじゃないかと思います。

あと、BigStopWatchのiPad版も絶賛テスト中ですので、近いうちにリリース出来ると思います。

METRONOME STAR v1.0.3 公開されました

METRONOME STAR 1.0.3が公開されました。ダウンロードはこちらから

前回のアップデートで、バックグラウンドからの復帰時に再生できなくなることがあった不具合を修正しました。

プログラミング的な対処法を書いておくと、前バージョンはアプリがアクティブになった時だけAudioSessionを設定していたのですが、新バージョンでは音声を再生するときにもAudioSessionを設定するようにしました。アプリがバックグラウンドで深く眠ってしまっていたりすると、BecomeActiveでAudioSessionを奪い取ろうとしてもダメな時があるようです。これで完全に問題が修正できていれば良いのですが…。

METRONOME STAR v1.0.2 公開されました

METRONOME STARのv1.0.2が、公開されました。

変更点は、
・再生停止中に画面をロックした時にAudioUnitを止めて、バッテリーの消費を軽減
・アプリがバックグラウンドに入ったときに、画面表示系のメモリを解放。
・色や画像の調整。
・起動時のアニメーションを追加
といったところです。

ただ、バックグラウンド時のメモリ解放によって、アプリが完全に終了されてしまうことが減ったのですが、バックグラウンドから復帰したときに再生できなくなることがたまにあります。ただ、その場合でも、もう一度アプリを起動し直すと再生できるようです。再生できない場合は、申し訳ありませんがアプリの再起動で対応をお願いします。

起動時のタイトルのフェードアウトは、まあ、起動画像からのアニメーションを試してみたかっただけです。今回のアップデートはメモリ解放やバッテリー消費の改善がメインだったのですが、なにかわかりやすい変更もしておきたいなぁとおもってちょっとやってみました。

CMSampleBufferからオーディオデータを取り出す

前回の続きというほどのものでもない内容になってしまいますが、CMSampleBufferからオーディオのデータを取り出す方法です。CMSampleBufferGetAudioBufferListWithRetainedBlockBufferという関数を使います。前回の内容も含めてDeveloper Forumsに書いてあったコードを参考に調べていたのですが、削るに削ってこれだけになりました。

CMSampleBufferRef sampleBuffer = [audioMixOutput copyNextSampleBuffer];
CMBlockBufferRef blockBuffer;
AudioBufferList audioBufferList;
        
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
                                                        NULL,
                                                        &audioBufferList,
                                                        sizeof(audioBufferList),
                                                        NULL,
                                                        NULL,
                                                        0,
                                                        &blockBuffer);
//
//    ここでAudioBufferListから読み出す処理など
//
CFRelease(sampleBuffer);
CFRelease(blockBuffer);

とりあえずInterleavedなオーディオデータが読み出せればよいのであれば、上記のコードのようにAudioBufferListを作って、その中のデータを保持するCMBlockBufferをこの関数で取得するというだけで良いようです。引数がたくさんあって他にもいろいろ取得できるようですが、結構みんなNULLであっさり通ったので、あえてそれらを使って何かするのでなければいらないんじゃないでしょうか。

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

Touch the Wave 2 v1.1.1公開されました

Touch the Wave 2のv1.1.1が公開されました。

今回のアップデートは、プレイヤー画面で波形をタッチしたまま「×」ボタンや「↓」ボタンを押すと、波形へのタッチが無効になってしまう問題を修正しました。

アップデートついでに、ファイル共有を有効にしてみました。ただ、ファイル共有で直接取り込んだ場合、ファイルフォーマットをTouch the Wave用に変換する処理を入れていないので、WAV・16ビット・44.1kHz・ステレオでないとアプリが受け付けません。その他のフォーマットのファイルを入れた場合は無視されるか削除されます。まぁ、iPadでも簡単にファイルが取り込めた方がいいかなぁと思っての気まぐれ追加機能なので、iPadがiOS4対応になったら無効にすると思います。

iPodライブラリからのファイル書き出し その2

※この記事の内容は、iOS4.2以降では正常に動作しない可能性があります。

iPodライブラリからの書き出し、第二回です。

前回、MP3からAACへの書き出しができないと書きましたが、おそらくAVAssetExportSessionがサポートしているファイルタイプにMP3がないからと思われます。supportedFileTypesメソッドで取得できるAVAssetExportSessionがサポートしているとおぼしきファイルタイプは以下のようなものです。

"com.apple.quicktime-movie",
"com.apple.m4a-audio",
"public.mpeg-4",
"com.apple.m4v-video",
"public.3gpp",
"org.3gpp.adaptive-multi-rate-audio",
"com.microsoft.waveform-audio",
"public.aiff-audio",
"public.aifc-audio"

オーディオで対応しているのは、「m4a」「3gpp」「wave」「aiff」「aifc」といったところのようです。

MP3の書き出しの他にも、WAVやAIFFの非圧縮ファイルをそのまま使いたい場合にも、ちょっとめんどくさい方法を使わないといけないようです。僕があれこれ試して成功した方法をとりあえず記録しておきます。もし他にスマートな方法がありましたらご指摘いただけるとうれしいです。

とりあえずコードです。前回のmediaPicker:didPickMediaItems:メソッドを以下のようにマルッと差し替えてください。あと、CoreMedia.Frameworkもインポートしてください。

- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
{
    MPMediaItem *item = [mediaItemCollection.items lastObject];
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:nil];
    
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]
                                           initWithAsset:urlAsset
                                           presetName:AVAssetExportPresetPassthrough];
    
    
    NSArray *tracks = [urlAsset tracksWithMediaType:AVMediaTypeAudio];
    AVAssetTrack *track = [tracks objectAtIndex:0];
    id desc = [track.formatDescriptions objectAtIndex:0];
    const AudioStreamBasicDescription *audioDesc = CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)desc);
    FourCharCode formatID = audioDesc->mFormatID;
    
    NSString *fileType = nil;
    NSString *ex = nil;
    
    switch (formatID) {
            
        case kAudioFormatLinearPCM:
        {
            UInt32 flags = audioDesc->mFormatFlags;
            if (flags & kAudioFormatFlagIsBigEndian) {
                fileType = @"public.aiff-audio";
                ex = @"aif";
            } else {
                fileType = @"com.microsoft.waveform-audio";
                ex = @"wav";
            }
        }
            break;
            
        case kAudioFormatMPEGLayer3:
            fileType = @"com.apple.quicktime-movie";
            ex = @"mp3";
            break;
            
        case kAudioFormatMPEG4AAC:
            fileType = @"com.apple.m4a-audio";
            ex = @"m4a";
            break;
            
        case kAudioFormatAppleLossless:
            fileType = @"com.apple.m4a-audio";
            ex = @"m4a";
            break;
            
        default:
            break;
    }
    
    exportSession.outputFileType = fileType;
    
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *filePath = [[docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]] stringByAppendingPathExtension:ex];
    exportSession.outputURL = [NSURL fileURLWithPath:filePath];
    
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            NSLog(@"export session completed");
        } else {
            NSLog(@"export session error");
        }
        
        [exportSession release];
    }];
}

では、ちょっと解説していきます。

AVAssetExportSessionのpresetNameは前回のM4AではなくAVAssetExportPresetPassthroughにしています。これはiPodライブラリから変換をせずにコピーをするpresetのようです。ただし、この場合、outputFileTypeに指定すべきファイルタイプをM4Aの時のように絞ってくれませんので、自分で調べる必要があります。

まず、AVURLAssetが持っているオーディオのAVAssetTrackを取得。さらにAVAssetTrackがもっているformatDescriptionsを取得。formatDescriptionsはNSArrayなのですが、その中に入ってるのはObjective-Cのオブジェクトではなくて、CMAudioFormatDescriptionRefというCarbon?なオブジェクトが入っています。このCMAudioFormatDescriptionRefからCMAudioFormatDescriptionGetStreamBasicDescription()という関数を使って、みなさんおなじみのAudioStreamBasicDescriptionが取得できますので、これを見て判断しています。

wavやaiffのLinearPCMなファイルはどちらもFileIDがkAudioFormatLinearPCMですので、エンディアンで違いを判別しています。

MP3はあれこれ試した結果、FileTypeを@”com.apple.quicktime-movie”にして、ファイル名に拡張子「.mp3」をつけておくと、ExtAudioFileで開くことができました。この挙動は試してみたらできちゃった的な感じなので、なにか正当な方法があるといいなと思っているのですが…。ちなみに、もしかしたらMP3以外もQuickTimeで共通でいけるんじゃないかと思ったのですが、MP3以外でもデータはコピーされるものの、ExtAudioFileでそのままOpenというわけにはいかないようです。

あと、何かが取得できなかった時とかのエラー処理とかはいっさいやってませんのであしからずご了承ください。

Touch the Wave 2 v1.1リリース

Touch the Wave 2のv1.1がリリースされました。ダウンロードはこちら

変更点は、念願のiPodライブラリからの読み込みに対応。あと、波形やタブの画像などを高解像度に対応させました。iPodライブラリから読み込めるようになったことで、だれでも使えるアプリになったんじゃないかなと思います。

ちなみに、波形表示はいままでテクスチャに画像を描いて拡大縮小していたのですが、テクスチャはやめました。おかげで画像作成時間のロスが無くなったり、拡大して表示が荒くなったのが無くなったりしてます。パフォーマンスに関してはiPhone 3Gじゃないとわかんないくらいの違いですけど、なぜか前はテクスチャの方が速いと思い込んでたんですよねぇ、なんでだろう。