iPhone用のアプリも公開できて一段落つきまして、さらにNDAも緩和されたという事で、iPhoneのオーディオプログラミングネタを書いていきたいと思います。
iPhoneで音を再生する方法はいくつかあります。短いサンプルを再生するならSystemSoundService。BGMで使うような長い曲を再生するならAudioQueueService。レイテンシーを低く再生するならAudioUnit。あと、OpenALを使うという手もあります。
「Touch the Wave」で使っているのはAudioUnitです。SystemSoundは短すぎて役に立ちませんし、AudioQueueではリアルタイムに音を変化させるような事や逆再生が出来ないですし(たぶん)、OpenALはよくわからないって感じでしたので。
何はなくともとりあえずAudioUnitを使うクラスのサンプルコードです。このクラスを生成すればAudioUnitが開始されて、解放すれば停止します。シンプルにするためにエラー処理はまったく記述していません。
※2008/11/27 サイン波を鳴らすように変更しました。
※2009/1/9 サンプルの最大値の求め方を変更しました。
※2009/6/28 OS 3.0に合わせて変更しました。
//
// 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対応
AudioSampleType *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;
}
}
/* OS2.2.1以前
AudioUnitSampleType *ptr = ioData->mBuffers[i].mData;
for (NSInteger j = 0; j < inNumberFrames; j++) {
//2009/1/9 最大値を変更
float max = 1 << kAudioUnitSampleFractionBits;
ptr[j] = sin(M_PI / inNumberFrames * j * 50) * 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(outputFormat);
AudioUnitGetProperty(outputUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Global,
0,
&outputFormat,
&size);
NSLog(@"samplerate = %f", outputFormat.mSampleRate);
NSLog(@"bits = %u", outputFormat.mBitsPerChannel);
NSLog(@"channels = %u", outputFormat.mChannelsPerFrame);
NSLog(@"%@",
(outputFormat.mFormatFlags & kLinearPCMFormatFlagIsNonInterleaved) ?
@"non interleaved" : @"interleaved");
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がスタートしたらコールバックが定期的に呼び出されますので、そこでオーディオデータを渡します。
MacでのAudioUnitのデフォルトのオーディオデータはFloat32でしたが、iPhoneではAudioUnitSampleTypeという型が定義されていまして、SInt32がtypedefされています。ただ、32ビットそのまま使えるという訳ではなく、24bit分だけが使えます。24bit以上の音量を入れると歪んでいきますので、使う型はSInt32だけど、最大値を24bit(±8388607くらい)に抑えるという感じになります。AudioUnitSampleTypeの宣言のすぐ下にkAudioUnitSampleFractionBitsという定数が24と定義されていますので、ここから最大値を計算したほうが良いのかもしれません。自分は#defineでINT24_MAXとか作ってしまってます。
MacでのAudioUnitのデフォルトのオーディオデータはFloat32でしたが、iPhoneではAudioUnitSampleTypeという型が定義されていまして、SInt32がtypedefされています。ただ、型は整数ですが、リファレンスを見ると8.24の固定小数点という書き方がされています。これは、上位8ビットは整数部分、下位24ビットは小数点以下の部分として数字を表現しまして、小数点数なので最大値は1.0、最小値は-1.0の間が実際に使える範囲になります。
ということなのですが、実際には整数と考えれば良さそうで、SInt32の下位24ビット分くらいの範囲にレベルを抑えておくという扱い方になると思います。最大値を求めるには、AudioUnitSampleTypeの宣言のすぐ下にkAudioUnitSampleFractionBitsという定数が24と定義されていますので、これを使って求めると(1 << kAudioUnitSampleFractionBits)という感じでしょうか。
iPhone OS 2.2.1までのRemoteIOのデフォルトのフォーマットは8.24の固定小数点でしたが、OS 3.0からは16bit整数のインターリーブドに変更されたようです。ただ、あくまでデフォルトが変更されただけですので、OSのバージョンがなんだろうが、自分でフォーマットを設定しておけば互換性は問題なく保たれます。ミキサーのユニットなんかは変わらず8.24みたいですので注意が必要です。
いつも大変興味深く拝見させて頂いております。
for (NSInteger i = 0; i mNumberBuffers; i++) {
memset(ioData->mBuffers[i].mData,
0,
inNumberFrames * sizeof(AudioUnitSampleType));
}
の部分をsin()関数を使用して正弦波が出力されるようにかえてみたのですが、どうやっても矩形波(正弦波のレベルを上げて音が割れた状態)しか出力されないのです。(設定した周波数では出力されています。)
また、0.0001をインクリメントし1以上になったら-1にするようなノコギリ波にかえてみても定期的にインパルスが出力されるだけです。
何か他に必要なのでしょうか?
ioData->mBuffers[i].mDataに渡すデータとしては、32bit固定小数点、24bit固定小数点、32bit浮動小数点等試しましたが全て同じ結果でした。
お手数かとは存じますがご回答頂ければ幸いです。
さっそくすみません。
24bit固定小数点と書いてありましたね。失礼しました。
また、ioData->mBuffers[i].mDataのアドレスをポインタに入れ使っていたのですが、そのポインタの型をfloat型にしていたのが原因でした。
自己解決で申し訳ありません。今後のご活躍も期待しております。
せっかくなんで、サイン波を鳴らすように変更してみました。
最初の頃はMacの感覚でiPhoneでもfloatを使おうとしていたのですが、パフォーマンス的にも不利になるので、iPhoneは割り切って整数で通して扱った方が良さそうです。
(あれ、Integerって固定小数点じゃなくて整数ですよね?)
あ、すみません、RemoteIOは固定小数点てことでしたね。自分的にちょっと勘違いしていましたので、また書き直しました。
ちょっとお尋ねしたいのですが、
再生スピードの制御(ゆっくりとか速くとか)はできるんでしょうか?
kAudioUnitSubType_Varispeedやらでできるのかと思ったのですが、
うまく動いてくれませんでした。
AudioUnitSetParameterにはパラメータでこれを指定したのですが、値としては何を渡せばよいのでしょう???
コメントいただきましてありがとうございます。
VarispeedはMacでのみでiPhoneでは使えません。
AudioUnitの機能で再生スピードを変えるという事でしたら、AUConverterを作ってRemoteIOの前に繋ぎ、そのAUConverterのイン側とアウト側のサンプリング周波数を変えるという事になると思います。RemoteIOだけでもできるような気がしますが試してません。
なお、kAudioUnitSubType_VarispeedというのはAudioUnitの種類になりますので、ParameterにセットするのではなくVarispeedのAudioUnitを作成するという使い方になります。
あと、拙作のTouch the Waveのような連続的なスピード変化をさせたいという事でしたら、自分自身で実装しないと実現できないと思います。