AVAudioEngineでエコキャン実装 (with ManualRendering)
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を指定すると解消されるようです。
音が断続的になる
参考文献
- twilio/video-quickstart-ios
- 数少ないマニュアルレンダリング実装が公開されているので大変参考にしました
記事が気に入ったらチップを送ることができます!
You can give me a cup of coffee :)
Kyash ID: soranoba
Amazon: Wish List
GitHub Sponsor: github.com/sponsors/soranoba
PayPal.Me: paypal.me/soranoba
(Updated: )