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)