BigStopWatch v2.4.1 リリース

BigStopWatchのv2.4.1が公開されました。

一つ前のv2.4でRetina対応にしたためiOS4.0以降でしかインストールできないようにしていたのですが、iPhone 2GなどiOS4.0にアップグレードできないデバイスを使っている方から3.1.3にも対応してほしいという要望があったので修正しました。他に変更点はありません。

OpenGL ESのRetina対応

OpenGL ESのRetina対応の方法をちょっとメモしておきます。OpenGL以外のRetina対応については@k_katsumiさんの記事「アプリケーションを iPhone 4 の Retina Display に対応するための方法いろいろ」がとても参考になると思います。というか、この記事を見てOpenGLについては書いてなかったので触発されて書くことにしました。

Xcodeの「OpenGL ES Application」で作成したプロジェクトの場合で書きますと、EAGLViewのViewのサイズはRetinaディスプレイでも変わらず320×480です。iOS4からUIView(およびEAGLViewなどのサブクラス)にcontentScaleFactorというメソッドが追加され、この値を変更することでUIViewの中の解像度を変更することができるようになりました。もうひとつ、UIViewがもってるCALayerのcontentsScaleでも解像度を変えることができるようです。デフォルトでは1.0になっていますので、Retinaディスプレイの解像度に合わせるには2.0にします。実際には、UIScreenのscaleからディスプレイのサイズの比率が取得できますので、そこからセットします。

ちょっとやってみましょう。AppDelegateのapplication:didFinishLaunchingWithOptions:メソッドに次のコードを書き加えてください。

glView.contentScaleFactor = [UIScreen mainScreen].scale;

基本、これだけでRetinaディスプレイのiPhone4だと倍の解像度になるのですが、「OpenGL ES Application」のデフォルト状態の描画内容だと効果が全然わからないので、さらにコードを変えてみます。

まず、シェーダーとか使ってるとめんどくさいのでES2Rendererを使わないようにします。以下のようにEAGLViewのinitWithCoderの中の一行を書き換えてください。

renderer = [[ES2Renderer alloc] init];
↓
renderer = nil;

次に、ES1Rendererのrenderメソッドの中の縦に動かしている一行を、高解像度の効果がわかりやすいように回転に変更します。

glTranslatef(0.0f, (GLfloat)(sinf(transY)/2.0f), 0.0f);
↓
glRotatef(transY, 0, 0, 1.0);

コードの変更は以上です。一番最初にAppDelegateに追加したglView.contentScaleFactorの一行のありなしでそれぞれiPhone 4で実行してみると四角形のエッジのスムーズさの違いがわかると思います。iPhone 4実機をもっていなくても、iPhone simulatorのデバイスをiPhone 4にすることで確認できます。

OpenGL的には単純に解像度が倍になりますので、ディスプレイのサイズに依存してアプリを作っていたら結構な修正を余儀なくされると思います。場所がずれたりとか、テクスチャも荒く表示されちゃったりとかすると思います。さらにやっかいなのが、UITouchの位置は2倍にならないってことです。scaleに応じて位置を調整する必要があります。

ちなみに、EAGLViewのcontentScaleFactorをセットするだけで解像度が変わっているのではなく、scaleをセットしてからESRendererのresizeFromLayerメソッドの中の[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]が呼ばれて始めてGLの解像度が変わります。もし、glView生成時でなく、後からscaleを変更したいなんていう場合には、contentScaleFactorをセットしたあとにresizeFromLayerを呼んでください。

あと今、iOS4より前のバージョンもサポートするなら、iPadのiPhoneシミュレータで動くことも考えないといけなそうです。iPadでiPhoneアプリを2倍に表示するとUIScreenのscaleが2.0になってしまいますが、UIViewにはcontentScaleFactorがありません。また、CALayerのcontentsScaleはiPadでも受け付けることができますが、GLの解像度が変わることはありません。プロパティの有無やOSのバージョンなどでscaleを無視する必要がありそうです。

BigStopWatch v2.4公開されました

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

主な変更点はRetina対応で、iPhone 4をお持ちの方はより美しく表示されます。今までもアンチエイリアスがかかるようにして文字や線の美しさにはこだわっていたのですが、Retina対応することで一段クオリティの高い美しさになってます。iPhone 4をお持ちの方はぜひアップデートをお願いします。

あとはバッテリーインジケータを標準のものに近い形に変えてたり、タッチしたときのアニメーションを変えたりくらいですね。カウントダウンの時に自分的に意図していない表示になっていたところとかも修正したりしてます。

AVPlayerでムービーを再生する方法

