iOS13からAVAudioEngineでVoiceProcessingIOを用いたエコーキャンセルがsetVoiceProcessingEnabled(_:) によって簡単にできるようになりました。
とはいえ、iOS13以前のバージョンでもエコーキャンセルをできるようにしたいので、以前のバージョンでも対応する形で実装してみました。
Manual Rendering
実装の方針としては、AVAudioEngineのManualRenderingModeを使用し、AudioUnitから取得した音声をAVAudioEngineに流し込むこみ、スピーカーへの出力はAVAudioEngineが担う形にします。
AudioUnitの初期化
AudioComponentDescription.componentSubType
にkAudioUnitSubType_VoiceProcessingIO
を指定してAudioUnitの初期化を行います。
この際に出力は使用しないので入力のみのkAudioOutputUnitProperty_EnableIO
を有効にします。
// @see AudioUnitElement
enum {
OUT_BUS = 0 ,
IN_BUS = 1
};
- ( BOOL ) setupAudioUnit {
OSStatus error = noErr ;
AudioComponent component = AudioComponentFindNext ( NULL , & self -> _audioComponentDescription );
if ( component == NULL ) {
return NO ;
}
error = AudioComponentInstanceNew ( component , & self -> _audioUnit );
if ( error != noErr ) {
goto fail ;
}
// BUS1からの出力
error = AudioUnitSetProperty ( self . audioUnit , kAudioUnitProperty_StreamFormat , kAudioUnitScope_Output , IN_BUS ,
& self -> _audioStreamBasicDescription , sizeof ( self -> _audioStreamBasicDescription ));
if ( error != noErr ) {
goto fail ;
}
// BUS0への入力
error = AudioUnitSetProperty ( self . audioUnit , kAudioUnitProperty_StreamFormat , kAudioUnitScope_Input , OUT_BUS ,
& self -> _audioStreamBasicDescription , sizeof ( self -> _audioStreamBasicDescription ));
if ( error != noErr ) {
goto fail ;
}
// 入力だけ使用する
UInt32 enabled = 1 ;
error = AudioUnitSetProperty ( self . audioUnit , kAudioOutputUnitProperty_EnableIO , kAudioUnitScope_Input , IN_BUS , & enabled , sizeof ( enabled ));
if ( error != noErr ) {
goto fail ;
}
// AVAudioEngine#enableManualRenderingMode:format:maximumFrameCount:error: で使用する
UInt32 framePerSliceSize = sizeof ( self -> _maximumFramesPerSlice );
error = AudioUnitGetProperty ( self -> _audioUnit , kAudioUnitProperty_MaximumFramesPerSlice , kAudioUnitScope_Global , OUT_BUS , & self -> _maximumFramesPerSlice , & framePerSliceSize );
if ( error != noErr ) {
goto fail ;
}
AURenderCallbackStruct renderCallback ;
renderCallback . inputProc = AudioRecorderRenderCallback ;
renderCallback . inputProcRefCon = ( void * ) self -> _recorderContext ;
error = AudioUnitSetProperty ( self . audioUnit , kAudioUnitProperty_SetRenderCallback , kAudioUnitScope_Output , OUT_BUS , & renderCallback , sizeof ( renderCallback ));
if ( error != noErr ) {
goto fail ;
}
AURenderCallbackStruct inputCallback ;
inputCallback . inputProc = AudioRecorderInputCallback ;
inputCallback . inputProcRefCon = ( void * ) self -> _recorderContext ;
error = AudioUnitSetProperty ( self . audioUnit , kAudioOutputUnitProperty_SetInputCallback , kAudioUnitScope_Input , IN_BUS , & inputCallback , sizeof ( inputCallback ));
if ( error != noErr ) {
goto fail ;
}
error = AudioUnitInitialize ( self . audioUnit );
if ( error != noErr ) {
goto fail ;
}
fail:
AudioComponentInstanceDispose ( self . audioUnit );
self . audioUnit = NULL ;
return NO ;
}
Manual Renderingの流れ
AudioUnitの初期化ができたら、これを用いてManualRenderingを実行していきます。
ManualRenderingは自分でレンダリング関数を実行する必要があり 、これをAudioUnitのループのタイミングで実行します。
ここで少しややこしいのは、renderingBlock
の引数にバッファはなく、AVAudioInputNode#setManualRenderingInputPCMFormat:inputBlock: によって取得されるという点です。
そのため、予め取得先のバッファを返すようにしておき、そのバッファにAudioUnitRender
で書き込みを行うように実装する必要があります。
static OSStatus AudioRecorderInputCallback ( void * _Nonnull inRefCon , AudioUnitRenderActionFlags * _Nonnull ioActionFlags ,
const AudioTimeStamp * _Nonnull inTimeStamp , UInt32 inBusNumber , UInt32 inNumberFrames , AudioBufferList * _Nullable ioData ) {
assert ( ioData == NULL );
AudioRecorderContext * context = ( AudioRecorderContext * ) inRefCon ;
OSStatus error = noErr ;
AudioBufferList * inputAudioBufferList = context -> pInputAudioBufferList ;
inputAudioBufferList -> mNumberBuffers = 1 ;
// sizeofの部分が設定依存なので注意
inputAudioBufferList -> mBuffers [ 0 ]. mDataByteSize = inNumberFrames * sizeof ( UInt16 ) * context -> pStreamBasicDescription -> mChannelsPerFrame ;
inputAudioBufferList -> mBuffers [ 0 ]. mNumberChannels = context -> pStreamBasicDescription -> mChannelsPerFrame ;
error = AudioUnitRender ( context -> audioUnit , ioActionFlags , inTimeStamp , inBusNumber , inNumberFrames , inputAudioBufferList );
if ( error != noErr ) {
return error ;
}
return noErr ;
}
- ( BOOL ) connectTo :( AVAudioEngine * _Nonnull ) engine {
NSParameterAssert ( engine != nil );
AudioRecorderContext * context = self -> _recorderContext ;
if ( ! [ engine enableManualRenderingMode : AVAudioEngineManualRenderingModeRealtime format : self . processingFormat maximumFrameCount : self . maximumFramesPerSlice error : NULL ]) {
return NO ;
}
BOOL success = [ engine . inputNode setManualRenderingInputPCMFormat : self . processingFormat inputBlock : ^ const AudioBufferList * _Nullable ( AVAudioFrameCount inNumberOfFrames ) {
// AudioUnitRenderで読み込む先のbufferを指定する
return context -> pInputAudioBufferList ;
}];
if ( ! success ) {
return NO ;
}
self . recorderContext -> renderingBlock = ( __bridge void * ) engine . manualRenderingBlock ;
return YES ;
}
static OSStatus AudioRecorderRenderCallback ( void * _Nonnull inRefCon , AudioUnitRenderActionFlags * _Nonnull ioActionFlags ,
const AudioTimeStamp * _Nonnull inTimeStamp , UInt32 inBusNumber , UInt32 inNumberFrames , AudioBufferList * _Nullable ioData ) {
assert ( ioData != NULL );
AudioRecorderContext * context = ( AudioRecorderContext * ) inRefCon ;
OSStatus error = noErr ;
AVAudioEngineManualRenderingBlock renderingBlock = ( __bridge AVAudioEngineManualRenderingBlock )( context -> renderingBlock );
AVAudioEngineManualRenderingStatus renderingStatus = renderingBlock ( inNumberFrames , ioData , & error );
if ( renderingStatus != AVAudioEngineManualRenderingStatusSuccess ) {
return error ;
}
return noErr ;
}
エコーキャンセルについて
これでiOS13以前にも対応した、AVAudioEngineのエコキャン実装ができます。
@constant kAudioUnitSubType_VoiceProcessingIO
- Available on OS X and with iOS 3.0 or greater
This audio unit can do input as well as output. Bus 0 is used for the output
side, bus 1 is used to get audio input (thus, on the iPhone, it works in a
very similar way to the Remote I/O). This audio unit does signal processing on
the incoming audio (taking out any of the audio that is played from the device
at a given time from the incoming audio).
kAudioUnitSubType_VoiceProcessingIO
はデバイスからの音声出力を使用して処理を行うようなので、必ずしも同じAudioUnitインスタンスから音を出す必要はありません。
上記の実装もAVAudioEngineに移譲するので別のAudioUnitインスタンスで音声が出力されます。
Trouble shooting
error -10868
2019-12-22 16:26:57.868008+0900 AudioPlayer[50276:2769503] [avae] AVAEInternal.h:109 [AVAudioEngineGraph.mm:1389:Initialize: (err = AUGraphParser::InitializeActiveNodesInOutputChain(ThisGraph, kOutputChainOptimizedTraversal, *GetOutputNode(), isOutputChainActive)): error -10868
このようなエラーが出た場合はkAudioUnitErr_FormatNotSupported なので、formatの指定が足りていません。
AVAudioEngine.connect(_:to:format:) でformatを正しく設定すると解消されます。
formatの指定がなくても動く時は動くのですが、指定した方が無難です。正しく指定しないとこれまたエラーになるので難しいのですが。
mPtrState == kPtrsInvalid is false
2019-12-22 17:37:24.649149+0900 AudioPlayer[50304:2785651] AUBuffer.h:61:GetBufferList: EXCEPTION (-1) [mPtrState == kPtrsInvalid is false]: “”
このようなログが大量に吐かれた場合はkAudioUnitProperty_SetRenderCallback
にcallbackを指定していない時なので、なんらかのcallbackを指定すると解消されるようです。
音が断続的になる
次の記事に書きました
参考文献