投稿者「yuki」のアーカイブ

再提出

Touch the Waveのバージョン0.5をApp Storeに再提出しました。

アラートつけるだけだったら一瞬で終わる変更だったんですけど、以前からiPod touch第2世代でまれに再生が止まるという不具合があって、解決するのに時間がかかってしまいました。

どうやら原因は、CALayerだったようです。波形の画像を表示するのにCALayerのcontentsにCGImageを渡して、position.xで動かすってことをやっていたのですが、このposition.xの頻度を上げていくと再生(というかアプリ全体の動作)が止まってしまう確率が高くなるという感じでした。自分のコードが根本的に悪かったという可能性もありますが、第1世代だとまったく出ないんですよねぇ。

解決策として、波形の表示をマルッとOpenGLに差し替えてしまいました。OpenGLはまだ研究途中だったのですが、AppleのサンプルソースのaurioというのがOpenGL ESを使っていたので参考にさせてもらいました。コピペコピペでなんとか取り繕ってます。OpenGLをちゃんと習得したら、プレイヤー画面全体をOpenGLで描画したいです。

aurioといえば、App Storeにaurioの画面表示部分だけを書き換えただけのようなマイク入力ビジュアライザーアプリをちらほらと見かけますねぇ。

初Rejected

Touch the Waveのアップデートを提出していたのですが、今朝リジェクトされた旨のメールが届いていました。その内容というのは、ネットからコンテンツをダウンロードしないとプレイリストが空の状態のままでユーザーが混乱するからアラートを出せというものでした。もっともな事なんですが、アップデート2回目にして今さら?って感じもします。

関係あるかどうかは分かりませんが、OS2.2ではアプリを削除したときに評価を入れるようなシステムになったらしく、インストールはしたものの何も出来なくてすぐ削除して星1つつけるって人が多かったからかもしれませんね。Touch the Waveの評価はもうガタ落ちです。

まあ、審査されている間にもパフォーマンスとか安定性とかアップしていたりするので、そこらへんも含めて今週中には再提出しようかなぁという感じです。

ともあれ、最近はAppStore開始当初と比べてガイドラインから外れるものは厳しくなっているみたいです。リジェクトされたくない人はガイドラインを熟読しておいた方が良いかもしれません。

iPhone 2.2の誰も教えてくれない新機能

普通の記事には書かれていないけど僕には重要なiPhone 2.2アップデートのポイントをあげてみます。

AVAudioPlayerというお手軽BGM再生APIができた

詳しくは前回のエントリー

アプリ内に大容量のファイルを溜め込んでいてもアップデートインストールに時間がかからなくなった

Touch the Waveにとってはかなりうれしいポイントです。オーディオファイルをアプリ内に溜め込んでいるとアップデート上書きインストールにものすごく時間がかかっていたので、頻繁にアップデートを出すのは良くないかなぁと思っていて控えていたのですが、これで気兼ねなくアップデートを提出する事が出来ます。それになにより、デバッグが非常にらくちん。

NSURLConnectionのリークが無くなったっぽい

たとえばTouch the Waveのサンプルファイルくらいの大きさのファイルのダウンロードを繰り返すと、湯水のようにメモリを消費していくという不具合があったのですが、2.2でチェックしてみたらちゃんと解放されているようです。めでたく予想通り自然に解決しました。

iPod touch第2世代でデバッグが出来るようになった

そうそう、なぜか第2世代でしか出ないバグがあるんですよねぇ。ようやく原因がつかめそうです。

でも、あいかわらずPush Notificationとか入ってないっぽいです。

さっそくAVAudioPlayerを使ってみた

iPhone OSが2.2にアップデートされまして、Frameworkに新しくAVFoundationなんてものが追加されていました。今のところAVAudioPlayerというAPIしか無いようですが、AVと名がついているだけに今後映像がらみでも機能が増えそうな気がします。

そのAVAudioPlayerを早速試してみたところ、お手軽BGM再生APIという感じのようです。オーディオファイルのパスを渡してやるだけでWAV、AIFF、mp3、AAC、ロスレスあたりはいとも簡単に再生できます。これがあればやたらめんどくさいAudio Queue Serviceなんていりません。

