プログラミング」カテゴリーアーカイブ

iPod touchのInterruption Callback

iPhoneでアラームとか通話とかが割り込んできてアプリのオーディオが停止してしまうときの通知を受け取るにはAudioSessionでInterruption Callbackを登録するって感じなのですが、サンプルソースなどは以下のようなコードになってまして…

void rioInterruptionListener(void *inUserData, UInt32 inInterruption)
{
    AudioUnit *remoteIO = (AudioUnit*)inUserData;

    if (inInterruption == kAudioSessionEndInterruption) {
        AudioSessionSetActive(true);
        AudioOutputUnitStart(*remoteIO);
    }

    if (inInterruption == kAudioSessionBeginInterruption)
        AudioOutputUnitStop(*remoteIO);		
}

自分のアプリに戻ってきたときにはこのコールバックが呼ばれて、kAudioSessionEndInterruptionがinInterruptionに来る事になっていますが、1つ例外があるようです。

iPod touchでホームボタンをダブルタップすると、ミュージックをコントロールできる小さいウィンドウが現れます。そこで再生を押すとオーディオの割り込み開始のkAudioSessionBeginInterruptionは来ますが、そのあと停止してウィンドウを閉じてもkAudioSessionEndInterruptionは来ません。

とりあえず、戻ってくるところはUIApplicationのapplicationDidBecomeActive:で処理しないといけなさそうです。

文字列のイメージを作成する

OpenGL ESで文字列を表示しようと思ったのですが、OpenGL自体にそんな機能は無さそうなので、テクスチャに文字列を描画する方法を調べてみました。

テクスチャをCGContextから作って貼付けるところはDev Centerのサンプルソースを参考にしていただくとして、その前段階でCGImageの文字列を作成するコードです。

CGRect imageRect; //文字列用の画像の大きさ
CGContextRef bitmapContext; //文字列用のコンテキスト
Byte *bitmapBuffer; //コンテキストのバッファ

というのが用意されているとして、以下のような感じです。

memset(bitmapBuffer, 0, imageRect.size.width * imageRect.size.height * 4);
    
CGContextSetRGBFillColor (bitmapContext, 0.0, 0.0, 0.0, 1.0);
CGContextFillRect (bitmapContext, imageRect);
    
UIGraphicsPushContext(bitmapContext);
    
UIFont *tFont = [UIFont systemFontOfSize:40];
[[UIColor whiteColor] set];
[@"Test String 1234567890" drawAtPoint:CGPointMake(0, 0) withFont:tFont];
[@"日本語もOK!" drawAtPoint:CGPointMake(0, 44) withFont:tFont];
    
UIGraphicsPopContext();
    
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
//ここでテクスチャに描画する
CGImageRelease(image);

NSStringの描画メソッドはコンテキストを指定する事が出来ないので、UIGraphicPushContext()でbitmapContextをカレントのコンテキストにしています。実際にテクスチャに貼付けて表示してみるとこんな感じ。

drawtext.jpg

ちなみに、ランドスケープで左上原点にしてみるってこともやっていたので、こんな位置に表示されています。

これでパフォーマンス的にどうなのかは実際にアプリに組み込んでみないとまだ分かりませんけど。

修正とiPod 2Gでの不具合

Touch the Waveの波形表示が再生位置とちょっとずれていたので、修正したものをApp Storeに提出してあります。何も無ければ2〜3日後くらいには公開されるんじゃないでしょうか。

それはそれとして、音質向上のための修正をしていたらv0.5をiPod touch第2世代で使っているとノイズがプチプチと入っている事が判明してしまいました。v0.4ではCore Animationを使っている事で数秒間の動作停止が起きることがあって、それを修正するためにv0.5ではOpenGLに変更したんですけど、どうやらそっちも相性が悪かったようです。

別に特別な処理をしているからという訳ではなく、何も描画していないOpenGLのビューを一枚表示しておいて、RemoteIOでシンプルにサイン波だけを鳴らしてみると、それだけでプチプチいっちゃいます。なのでTouch the Waveではプレイリストとダウンロードの画面では大丈夫なんですけど、肝心のプレイヤーでノイズが入ってしまうという困った感じです。

まさか僕のデバイスだけの故障とかじゃないですよねぇ。第1世代では何事も無く再生できちゃうんですけど。

まあ、iPod touch第2世代はもともとロスレスの再生がプツプツとぎれるって不具合がありますから。全体的にパフォーマンスはアップしているけどオーディオは逆に弱くなっているんでしょうか。

