AUGraphを用いたリアルタイム・タイムストレッチ

**[追記 2019/1/7]**
**AUGraphはiOS 12まででDeprecatedとなりました。本記事はそれをふまえてご参照ください。**

[前回の投稿](http://qiita.com/MJeeeey/items/b09e65cbded0cc3eb193)で、リアルタイム・オーディオ処理について書きましたが、その続きになります。

今回はリアルタイム・タイムストレッチ（音程をそのままで、スピードを変更する処理）の実現方法について説明していきます。

今回の内容も、以下のブログにて以前書いた内容を整理したものになります。
http://www.loopsessions.com/blog/?p=39
http://www.loopsessions.com/blog/?p=44

前回の投稿もとに、修正箇所と追加内容について説明していきます。

今回のオーディオ関連のクラスの定義は以下のようになります。

~~~objectivec
@interface TimeStretchAudioIO ()
{
	AudioStreamBasicDescription _outputFormat;
	
	AUGraph _graph;
	AudioUnit _remoteIOUnit;
	AudioUnit _converterUnit;
	AudioUnit _aUiPodTimeUnit;	// add
	
	AudioUnitParameterID _paramId;	// add
	AudioUnitParameterInfo *_paramInfo;	// add
}

@property (readonly) ExtAudioFileRef extAudioFile;
@property (nonatomic, assign) UInt32 numberOfChannels;
@property (nonatomic, assign) SInt64 totalFrames;
@property (nonatomic, assign) SInt64 currentFrame;

@end
~~~

## AUGraphの準備

まずは以下の関数の修正を行います。
リアルタイム・タイムストレッチを実現するには、AUiPodTimeOther Unit (`kAudioUnitSubType_AUiPodTimeOther`)を使用します。

接続は以下の通りです。

Callback -> AUiPodTimeOther -> AUConverter -> Remote IO

~~~objectivec
- (OSStatus)initAUGraph
{
	OSStatus ret = noErr;
	
	// AUGraphの準備
	NewAUGraph(&_graph);
	AUGraphOpen(_graph);
	
	// AUNodeの作成
	AudioComponentDescription cd;
	
	cd.componentType = kAudioUnitType_FormatConverter;
	cd.componentSubType = kAudioUnitSubType_AUConverter;
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;
	cd.componentFlags = 0;
	cd.componentFlagsMask = 0;
	AUNode converterNode;
	AUGraphAddNode(_graph, &cd, &converterNode);
	AUGraphNodeInfo(_graph, converterNode, NULL, &_converterUnit);
	
	cd.componentType = kAudioUnitType_FormatConverter;
	cd.componentSubType = kAudioUnitSubType_AUiPodTimeOther;
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;
	cd.componentFlags = 0;
	cd.componentFlagsMask = 0;
	AUNode aUiPodTimeNode;
	AUGraphAddNode(_graph, &cd, &aUiPodTimeNode);
	AUGraphNodeInfo(_graph, aUiPodTimeNode, NULL, &_aUiPodTimeUnit);
	
	cd.componentType = kAudioUnitType_Output;
	cd.componentSubType = kAudioUnitSubType_RemoteIO;
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;
	cd.componentFlags = 0;
	cd.componentFlagsMask = 0;
	AUNode remoteIONode;
	AUGraphAddNode(_graph, &cd, &remoteIONode);
	AUGraphNodeInfo(_graph, remoteIONode, NULL, &_remoteIOUnit);
	
	// Callbackの作成
	AURenderCallbackStruct callbackStruct;
	callbackStruct.inputProc = renderCallback;
	callbackStruct.inputProcRefCon = self;
	AUGraphSetNodeInputCallback(_graph,
								converterNode,
								0,  // bus number
								&callbackStruct);
	
	// 各NodeをつなぐためのASBDの設定
	UInt32 size = sizeof(AudioStreamBasicDescription);
	// converter I
	ret = AudioUnitSetProperty(_converterUnit,
							   kAudioUnitProperty_StreamFormat,
							   kAudioUnitScope_Input, 0,
							   &_outputFormat, size);
	
	// remoteIO I
	ret = AudioUnitSetProperty(_remoteIOUnit,
							   kAudioUnitProperty_StreamFormat,
							   kAudioUnitScope_Input, 0,
							   &_outputFormat, size);
	
	AudioStreamBasicDescription outputFormatTmp;
	
	// [GET] AUiPodTimeOther I
	ret = AudioUnitGetProperty(_aUiPodTimeUnit,
							   kAudioUnitProperty_StreamFormat,
							   kAudioUnitScope_Input, 0,
							   &outputFormatTmp, &size);
	
	// converter O
	ret = AudioUnitSetProperty(_converterUnit,
							   kAudioUnitProperty_StreamFormat,
							   kAudioUnitScope_Output, 0,
							   &outputFormatTmp, size);
	
	// Nodeの接続
	// AUConverter -> AUiPodTimeOther
	ret = AUGraphConnectNodeInput(_graph, converterNode, 0, aUiPodTimeNode, 0);
	// AUiPodTimeOther -> Remote IO
	ret = AUGraphConnectNodeInput(_graph, aUiPodTimeNode, 0, remoteIONode, 0);
	
	// コンソールに現在のAUGraph内の状況を出力(デバッグ)
	CAShow(_graph);
	
	// AUGraphを初期化
	ret = AUGraphInitialize(_graph);
	
	// AUiPodTimeOtherのパラメータを初期化
	[self initAUiPodTimeOther];
	
	return ret;
}
~~~

AUiPodTimeOther Unit を追加したことで、各Unitのオーディオフォーマットを合わせることを考慮しておく必要があります。細かな説明は省きますが、「ASBD（Audio Stream Basic Description）の設定」の箇所を確認してみてください。

## パラメータの設定

ここからは追加の内容です。AUiPodTimeOther Unit のパラメータを設定するための実装になります。

### 初期化処理

~~~objectivec
- (void)initAUiPodTimeOther
{
	// AudioUnitGetProperty で取得する paramList のサイズを取得
	UInt32 size = sizeof(UInt32);
	AudioUnitGetPropertyInfo(_aUiPodTimeUnit,
							 kAudioUnitProperty_ParameterList,
							 kAudioUnitScope_Global,
							 0,
							 &size,
							 NULL);
	
	int numOfParams = size / sizeof(AudioUnitParameterID);
	NSLog(@"numOfParams = %d", numOfParams);
	
	// paramList の各IDを取得
	AudioUnitParameterID paramList[numOfParams];
	AudioUnitGetProperty(_aUiPodTimeUnit,
						 kAudioUnitProperty_ParameterList,
						 kAudioUnitScope_Global,
						 0,
						 paramList,
						 &size);
	
	_paramInfo = (AudioUnitParameterInfo *)malloc(numOfParams * sizeof(AudioUnitParameterInfo));
	
	for (int i = 0; i < numOfParams; i++) {
		NSLog(@"paramList[%d] = %d", i, (unsigned int)paramList[i]);
		_paramId = paramList[i];
		
		// 各IDのパラメータを取得
		size = sizeof(_paramInfo[i]);
		AudioUnitGetProperty(_aUiPodTimeUnit,
							 kAudioUnitProperty_ParameterInfo,
							 kAudioUnitScope_Global,
							 paramList[i],
							 &_paramInfo[i],
							 &size);
		
		NSLog(@"paramInfo.name = %s", _paramInfo[i].name);
		NSLog(@"paramInfo.minValue = %f", _paramInfo[i].minValue);
		NSLog(@"paramInfo.maxValue = %f", _paramInfo[i].maxValue);
		NSLog(@"paramInfo.defaultValue = %f", _paramInfo[i].defaultValue);
		
		// init
		AudioUnitSetParameter(_aUiPodTimeUnit,
							  paramList[i],
							  kAudioUnitScope_Global,
							  0,
							  _paramInfo[i].defaultValue,
							  0);
	}
}
~~~

paramList という変数に AUiPodTimeOther Unit に関するパラメータがenum型で入ります。
一般的なAudioUnitのパラメータ情報は`AudioUnit/AudioUnitParameters.h`に定義されているのですが、なぜか今回の対象の AUiPodTimeOther Unit は定義されていません。ただ、上記のように実装することで、あらゆるAudioUnitのパラメータ情報を取得することができますので、あまり気にすることはないかと思います。

今回の場合で initAUiPodTimeOther 関数を処理した際の NSLog の出力結果は以下のようになります。

~~~
numOfParams = 1
paramList[0] = 0
paramInfo.name = rate
paramInfo.minValue = 0.500000
paramInfo.maxValue = 2.000000
paramInfo.defaultValue = 1.000000
~~~

つまり、0.5〜2.0倍の範囲で再生速度が変更可能であることがわかります。

### パラメータの値の代入

以下の関数を呼び出すことで、再生速度が変更できます。スライダーで操作するのが良いかと思います。
以下の実装は簡略化していますので、実際にはエラー処理を入れた方が良いと思います。（サンプルコードを参照してください）

~~~objectivec
- (void)setPlaybackRate:(Float32)value
{
	AudioUnitSetParameter(_aUiPodTimeUnit,
						  _paramId,
						  kAudioUnitScope_Global,
						  0,
						  value,
						  0);
}
~~~

以上です。

今回のサンプルプログラムは以下にあります。（TimeStretchAudioIO）
https://github.com/JunichiMinamino/AudioObjCSample

あと、今回の実装をもとに作成したアプリもありますので、よろしければ試してみてください。
タイムストレッチ（再生速度変更）／イコライザー
https://itunes.apple.com/jp/app/time-stretcher/id637376117?mt=8