ということでとりあえず再生してみるコードです。バンドルにmp3を入れてから実行すると延々とループして再生します。2つ以上あればミックスして再生されます。ちょっとお行儀の悪いコードですので、そのへんは適当に流してください。

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
    [window makeKeyAndVisible];
    
    NSBundle *bundle = [NSBundle mainBundle];
    NSArray *wavePaths = [NSBundle pathsForResourcesOfType:@"mp3" inDirectory:[bundle bundlePath]];
    
    for (NSString *path in wavePaths) {
        NSURL *url = [NSURL fileURLWithPath:path];
        NSError *error = nil;
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        if (!error) {
            audioPlayer.numberOfLoops = -1;
            [audioPlayer play];
        }
    }
}

Touch the Wave 0.5 開発状況

Ver0.5の修正・追加作業はとりあえず終わりまして、ここからはしばらく動作を検証してみようかなという状況です。もしかするとOSの2.2のアップデートが近々あるかもしれないので、一週間ほど待ってみて何も無ければ来週末あたりにコミットしようかと思っています。

前回書いたもの以外の主な修正点は、

・再生中に直接スクラッチ
・ボタンをタッチしたときに枠を表示
・htmlファイル内のリンク先のオーディオファイルを自動で連続ダウンロード
・パーセントエスケープされたファイル名を変換して曲のタイトルに表示

といったところです。

3つ目の修正によって、web共有を使ってダウンロードするのが楽になります。例えば”/Users/username/Sites/music/”というフォルダ内にオーディオファイルを置いておけば、Touch the Waveのダウンロード画面で”http://IPアドレス/~username/music/”と打ち込んでスタートすると、フォルダ内のファイルが連続でダウンロードされるようになります。さらに4つ目のパーセントエスケープの修正によって日本語のファイル名もそのままでダウンロードできるようになりました。でも、ミュージックライブラリにアクセスできればこんな事しなくてもいいんですけどねぇ…。

Touch the Waveの詳細とアップデート予定

アプリの説明に書くと長くなりそうだったので省いていたTouch the Waveの細かい挙動についてと、現在の開発状況について書いておこうと思います。

ダウンロードについて

・ファイル名の拡張子前に、「@bpm」+「テンポ」を書いておくと、スピード0.0%時のテンポが登録されます。小数点以下もちゃんと認識されます。サンプルファイルのように「sample@bpm135.wav」としてあれば、プレイリストのタイトルには「sample」と表示され、テンポは135で登録されます。

・バージョン0.4で、8〜24ビットのAIFFとWAVファイルはそのままダウンロード出来るようになりましたが、アプリ内に保存するときに16ビットの44.1kHzのステレオに変換してしまいますので、クオリティを高くしていても無駄になりますし、低くしていても容量の節約にはなりません。

・同じファイル名のファイルをダウンロードすると上書きされます。

プレイヤーについて

・スライダー上に小さい四角の点で示されている赤の「スピード位置」、青の「キューポイント1」、紫の「キューポイント2」や、音符マークの下にオレンジで示されているスピード0.0%時のテンポは、一曲ごとに保存されます。その他の白丸やボタンの状態はアプリ全体で共通です。

・キューポイント2はキューポイント1の前には設定できません。キューポイント1(青)をキューポイント2(紫)の後ろに設定すると、キューポイント2は曲の最後のフレームへリセットされます。

・キューポイント間に再生位置がある場合はループをオン・オフしてもバッファの読み込みを待たないので、再生中でも読み込みの時間さえ間に合えば音を途切れさせずに再生する事が出来ます。

アップデートについて

次の0.5として公開しようとしている開発中のバージョンで既に改善している点は、

・スクラッチから素早くフリックしたときにレベルが下がりすぎて不自然なので修正
・オーディオデータ読み込み時の無駄を省いてパフォーマンスの向上
・プレイヤー画面とダウンロードで自動ロックがかからないように変更
・プレイリストでカクカクせずスムーズにスクロールするように修正
・±20%のスピード可変
・ファイル名につけたテンポへのリセット機能

といったところです。

オーディオ読み込み高速化の影響は大きくて、iPod touch第一世代(と、おそらくiPhone)でも12fpsであれば早送り巻き戻しの音が途切れなくなりそうです。第二世代だと、30fps近くでのスクラッチやフリックにも強くなります。

あと、これからやろうとしている事は、

・再生中は4倍速早送り巻き戻しをやめて、30%くらいのスピードアップとダウン。停止中はそのまま
・複数のファイルを一度にダウンロード

