AVAudioEngineでエコキャン実装 (with ManualRendering) その2
前回の続き。
前回の実装では周期的に音が断続的になる不具合がありました。
ManualRecordingでない場合は、この現象が発生していなかったので、どこかでバッファが枯渇しているのでは? と当たりをつけて修正していきます。
AVAudioIONodeInputBlockで要求されるフレーム数は常に同じではない
static OSStatus AudioRecorderRenderCallback(void* _Nonnull inRefCon, AudioUnitRenderActionFlags* _Nonnull ioActionFlags,
const AudioTimeStamp* _Nonnull inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* _Nullable ioData) {
上記関数のinNumberFrames
は毎回1024
で呼び出されていました。
BOOL success = [engine.inputNode setManualRenderingInputPCMFormat:self.processingFormat inputBlock:^const AudioBufferList * _Nullable(AVAudioFrameCount inNumberOfFrames) {
// AudioUnitRenderで読み込む先のbufferを指定する
return context->pInputAudioBufferList;
}];
一方で、ここのinNumberOfFrames
は基本1024
であるものの、周期的に1023
や1025
が登場するようでした。
毎回AudioRecorderInputCallback
でreadした分をそのまま返していたので、要求されたフレーム数に関係なく1024
フレーム分を返していたことになります。
てっきり同じ回数呼び出されるので同じフレーム数だろうと思っていましたが、ダウンサンプリングしているせいか割り切れなかった分の誤差が発生しているようです。
誤差程度のずれしか発生していないので、以下のようなバッファを保持することで対応します。

static OSStatus AudioRecorderInputCallback(void* _Nonnull inRefCon, AudioUnitRenderActionFlags* _Nonnull ioActionFlags,
const AudioTimeStamp* _Nonnull inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* _Nullable ioData) {
// 省略
// mDataに独自管理しているバッファの書き込み箇所のポインタを代入する
// バッファサイズが足りない場合にバッファを超えないように制限するのを忘れずに.
inputAudioBufferList->mBuffers[0].mData = context->pRecorderBuffer->pointee + context->pRecorderBuffer->frameCount * context->pStreamBasicDescription->mBytesPerFrame;
error = AudioUnitRender(context->audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, inputAudioBufferList);
// 省略
}
- (BOOL)connectTo:(AVAudioEngine* _Nonnull)engine {
// 省略
BOOL success = [engine.inputNode setManualRenderingInputPCMFormat:self.processingFormat inputBlock:^const AudioBufferList * _Nullable(AVAudioFrameCount inNumberOfFrames) {
// バッファに書き込まれているフレーム数と要求フレーム数の小さい方を使用する.
// 要求フレームに満たない場合はAVAudioEngineがドロップしてくれるのであまり気にせず要求フレーム数の内、読み込めるだけを返すようにする.
UInt32 readNumberOfFrames = MIN(inNumberOfFrames, context->pRecorderBuffer->frameCount);
// AudioUnitRenderに渡したAudioBufferListを使い回す
AudioBufferList* audioBufferList = context->pInputAudioBufferList;
// 読み込み開始位置とサイズを代入する
audioBufferList->mBuffers[0].mDataByteSize = readNumberOfFrames * context->pStreamBasicDescription->mBytesPerFrame;
audioBufferList->mBuffers[0].mData = context->pRecorderBuffer->pointee;
// 読み込み済みフレーム数を保持しておく
context->pRecorderBuffer->readFrameCount += readNumberOfFrames;
return audioBufferList;
}];
// 省略
}
static OSStatus AudioRecorderRenderCallback(void* _Nonnull inRefCon, AudioUnitRenderActionFlags* _Nonnull ioActionFlags,
const AudioTimeStamp* _Nonnull inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* _Nullable ioData) {
// 省略
AVAudioEngineManualRenderingBlock renderingBlock = (__bridge AVAudioEngineManualRenderingBlock)(context->renderingBlock);
AVAudioEngineManualRenderingStatus renderingStatus = renderingBlock(inNumberFrames, ioData, &error);
// ここで読み込みフレーム分を捨てて、バッファを詰める処理を行う
context->pRecorderBuffer->frameCount -= context->pRecorderBuffer->readFrameCount;
size_t readByteSize = context->pRecorderBuffer->readFrameCount * context->pStreamBasicDescription->mBytesPerFrame;
size_t restByteSize = context->pRecorderBuffer->frameCount * context->pStreamBasicDescription->mBytesPerFrame;
memmove(context->pRecorderBuffer->pointee, context->pRecorderBuffer->pointee + readByteSize, restByteSize);
context->pRecorderBuffer->readFrameCount = 0;
if (renderingStatus != AVAudioEngineManualRenderingStatusSuccess) {
return error;
}
return noErr;
}
これで音が断続することはなくなったものの、AVAudioEngineManualRenderingBlock
の呼び出しの中では何が起きているのかが謎に包まれていて消化不良感が否めない……
記事が気に入ったらチップを送ることができます!
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: )