AVFoundationといえばOS3.xではAVAudioPlayerとAVAudioRecorderくらいだったのですが、iOS4.0から一気にいろんなものが増えてます。僕が以前予想していたように、オーディオだけじゃなくて映像関係も扱えるように進化していますね。なんかいろいろありすぎて、かつ自分がAudioUnitでやりたいことを置き換えるようなものじゃなさそうなので基本的にはスルーしているのですが、一番手軽で役に立ちそうだったけどちょっと使い方にハマったAVPlayerを取り上げたいと思います。

AVAudioPlayerはサウンドファイルを再生するだけでしたが、AVPlayerはサウンドもムービーも関係なくこれひとつで再生できます。サウンドはファイルパス渡してプレイってするだけでなんとなく鳴ってしまうのであえて説明する必要はないと思いますが、ちょっと使い方を知らないと厄介なのがムービーの再生です。

僕がハマってしまったのは、AppleのAVPlayerLayerのリファレンスに嘘が書いてあるところです。いや嘘というよりも、OS4.0のベータが進んでいくうちに使い方が変わってしまっているのにリファレンスがそのまま残っているというところです。

以下が、AVPlayerLayerのリファレンスに書いてあるコードです。

AVPlayer *player = <#A configured AVPlayer object#>;
CALayer *superlayer = <#Get a CALayer#>;
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[superlayer addSublayer:playerLayer];

どっかのCALayerのサブレイヤーにAVPlayerLayerをのせろと書いてあります。ですが、この通りにやっても正常にムービーは映し出されないようです。一瞬だけ映像が見えることもありますが、音だけが鳴り、映像が再生されることはありません。最初この状態になったときには、きっとGMの頃には再生できるようになるだろうとおもっていたのですが、GMになっても再生できず、じゃあ別のやり方に変わったんだろうと妄想を膨らませて解決しました。

ではどうするかというと、UIViewのサブクラスを作り、そのlayerClassメソッドをオーバーライドして、UIViewのlayerをAVPlayerLayerに差し替えます。OpenGLのEAGLViewでやっているのと同じ手法です。

さっそく、その方法でムービーを再生するサンプルを作ってみたいと思います。まずは、XcodeでWindow-based Applicationを「AVPlayerTest」とかの名前で作成して、AVFoundationをインポートしてください。

次に、UIViewのサブクラスを作成してください。名前は「AVPlayerView」としておきます。コードを以下のような感じで書き換えてください。

//
//   AVPlayerView.h
//
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface AVPlayerView : UIView {
}
@end
//
//   AVPlayerView.m
//
#import "AVPlayerView.h"
@implementation AVPlayerView
+ (Class)layerClass
{
    return [AVPlayerLayer class];
}
@end

AVPlayerViewを書き換えたら、今度はAVPlayerTestAppDelegateのapplication:didFinishLaunchingWithOptions:メソッドにもろもろセットアップする処理を記述します。以下のようなコードをAppDelegateに書き足してください。

//
//   AVPlayerTestAppDelegate.m
//
#import "AVPlayerView.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    [UIApplication sharedApplication].statusBarHidden = YES;
    
    CGRect viewFrame = CGRectMake(0, 0, window.frame.size.height, window.frame.size.width);
    AVPlayerView *view = [[AVPlayerView alloc] initWithFrame:viewFrame];
    view.center = CGPointMake(window.frame.size.width / 2.0, window.frame.size.height / 2.0);
    view.transform = CGAffineTransformMakeRotation(M_PI_2);
    [window addSubview:view];
    [view release];
    
    // ↓m4vのところを再生したいムービーの拡張子に変更してください
    NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"m4v" inDirectory:nil];
    NSURL *url = [NSURL fileURLWithPath:[paths objectAtIndex:0]];
    AVPlayer *player = [AVPlayer playerWithURL:url];
    
    AVPlayerLayer *playerLayer = (AVPlayerLayer *)view.layer;
    playerLayer.player = player;
    
    [player play];
    [window makeKeyAndVisible];
    
    return YES;
}

コードはこんなところでしょうか。あとはバンドルになにかひとつムービーを入れてアプリを起動させればムービーが再生されると思います。上のコードではムービーの拡張子をm4vとしているので、違う拡張子のムービーであれば変更してください。

あと、AVPlayerを使ってみて気になったのが、ムービーの元のサイズとかの情報が、AVPlayerを生成したときには取得できなさそうだということです。どうもAVPlayerの中での初期化処理がバックグラウンドで行われているような感じで、それが終わってAVPlayerのstatusがReadyToPlayになってはじめて情報が取得できるという感じです。それなのに、その初期化終わった通知とかが無さそうなのが、いまいちわからないところです。

