従来のタイムストレッチ、ピッチシフトに加え、8バンドのグラフィックイコライザーの機能を追加しました。
https://itunes.apple.com/jp/app/tune-changer-for-karaoke-instrument/id734191615?mt=8
https://itunes.apple.com/jp/app/realtime-pitch-shifter/id670120398?mt=8
https://itunes.apple.com/jp/app/time-stretcher/id637376117?mt=8
イコライザーの実装は、Core Audio の kAudioUnitSubType_NBandEQ を用いました。
自前で作成するとなると、FFTを用いたりする方法になるのでしょうが、素直に用意されている AUGraph を用いる方が音質もパフォーマンスも間違いないでしょう、ということで。。
まず定義部分の実装です。
以前書いた以下のブログをもとに、変更/追加部分について触れます。
「Audio Unit によるリアルタイム・タイムストレッチ」
http://www.loopsessions.com/blog/?m=201310
ですので、タイムストレッチ -> イコライザー の流れについての実装になります。(ピッチシフトは含んでいません)
まずは変数定義です。
AUGraph _graph; AudioUnit _converterUnit; AudioUnit _aUiPodTimeUnit; AudioUnit _auNBandEQUnit; AudioUnit _converterUnit2; AudioUnit _multiChannelMixerUnit; AudioUnit _remoteIOUnit;
AUGraph に NBandEQ の Node の追加します。(他の Node については省略)
AudioComponentDescription cd; cd.componentType = kAudioUnitType_Effect; cd.componentSubType = kAudioUnitSubType_NBandEQ; cd.componentManufacturer = kAudioUnitManufacturer_Apple; cd.componentFlags = 0; cd.componentFlagsMask = 0; AUNode auNBandEQNode; ret = AUGraphAddNode(_graph, &cd, &auNBandEQNode); ret = AUGraphNodeInfo(_graph, auNBandEQNode, NULL, &_auNBandEQUnit);
続いて、AudioUnit の接続の設定です。
今回の接続は以下の流れになります。
AUConverter -> AUiPodTimeOther -> AUNBandEQ -> AUConverter2 -> Remote IO
従来の AudioUnit は SInt32型の8.24固定小数点で定義されていたのですが、iOS5以降から定義された AudioUnit については、Float32型で定義されています。
そのため、AUNBandEQ のinput側のASBD(AudioStreamBasicDescription)構造体を取得して、AUiPodTimeOther および AUConverter のoutput側にセットする必要があります。
ret = AudioUnitSetProperty(_converterUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &_outputFormat, size); ret = AudioUnitSetProperty(_converterUnit2, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &_outputFormat, size); ret = AudioUnitSetProperty(_converterUnit2, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &_outputFormat, size); //////////////// AudioStreamBasicDescription outputFormatTmp; // [GET] NBandEQ I ret = AudioUnitGetProperty(_auNBandEQUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormatTmp, &size); ret = AudioUnitSetProperty(_aUiPodTimeUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outputFormatTmp, size); ret = AudioUnitSetProperty(_converterUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outputFormatTmp, size); //////////////// ret = AudioUnitSetProperty(_multiChannelMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &_outputFormat, size); ret = AudioUnitSetProperty(_multiChannelMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &_outputFormat, size); // remoteIO I ret = AudioUnitSetProperty(_remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &_outputFormat, size);
上記で説明した流れで Node を接続します。
ret = AUGraphConnectNodeInput(_graph, converterNode, 0, aUiPodTimeNode, 0); ret = AUGraphConnectNodeInput(_graph, aUiPodTimeNode, 0, auNBandEQNode, 0); ret = AUGraphConnectNodeInput(_graph, auNBandEQNode, 0, converterNode2, 0); ret = AUGraphConnectNodeInput(_graph, converterNode2, 0, multiChannelMixerNode, 0); ret = AUGraphConnectNodeInput(_graph, multiChannelMixerNode, 0, remoteIONode, 0);
次にパラメータ部分の説明です。今回は以下の帯域(Hz)を固定で設定しましたが、実際は任意の周波数で設定することが可能です。
64, 125, 250, 500, 1k, 2k, 4k, 8k
加えて、帯域幅(Q値)の設定も可能ですが、今回はデフォルト値で固定にしています。
以下に該当部分のソースコードを全て載せましたので、詳細の説明は省略します。
AUEffectProxy_NBandEQ.h
#import <Foundation/Foundation.h> #import <AudioToolbox/AudioToolbox.h> #define BAND_NUM (8) @interface AUEffectProxy : NSObject - (id)initWithAudioUnit:(AudioUnit)audioUnit; - (void)resetParameters; - (Float32)getFrequency:(int)index; - (void)setFrequency:(Float32)value index:(int)index; - (Float32)getGain:(int)index; - (void)setGain:(Float32)value index:(int)index; - (Float32)getBandwidth:(int)index; - (void)setBandwidth:(Float32)value index:(int)index; @end
AUEffectProxy_NBandEQ.m
#import "AUEffectProxy_NBandEQ.h" @interface AUEffectProxy () { AudioUnit _audioUnit; // デフォルト値の保存用 Float32 gain_; Float32 bandwidth_; AudioUnitParameterID paramId_; } - (Float32)valueForParameter:(int)parameter; - (void)setValue:(Float32)value forParameter:(int)parameter min:(Float32)min max:(Float32)max; @end @implementation AUEffectProxy Float32 const FREQUENCY[] = { 64.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0 }; - (id)initWithAudioUnit:(AudioUnit)audioUnit { self = [super init]; if (self) { _audioUnit = audioUnit; UInt32 size = sizeof(UInt32); AudioUnitGetPropertyInfo(_audioUnit, kAudioUnitProperty_ParameterList, kAudioUnitScope_Global, 0, &size, NULL); int numOfParams = size / sizeof(AudioUnitParameterID); AudioUnitParameterID paramList[numOfParams]; AudioUnitGetProperty(_audioUnit, kAudioUnitProperty_ParameterList, kAudioUnitScope_Global, 0, paramList, &size); int iCntFreq = 0; for (int i = 0; i < numOfParams; i++) { paramId_ = paramList[i]; AudioUnitParameterInfo paramInfo; size = sizeof(paramInfo); AudioUnitGetProperty(_audioUnit, kAudioUnitProperty_ParameterInfo, kAudioUnitScope_Global, paramList[i], ¶mInfo, &size); // init if (strcmp(paramInfo.name, "bypass") == 0) { // 「使用:0」固定 AudioUnitParameterValue fBypass = 0; AudioUnitSetParameter(_audioUnit, paramList[i], kAudioUnitScope_Global, 0, fBypass, 0); } else if (strcmp(paramInfo.name, "frequency") == 0) { // set AudioUnitSetParameter(_audioUnit, paramList[i], kAudioUnitScope_Global, 0, FREQUENCY[iCntFreq], 0); iCntFreq ++; } else if (strcmp(paramInfo.name, "gain") == 0) { AudioUnitGetParameter(_audioUnit, paramList[i], kAudioUnitScope_Global, 0, &gain_); } else if (strcmp(paramInfo.name, "bandwidth") == 0) { AudioUnitGetParameter(_audioUnit, paramList[i], kAudioUnitScope_Global, 0, &bandwidth_); } } } return self; } - (void)resetParameters { for (int i = 0; i < BAND_NUM; i++){ [self setFrequency:FREQUENCY[i] index:i]; [self setGain:gain_ index:i]; [self setBandwidth:bandwidth_ index:i]; } } - (Float32)getFrequency:(int)index { return FREQUENCY[index]; } - (void)setFrequency:(Float32)value index:(int)index { [self setValue:value forParameter:kAUNBandEQParam_Frequency + index min:10 max:21600.0]; } - (Float32)getGain:(int)index { return [self valueForParameter:kAUNBandEQParam_Gain + index]; } - (void)setGain:(Float32)value index:(int)index { [self setValue:value forParameter:kAUNBandEQParam_Gain + index min:-96.0f max:24.0f]; } - (Float32)getBandwidth:(int)index { return [self valueForParameter:kAUNBandEQParam_Bandwidth + index]; } - (void)setBandwidth:(Float32)value index:(int)index { [self setValue:value forParameter:kAUNBandEQParam_Bandwidth + index min:0.05 max:5.0]; } - (Float32)valueForParameter:(int)parameter { Float32 value; OSStatus rt = AudioUnitGetParameter(_audioUnit, parameter, kAudioUnitScope_Global, 0, &value); if (rt != noErr) { NSLog(@"Error getting parameter(%d)", parameter); return MAXFLOAT; } return value; } - (void)setValue:(Float32)value forParameter:(int)parameter min:(Float32)min max:(Float32)max { if (value < min || value > max) { return; } OSStatus rt = AudioUnitSetParameter(_audioUnit, parameter, kAudioUnitScope_Global, 0, value, 0); if (rt != noErr) { NSLog(@"Error Setting parameter(%d)", parameter); } } @end
また、Ver.2.01 にて、他のプレーヤーアプリとのミックス再生にも対応しました。
(逆を言えば、今までは途中でiPodを起動したら再生が停止してしまっている状態でした。。)
AudioSessionInitialize は deprecated になるようですので、AVAudioSession を用いた以下の方法で実装しました。
- (void)setupAudioSession { AVAudioSession *session = [AVAudioSession sharedInstance]; // スリープモードでも再生 // オーディオのミックス動作のオーバーライド NSError *setCategoryError = nil; [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&setCategoryError]; [session setActive:YES error:nil]; }
iOSでの Audio Session は AVAudioSession の使用にシフトしていくようです。フレームワークも整備されていき、どんどん実装も簡単になっていく傾向にあるのかもしれません。
いつも参考にさせて頂いています。
Core Audioは情報が少ないので非常に助かっています。
> また、Ver.2.01 にて、他のプレーヤーアプリとのミックス再生にも対応しました。
セッションカテゴリをAVAudioSessionCategoryOptionMixWithOthersにすると、MPNowPlayingInfoCenterを使ってもロック画面に画像を表示できなくなってしまいましたが、これはそういうものなんでしょうね。
ロック画面等からのリモートイベントに対応したいプレーヤー系のアプリはミックス再生に対応しようとしてはいけないんですね。