回避策としてあと残されているのは、Quartzですかね。試してみてパフォーマンス的に問題無さそうならそっちに切り替えます。せっかくのiPhoneでのプログラミングなんだからCore AnimationとかOpenGLとか使ってみたかったのに、結局Quartzに落ち着くとしたらもったいないなぁと思いますけど。それでも駄目ならOS側の修正アップデートを待つしかないって感じです。

さっそく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];
        }
    }
}

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)

ジョグホイール

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

24ビット

24bitのオーディオデータを作ったり読みこもうとしてもObjective-C(というかC言語)では24bitの型がありませんので、符号付き32bit整数の上位24bit分を使うというのが簡単な方法ではないかと思います。

ただこの場合、32bitの下位8bitは単純に切り捨てられますので、音にこだわるのであればディザをかけたりしたほうが良いのかもしれませんが、とりあえず今回は無視します。

32bit領域のAlignedHighなデータとして渡したりするのであれば何もしなくてもいいですが、32bit領域でAlignedLowな24bitのデータにしなくちゃいけないなんて時があったりしたら、

SInt32 value;

という、あるオーディオのデータがあったとして、

value >>= 8;

としてやります。

処理系によって算術シフトになるか論理シフトが変わるらしいですが、Xcodeでgccを使っているのであればこの場合算術シフトで、valueがマイナスのときは上位8bitが1で埋め尽くされています。

ちなみに、IntelMac想定の検証コード。

#import <Foundation/Foundation.h>

void logBit (SInt32 value) {
    printf("MSB ");
    for (NSInteger i = 3; i >= 0; i--) {
        Byte *valuePtr = (Byte *)&value;
        for (NSInteger j = 7; j >= 0; j--) {
            SInt32 isBitOn = (valuePtr[i] & (1 << j)) ? YES : NO;
            printf("%d", isBitOn);
        }
        printf(" ");
    }
    printf(" LSB\n\n");
}

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

    SInt32 value = INT32_MIN;
    printf("value = %d\n", value);
    logBit(value);
    
    SInt32 sValue = value;
    sValue >>= 8;
    printf("signed shift value = %d\n", sValue);
    logBit(sValue);
    
    UInt32 uValue = (UInt32)value;
    uValue >>= 8;
    printf("unsigned shift value = %u\n", uValue);
    logBit(uValue);
    
    [pool drain];
    return 0;
}

実行すると…

value = -2147483648
MSB 10000000 00000000 00000000 00000000  LSB

signed shift value = -8388608
MSB 11111111 10000000 00000000 00000000  LSB

unsigned shift value = 8388608
MSB 00000000 10000000 00000000 00000000  LSB

という感じになります。

リサージュ波形を表示する

オシロスコープで表示できるようなリサージュ波形を描画するには、

x = sin(右チャンネルのオーディオデータ);
y = sin(左チャンネルのオーディオデータ);

という感じでサンプルごとに座標を求めて、線で繋ぐ。

NSViewのサブクラスでこのようなインスタンス変数があるとして、

NSUInteger length; //オーディオデータのサンプル数
float *lPtr; //左チャンネルのオーディオデータ
float *rPtr; //右チャンネルのオーディオデータ

以下のようにdrawRectメソッドを記述する。(※2008/7/8 描画する位置をrectではなくboundsから求めるように変更しました)

- (void)drawRect:(NSRect)rect {
    
    NSRect viewRect = [self bounds];
    double halfWidth = viewRect.size.width / 2.0;
    double halfHeight = viewRect.size.height / 2.0;
    
    [[NSColor blackColor] set];
    NSRectFill(
        NSMakeRect(0, 0, viewRect.size.width, viewRect.size.height));
    
    NSBezierPath *path = [NSBezierPath bezierPath];
    [[NSColor greenColor] set];
    [path setLineWidth:1.0];
    
    for (NSUInteger i = 0; i < length; i++) {
        
        double x = sin(rPtr[i]);
        double y = sin(lPtr[i]);
        
        NSPoint point = 
            NSMakePoint(x * halfWidth + halfWidth , 
                y * halfHeight + halfHeight);
        if ([path isEmpty]) {
            [path moveToPoint:point];
        }
        
        [path lineToPoint:point];
    }
    
    [path stroke];
}

Core Audio Clock その4 スレーブ時の再生スピード調整

Core Audio ClockをMTCのスレーブにした時、あるいはインターナルのソースをHostTimeにした時、Core Audio Clockの時間を基準にしてオーディオを再生しようとすると、オーディオデバイスとスピードのずれが出てきますので、調整しなければいけません。