iOS4.0のオーディオ

iOS4.0がリリースされましたので、個人的に気になった変更点などをまとめておきたいと思います。大々的に告知されていたような目玉機能の羅列ではなく、主にオーディオ周りで気になったところです。

バックグラウンドオーディオ

これはiOS4.0のオーディオ系では一番の改良点と思われます。自分のアプリがバックグラウンドに入っても音を流し続けられることができます。対応しているのはたしかiPhone 3GSとiPod touch第3世代とiPhone 4あたりですね。マルチタスク非対応のデバイスでは残念ながら使うことはできません。

とりあえずバックグラウンドで流せればいいのであれば設定は簡単です。info.plistに「Required background modes」という項目を作って、そこで選べるモードの中から「App plays audio」を選択するだけです。これでバックグラウンドに入ってもAudioUnitなどが止まらずに動き続けてくれます。

appplaysaudio.jpg

しかし、自分のアプリが音を流しっぱなしにできるということは、iPodアプリだけでなく、他のいろいろなアプリも同じようにしてくるということです。iOS4デバイスの中でオーディオの主導権を握る仁義なき戦いが繰り広げられることになります。

そこでやっぱり重要なのがカテゴリの設定です。自分のアプリから音を出すときに、他のアプリの音をどうするか。逆に自分のアプリをバックグラウンドで流すときにどうするのか。OS3.xの時と比べて結構シビアになってくると思います。ガッツリ音を鳴らすようなアプリでもないのに他のアプリの音を消してしまったりしたら、ユーザーさんからクレームが来るかもしれません。

あとバッテリーのことを考えると、アプリがバックグラウンドに入るときに継続して音が鳴らないタイミングであれば、AudioUnitを止めておくことも必要と思われます。まぁ、これはバックグラウンドじゃなくてもそうかもしれませんが、再生するときのレスポンスとかもありますし。

また、オーディオに限りませんが、マルチタスキング対応にしていると基本的にずっとアプリが起動しているような状態になっていますので、ユーザデフォルトが設定アプリから変更されたり、File Sharingでドキュメントフォルダの内容が変更されたりということもあります。自分のアプリがアクティブになったらデータを更新するということも忘れないようにしないといけないでしょう。

オーディオカテゴリが厳格になった?

もしかするとiOS4.0からではないのかもしれませんが、オーディオ関連の挙動がオーディオカテゴリの設定にかなり厳格になっているようです。

たしかOS3.0とかの頃に試していたときにはカテゴリはあくまでルーティングが変わるというような影響でしかなかったのですが、iOS4.0を試していたところ、カテゴリをRecordにするとAudioUnitでアウトプットを生成できなかったり、MediaPlaybackにするとインプットを生成できなかったりします。イレギュラーな事にはカテゴリのオーバーライドで対応しろということでしょうかね。

Accelerate Framework

iOSにもいよいよ念願のAccelerate Frameworkが実装されました。

といっても、MacのAccelerate Frameworkそのままではなく、vecLibのみ。その中でも限定されていて、vDSPとcblasとclapackだけが使えるようです。FFTは使えますし、deq22もあるのでEQも楽に作れそうです。でも、vForceあたり(サイン・コサインとか)は全滅なのが、ちょっと残念。

とりあえずこんなところです。まだ全然チェックしてませんが、iPhone Dev CenterのサンプルコードもどっさりiOS4.0に対応しているみたいです。オーディオ以外や、ちゃんとまとめておきたいことでまた何かエントリを書くかもしれません。

※以下追記 2010/8/1

iPodライブラリからのファイル書き出しが可能に!

この記事を書いたときに気づいてなかったのですが、iPodライブラリから生のデータとして扱えるファイルに書き出すことができるようになりました。くわしい方法はこちらのエントリへどうぞ。

METRONOME STAR 1.0.1 公開しました

METRONOME STAR v1.0.1がリリースされました。ダウンロードはこちら

変更点はiOS 4.0対応ということで、マルチタスキング対応デバイスであれば、アプリを終了しても音が鳴り続けるようになりました。iPhone 3Gなどの非対応デバイスでは、おそらく何も変わってません。むしろよけいな処理が増えている分、パフォーマンスが落ちているかも…。あと、高解像度にも対応して、iPhone 4だとグラフィックがたぶんきれいになっているはずです。iPhone4実機の本予約がまだ確定していないので、その美しさを自分で体験するのがいつになるのがわからないのがもどかしいですね。iPhoneシミュレータだとすごく動きがカクカクしててパフォーマンスも不安ですし。