といったところでしょうか。ダウンロード後に不安定になるのは、まだ改善できる見込みがありません。シミュレータでは起きていないメモリリークが原因のようなので、OSがアップデートされると自然と直るかもしれません。

それと、もしVer 0.5が公開されて、アップデートする際に気をつけていただきたいのは、アプリ内にオーディオデータを溜め込んでいる分だけアップデートに時間がかかるという事です。おそらく500MBで20分くらいかかると思います。時間のあるときにアップデートするか、一旦アプリを削除してインストールし直したほうが良いと思います。

ひととおり気になるところが修正し終わったらしばらく動作を検証したいので、AppStoreへのコミットはまだ先になると思います。

iPod touch 2Gでいまさら発見

なんとなくネットをブラウジングしてたら、iPod touchの第二世代でマイク付きイヤホンを使えるというような記述をいたるところで見かけたので、第一世代と見比べてみたら・・・

touchmic.jpg

ありました。外に近いところにそれらしきポッチが。

iPhoneをまったく買うつもりが無いので録音関係はあきらめていたのですが、なんかいいアイデアが思いついたらマイクを生かしたアプリを作るかもしれません。

RemoteIOでのオーディオ再生

iPhone用のアプリも公開できて一段落つきまして、さらにNDAも緩和されたという事で、iPhoneのオーディオプログラミングネタを書いていきたいと思います。

iPhoneで音を再生する方法はいくつかあります。短いサンプルを再生するならSystemSoundService。BGMで使うような長い曲を再生するならAudioQueueService。レイテンシーを低く再生するならAudioUnit。あと、OpenALを使うという手もあります。

「Touch the Wave」で使っているのはAudioUnitです。SystemSoundは短すぎて役に立ちませんし、AudioQueueではリアルタイムに音を変化させるような事や逆再生が出来ないですし(たぶん)、OpenALはよくわからないって感じでしたので。

何はなくともとりあえずAudioUnitを使うクラスのサンプルコードです。このクラスを生成すればAudioUnitが開始されて、解放すれば停止します。シンプルにするためにエラー処理はまったく記述していません。

※最終修正 2011/10/24

//
//  YKAudioOutput.h
//
#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>
@interface YKAudioOutput : NSObject {
AudioUnit outputUnit;
}
@end
//
//  YKAudioOutput.m
//
#import "YKAudioOutput.h"
@implementation YKAudioOutput
static OSStatus OutputCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData)
{
    OSStatus err = noErr;
    for (NSInteger i = 0; i < ioData->mNumberBuffers; i++) {
        //2009/6/28 OS3.0対応
        SInt16 *ptr = ioData->mBuffers[i].mData;
        for (NSInteger j = 0; j < inNumberFrames; j++) {
            UInt32 channels = ioData->mBuffers[i].mNumberChannels;
            for (NSInteger k = 0; k < channels; k++) {
                ptr[j * channels + k] = sin(M_PI / inNumberFrames * j * 50) * INT16_MAX;
            }
        }
    }
    return err;
}
- (void)setupOutputUnit
{
    AudioComponent component;
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    
    component = AudioComponentFindNext(NULL, &desc);
    AudioComponentInstanceNew(component, &outputUnit);
    AudioUnitInitialize(outputUnit);
    AURenderCallbackStruct callback;
    callback.inputProc = OutputCallback;
    callback.inputProcRefCon = self;
    
    AudioUnitSetProperty(outputUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Global,
                         0,
                         &callback,
                         sizeof(AURenderCallbackStruct));
    
    AudioStreamBasicDescription outputFormat;
    UInt32 size = sizeof(AudioStreamBasicDescription);
    
    outputFormat.mSampleRate = 44100;
    outputFormat.mFormatID = kAudioFormatLinearPCM;
    outputFormat.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
    outputFormat.mBitsPerChannel = 16;
    outputFormat.mChannelsPerFrame = 2;
    outputFormat.mFramesPerPacket = 1;
    outputFormat.mBytesPerFrame = outputFormat.mBitsPerChannel / 8 * outputFormat.mChannelsPerFrame;
    outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
    AudioUnitSetProperty(outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormat, size);
    AudioOutputUnitStart(outputUnit);
}
- (void)dispose
{
    AudioOutputUnitStop(outputUnit);
    AudioUnitUninitialize(outputUnit);
    AudioComponentInstanceDispose(outputUnit);
    outputUnit = NULL;
}
- (id) init
{
    self = [super init];
    if (self != nil) {
        [self setupOutputUnit];
    }
    return self;
}
- (void) dealloc
{
    [self dispose];
    [super dealloc];
}
@end