まず、オーディオの再生をしているときのIOProc内で、再生する頭のタイミングのHostTime(以下スタートホストタイム)と、HostTimeとオーディオデバイスのスピードの比率である「RateScalar」が取得できます。それと、MTCのスレーブにしているときはCoreAudioClockの「PlayRate」が、MTCとHostTimeのスピードの比率になっていますので、基本的にこれらを使って調整してみます。

1回のIOProcごとのオーディオデータを用意するスタート時間としてスタートホストタイムを設定し、オーディオデータに( RateScalar × PlayRate )で求めたレートでVariSpeed等をかけてやれば良いはずなのですが、実際はスタートホストタイムがアバウトで微妙にデバイスとずれていたりして音を鳴らすとプチプチとノイズが入ってしまったりします。

なので、スタートホストタイムは直接使わずに、( RateScalar × PlayRate )で調整したバッファフレームサイズ分の時間を毎度足していった値をスタート時間にして、スタートホストタイムとの差を少しずつ調整していくという感じでやってみました。

ということで、インスタンス変数には以下のようなものがあるとしておいて、

CAClockRef clockRef; //Core Audio Clock
Float64 deviceSampleRate; //オーディオデバイスのサンプリング周波数
UInt32 bufferFrames; //オーディオデバイスのバッファフレーム数
double preSeconds; //スタート時間の差を求めるための時間

IOProc内で以下のようなメソッドを呼ぶという感じで試してみました。ここでは、オーディオデータのスタート時間と1フレーム分の時間を求めています。

- (void)ioProc:(AudioBufferList *)outOutputData 
    outputTime:(const AudioTimeStamp *)inOutputTime
{
    OSStatus err;
    
    //アウトプットデバイスのRateScalarを取得する
    double rateScalar = inOutputTime->mRateScalar;
    //スピード調整のためのレート
    double ajustRate;
    //今回のスタート時間
    double startSeconds;
    
    //CAClockのプレイレートを取得する
    double playRate;
    err = CAClockGetPlayRate(clockRef, &playRate);
    if (err != noErr) NSLog(@"get PlayRate Err");
    
    //アウトプットデバイスのスタート時間をホストタイムから変換して取得する
    CAClockTime inHostTime;
    inHostTime.format = kCAClockTimeFormat_HostTime;
    inHostTime.time.hostTime = inOutputTime->mHostTime;
    
    CAClockTime outSecondsTime;
    outSecondsTime.format = kCAClockTimeFormat_Seconds;
    
    err = CAClockTranslateTime(clockRef, &inHostTime, 
        kCAClockTimeFormat_Seconds, &outSecondsTime);
    if (err != noErr) NSLog(@"translatetime Err");
    
    //前回コールバック時に求めたスタート時間との差
    double gap = outSecondsTime.time.seconds - preSeconds;
    
    //今回のスタート時間を設定する。前回との差が大きければリセット
    if (fabs(gap) > 0.1) {
        startSeconds = outSecondsTime.time.seconds;
        gap = 0.0;
    } else {
        startSeconds = preSeconds;
    }
    
    //差がきわめて小さければ調整はしない。
    //調整するなら差の大きさによって調整するレートを求める
    if (fabs(gap) > 0.0001) {
        ajustRate = pow(10, gap);
    } else {
        ajustRate = 1.0;
    }
    
    //1サンプル分の時間
    double secondsPerFrame = 
        playRate * rateScalar / deviceSampleRate * ajustRate;
    //次のコールバック時のスタート時間を算出する
    preSeconds = startSeconds + secondsPerFrame * bufferFrames;


    //
    // ここで、startSecondsとsecondsPerFrameを元に
    // オーディオデータを書き込む
    //
}

テンポラリファイル名を取得する

テンポラリのディレクトリの中のテンポラリのファイル名を取得するサンプル。

NSTemporaryDirectory()でテンポラリフォルダを取得。mkstempsでテンポラリファイル名が取得できるが、一旦書き込み可能な文字列の配列にコピーしないといけない。NSStringのlengthでは日本語とか含まれている場合、取得した長さが配列の長さと違うので、strlenで配列の長さを取得してコピーする。

#import <Foundation/Foundation.h>

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

    NSString *tempDir = NSTemporaryDirectory();
    NSString *filePath = 
        [tempDir stringByAppendingPathComponent:
            @"prefixXXXXXXsuffix"];
    
    size_t bufferSize = 
        strlen([filePath fileSystemRepresentation]) + 1;
    char buffer[bufferSize];
    if ([filePath getFileSystemRepresentation:buffer 
            maxLength:bufferSize]) {
        if (mkstemps(buffer, 6) != -1) {
            NSLog(@"TemporaryFile = '%s'", buffer);
        }
    }
    
    [pool drain];
    return 0;
}