まあMETRONOME STARは正直ダウンロード数が少ないので万が一何かあっても被害が少ないかな、と。あと、グラフィックもサウンドもそれなりに使ってるアプリなので、新しい環境で試すにはちょうどいいアプリだというのもありました。何より、新OSと同時にリリースしてみたかったんですよね。(iOS4にはちょっと遅れましたが、iPhone 4には間に合った)

iOS4.0に対応したといっても、新機能を使ってみたという感じでバックグラウンドの作法は全く考えておらず、バックグラウンドに入る前に不必要なメモリを解放するとかやってないので、もしかするとシステムに迷惑なアプリになっているかもしれません。再生していないときのAudioUnitを止めたりとかはしてるくらいですかね。このへんは今後の課題かなぁと思っています。

あと、ほんとうにメトロノームをバックグラウンドでならす必要があるのかということも頭をよぎりましたが、逆にいろんなアプリがバックグラウンドオーディオ対応して、iPhoneユーザさんたちがどう感じるかというのを様子見するのがよいのかなと思いました。

ちなみに、BigStopWatchのiPhone 4高解像度対応バージョンというのも出来上がってはいますが、保留しています。こちらはいままでのダウンロード数が多いので、慎重に実機で試してからリリースしようと思っています。

離散フーリエ変換 その3

前回記事を書いてからだいぶ時間が経ってしまって5月にエントリがひとつもないのもさみしいので、書きかけだった記事をアップしておきます。iPadから書き込んでみたいというのもあったので…。

最近はiPhoneOS4.0にどっぷり使っていまして、あまり書けることがないんですよねぇ。ってことで、フーリエ変換の流れで今回は位相の話です。逆離散フーリエ変換とか行きたいところですが、またの機会にします。

位相とは

これまでの説明で何度か位相という言葉を使ってきたと思いますが、ちゃんとその定義を調べずに使っていたので、改めてwikipediaなどを見てみますと…

位相 (Wikipedia)

「ひとつの周期中の位置を示す無次元量」なんて書いてあって、無次元量って何だ?なんて思ってしまうわけですが、まぁ、DFTで使っているサイン波でいえば、サイン波の中のどの位置かということと思われます。さらに、周期のスタート位置の位相は「初期位相」というそうで、初期位相の事を単純に位相といわれたりすることもある、だそうです。「位相が○度ずれている」と言ったときには、2つの同じ周波数のサイン波を同じ時間軸にならべた状態で「初期位相が○度ずれている」という理解になるかと思います。

サイン関数を使ってサイン波を作る場合などは位相をラジアンで渡して値を取得しますが、0〜2π(角度で表すなら0°〜360°)が一周期で、2π以上や0以下の値を渡しても、返ってくるのは同じ値の繰り返しとなります。たとえば、0から始まるサイン波と、2πや4πや-2πから始まるサイン波というのは、全く同じ形となりますので、同じ位相といえます(たぶん)。DFTの周波数成分は、繰り返されているサイン波の1周期ですので、その位相はどこか2π分の範囲の中の位置だけを考えれば良い事になります。

DFT03_01.jpg

直線位相特性

よくデジタルフィルタの本をみていると、FIRフィルタだと位相のずれがない直線位相特性のフィルタが実現できるなんて書いてあったりします。下の図が直線位相特性のグラフなのですが…

DFT03_03.jpg

これを最初見た時は位相がずれないといってるのに、位相が周波数によってずれるというのが僕はよく理解できなかったのですが、いくつかのサイン波を並べて同じ時間遅らせて、遅らせたタイミングでも同じ波形になるようにしてみるとわかります。

DFT03_02.jpg

上の図では1Hzのサイン波を3/4周期遅らせて、同じ時間2Hzと3Hz遅らせて並べてみています。元のサイン波が緑色で、遅らせたサイン波が青色です。各周波数を同じ時間遅らせているので、遅らせたあとの波形はどの周波数も元の波形と同じ形に保たれます。

1Hzの3/4周期の遅れに対して、2Hzは3/2周期、3Hzは9/4周期、と位相がずれています。このような感じで周波数に比例して位相がずれるというのが直線位相特性です。直線位相特性を実現したフィルタを使用すれば、周波数によって位相のずれがないので、クオリティの高い処理ができるということのようです。

METRONOME STAR v1.0 公開しました

先月末に予告していたメトロノームアプリがリリースされました。

ダウンロードはこちらから。(iTunesへ飛びます)

まあ、機能的にはたいした事はできませんが、星が揺れているさまをみてなごんでいただければうれしいです。無料ですのでお気軽にどうぞ。