まず、AudioUnitを使うからといってAudioUnitFrameworkをインポートしてもエラーが出ます。AudioUnitだけをつかうときでもAudioUnitFrameworkはインポートせず、AudioToolbox.Frameworkだけをインポートすれば良いようです。

コードを見ていきますと、setupOutputUnitメソッドがAudioUnitのセットアップしている部分になります。順番としては、

・AudioComponentDescriptionに呼び出すAudioUnitの情報を記述する
・AudioComponentNextでAudioComponentを取得する
・AudioComponentInstanceNewでAudioUnitを取得する
・AudioUnitInitializeでAudioUnitを初期化する
・AURenderCallbackStructでコールバックの情報を記述する
・AudioUnitSetPropertyでコールバックの設定をする
・AudioUnitStartでRemoteIOをスタートする

という流れになっています。Macでのときと違って関数の名前にAudioとかついていますが、使い方は変わりません。iPhoneでのアウトプットユニットはRemoteIOと名前がついてまして、サブタイプにkAudioUnitSubType_RemoteIOを指定します。RemoteIOがスタートしたらコールバックが定期的に呼び出されますので、そこでオーディオデータを渡します。

iPhone OS 2.2.1までのRemoteIOのデフォルトのフォーマットは8.24の固定小数点でしたが、OS 3.0からは16bit整数のインターリーブドに変更されたようです。ただ、あくまでデフォルトが変更されただけですので、OSのバージョンがなんだろうが、自分でフォーマットを設定しておけば互換性は問題なく保たれます。ミキサーのユニットなんかは変わらず8.24みたいですので注意が必要です。

iOS 5ではさらにデフォルトが32ビットfloatに変更されているようです。AudioUnitSetPropertyでフォーマットを設定するように変更しました。(2011/10/24)

Touch the Wave公開しました

「Touch the Wave」が公開されました。オーディオデータの波形にタッチしてスクラッチをするというのをやってみたかったので作ってみたiPhoneアプリです。とりあえず今のところバージョン0.4にしているので、今後、基本的な仕様が大幅に変更されるかもしれません。

スピードを変化させたときの音質が一番改善したい点ではありますが、第一世代を切り捨てないと厳しいかなぁと思います。今でも画面の更新を12fpsくらいに落とさないと等速の再生すらおぼつかないので…。まあ、のんびりやっていきます。

ジョグホイール

iPodのクリックホイールのように、グルグルと円を描いて値を増減させる例です。NSViewのサブクラスで以下のような感じに。1周まわすと1.0増減します。

//
// YKJogWheelView.h
//

#import <Cocoa/Cocoa.h>

@interface YKJogWheelView : NSView {

    CGFloat gStartAngle;
    CGFloat gPreAngle;
    NSInteger gRotCount;
}

@end
//
// YKJogWheelView.m
//

#import "YKJogWheelView.h"

@implementation YKJogWheelView

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

- (void)drawRect:(NSRect)rect {
    // Drawing code here.
}

- (void)setValue:(CGFloat)value
{
    NSLog(@"value = %f", value);
}

- (CGFloat)getAngle:(NSEvent *)theEvent
{
    NSPoint point = [theEvent locationInWindow];
    NSRect frame = [self frame];
    return -atan2(point.y - frame.origin.y - frame.size.height / 2, point.x - frame.origin.x - frame.size.width / 2) / M_PI / 2.0;
}

- (void)setDragEvent:(NSEvent *)theEvent
{
    CGFloat newAngle = [self getAngle:theEvent];
    
    if ((newAngle - gPreAngle) > 0.5) {
        gRotCount--;
    } else if ((newAngle - gPreAngle) < -0.5) {
        gRotCount++;
    }
    
    [self setValue:newAngle - gStartAngle + gRotCount];
    
    gPreAngle = newAngle;
}

- (void)mouseDown:(NSEvent *)theEvent
{
    gStartAngle = [self getAngle:theEvent];
    gRotCount = 0;
    [self setValue:0];
}

- (void)mouseDragged:(NSEvent *)theEvent
{
    [self setDragEvent:theEvent];
}

@end