soranoba
soranoba Author of soranoba.net
programming

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であるものの、周期的に10231025が登場するようでした。
毎回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の呼び出しの中では何が起きているのかが謎に包まれていて消化不良感が否めない……

(Updated: )

comments powered by Disqus