プログラミング的な所をいいますと、サンプル単位のシーケンス+ホストタイムにスピードを合わせているので、iPhoneアプリにおいては結構リズムが正確なんじゃないかと思います。逆に言えば、オーディオデバイスのクロックからはタイミングがずれているので、同じテンポのiPodのミュージックと一緒に鳴らしてもぴったり同じタイミングには鳴りません。

離散フーリエ変換 その2

離散フーリエ変換の第2回目という事で、実際にどういうことをやっているのかをコードで見ていきたいと思います。前回のDFTのコードの変換を行っているところが以下の部分です。

// iは抜き出す周波数
for (int i = 0; i < n; i++) {
    
    tmpReal[i] = 0.0;
    tmpImag[i] = 0.0;
    
    // 1サンプルの位相の差分
    double d = 2.0 * M_PI * i / n;
    
    for (int j = 0; j < n; j++) {
        
        double phase = d * j;
        
        // コサインをかけた結果を加算する
        tmpReal[i] += real[j] * cos(phase);
        // マイナスサインをかけた結果を加算する
        tmpImag[i] -= real[j] * sin(phase);
    }
}

変換するオーディオデータに対して、抜き出す周波数のcosをかけ算して全て足し合わせた値が実部、-sinをかけて足し合わせた値が虚部になります。具体的に説明はしませんが、このようにする事で、それぞれの周波数のcosやsinがオーディオデータにどれくらい含まれているかを得る事ができます。(ちゃんと理解したい方は、なにかしらフーリエ変換の本などを参考にしてください)

抜き出す周波数というのは、0Hzから始まって1Hz、2Hz...と続いて、変換するサンプル数-1Hzまでです。つまり、変換するサンプル数と同じ数の周波数成分に分けられます。DFTは、変換するオーディオデータが延々とループしていると仮定して周波数成分を取り出しますので、1Hz以上の周波数はぴったり整数の周波数になります。

DFT02ri.jpg

上の図を見ると、cosもsinも0Hzの時は横一直線になっています。0HzはDC成分といわれたりしてちょっと特別です。cos0Hzでは全体に1がかけられますので、波形全体がプラスとマイナスのどちらにバランスが偏っているかという要素になります。sinは0をかけているので、元がどんな波形だろうと結果は0になります。

ちなみに、下の図の1.23Hzみたいな中途半端な周波数のサイン波などというのは途中で急激に途切れた状態で繰り返されることになりますので、DFTをすると1Hzを中心に高い周波数まで全体的に成分が含まれることがわかります。

DFT02s123.jpg

また、ちょっと説明が後回しになりましたが、なぜDFTでcosとsinを抜き出すかといえば、同じ周波数のcosとsinを足し合わせることで、360度どの位相のサイン波でも表現できるからです。

本当にcosとsinの足し算でサイン波が作れるのか、検証してみたのが以下のコードです。stRadを0〜2πの間で変えてみて実行してみてください。sinはDFTと同じようにマイナスにしています。もちろん計算の誤差はあると思いますので、近ければOKという基準で判断すれば、どの位相のサイン波でも、スタート位置がゼロのcosとsinの足し合わせで作り出せる事がわかると思います。(てきとうに書いたので、なんかおかしかったらすみません。0〜2πの範囲外では正確な結果が出るようになってません)いちおうDFTと同じように、sinはマイナスsinでやってます。

#include <stdio.h>
#include <math.h>
int main (int argc, const char * argv[]) {
    
    double stRad = 0.0; //sinの開始位置。0〜2πの間で!
    int n = 16;
    
    double xcos = 0;
    double xsin = 0;
    double co = fabs(sin(stRad));
    double si = sqrt(1.0 - co * co);
    
    if (stRad < M_PI_2) {
        //第一象限
        xcos = co;
        xsin = -si;
    } else if (stRad < M_PI) {
        //第二象限
        xcos = co;
        xsin = si;
    } else if (stRad < M_PI_2 * 3) {
        //第三象限
        xcos = -co;
        xsin = si;
    } else {
        //第四象限
        xcos = -co;
        xsin = -si;
    }
    
    for (int i = 0; i < n; i++) {
        
        double phase = (double)i / n * 2.0 * M_PI;
        double sinVal = sin(stRad + phase);
        double mixVal = cos(phase) * xcos - sin(phase) * xsin;
        
        printf("sin %03d %f / sin+cos %f\n", i, sinVal, mixVal);
        if (fabs(sinVal - mixVal) > 0.000001) {
            printf("error\n");
            return 0;
        }
    }
    
    printf("success\n");
    
    return 0;
}