在音频单元渲染周期中处理不同数量的样本

Handle Varying Number of Samples in Audio Unit Rendering Cycle

这是在引入 iPhone 6s 和 6s+ 后出现在我的应用程序中的问题,我几乎可以肯定这是因为新型号的内置麦克风卡在录音48kHz(您可以阅读更多相关内容 here)。澄清一下,这对于我测试过的以前的 phone 模型从来都不是问题。我将在下面根据 phone 模型逐步介绍我的音频引擎实现和不同点的不同结果。

所以这就是发生的事情 - 当我的代码在以前的设备上运行时,我在 AVCaptureDevice 返回的每个 CMSampleBuffer 中获得了一致数量的音频样本,通常是 1024 个样本。我的音频单元图的渲染回调为 1024 帧提供了 space 的适当缓冲区。一切都很好,听起来也很棒。

然后 Apple 不得不去做这个该死的 iPhone 6s(开玩笑,这太棒了,这个错误刚刚让我头疼)现在我得到了一些非常不一致和令人困惑的结果。 AVCaptureDevice 现在在捕获 940 或 941 个样本之间变化,渲染回调现在开始在第一次调用时为 940 或 941 个样本帧创建一个 space 的缓冲区,但随后立即开始增加它保留的 space随后调用最多 1010、1012 或 1024 个样本帧,然后停留在那里。它最终保留的 space 因会话而异。老实说,我不知道这个渲染回调是如何确定它为渲染准备了多少帧,但我猜它与渲染回调开启的音频单元的采样率有关。

无论设备是什么,CMSampleBuffer 的格式都以 44.1kHz 采样率出现,所以我猜在我从 AVCaptureDevice 接收 CMSampleBuffer 之前发生了某种隐式采样率转换在 6s 上。唯一的区别是 6s 的首选硬件采样率为 48kHz,而早期版本为 44.1kHz。

我读到,对于 6s,您必须准备好 space 返回不同数量的样本,但我上面描述的那种行为是否正常?如果是,如何调整我的渲染周期来处理这个问题?

下面是处理音频缓冲区的代码,如果您想进一步研究的话:

音频样本缓冲区 CMSampleBufferRefs, come in through the mic AVCaptureDevice 并被发送到我的音频处理函数,该函数对捕获的名为 audioBuffer

的 CMSampleBufferRef 执行以下操作
CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer(audioBuffer);
        
CMItemCount numSamplesInBuffer = CMSampleBufferGetNumSamples(audioBuffer);
AudioBufferList audioBufferList;
        
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(audioBuffer,
                                                            NULL,
                                                            &audioBufferList,
                                                            sizeof(audioBufferList),
                                                            NULL,
                                                            NULL,
                                                            kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
                                                            &buffer
                                                            );

self.audioProcessingCallback(&audioBufferList, numSamplesInBuffer, audioBuffer);
CFRelease(buffer);

这是将音频样本放入 AudioBufferList 并将其与样本数量和保留的 CMSampleBuffer 一起发送到我用于音频处理的以下函数。 TL;DR 以下代码设置音频图中的一些音频单元,使用 CMSampleBuffer 的格式设置输入的 ASBD,通过转换器单元、newTimePitch 单元和另一个转换器单元运行音频样本。然后,我使用从 CMSampleBufferRef 收到的样本数在输出转换器单元上启动渲染调用,并将渲染后的样本放回 AudioBufferList 中,随后写入电影文件,更多信息请参见下面的音频单元渲染回调。

movieWriter.audioProcessingCallback = {(audioBufferList, numSamplesInBuffer, CMSampleBuffer) -> () in

        var ASBDSize = UInt32(sizeof(AudioStreamBasicDescription))
        self.currentInputAudioBufferList = audioBufferList.memory
        
        let formatDescription = CMSampleBufferGetFormatDescription(CMSampleBuffer)
        let sampleBufferASBD = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription!)
        
        if (sampleBufferASBD.memory.mFormatID != kAudioFormatLinearPCM) {
            print("Bad ASBD")
        }
        
        if(sampleBufferASBD.memory.mChannelsPerFrame != self.currentInputASBD.mChannelsPerFrame || sampleBufferASBD.memory.mSampleRate != self.currentInputASBD.mSampleRate){
            
            
            // Set currentInputASBD to format of data coming IN from camera
            self.currentInputASBD = sampleBufferASBD.memory
            print("New IN ASBD: \(self.currentInputASBD)")
            
            // set the ASBD for converter in's input to currentInputASBD
            var err = AudioUnitSetProperty(self.converterInAudioUnit,
                kAudioUnitProperty_StreamFormat,
                kAudioUnitScope_Input,
                0,
                &self.currentInputASBD,
                UInt32(sizeof(AudioStreamBasicDescription)))
            self.checkErr(err, "Set converter in's input stream format")
            
            // Set currentOutputASBD to the in/out format for newTimePitch unit
            err = AudioUnitGetProperty(self.newTimePitchAudioUnit,
                kAudioUnitProperty_StreamFormat,
                kAudioUnitScope_Input,
                0,
                &self.currentOutputASBD,
                &ASBDSize)
            self.checkErr(err, "Get NewTimePitch ASBD stream format")
            
            print("New OUT ASBD: \(self.currentOutputASBD)")
            
            //Set the ASBD for the convert out's input to currentOutputASBD
            err = AudioUnitSetProperty(self.converterOutAudioUnit,
                kAudioUnitProperty_StreamFormat,
                kAudioUnitScope_Input,
                0,
                &self.currentOutputASBD,
                ASBDSize)
            self.checkErr(err, "Set converter out's input stream format")
            
            //Set the ASBD for the converter out's output to currentInputASBD
            err = AudioUnitSetProperty(self.converterOutAudioUnit,
                kAudioUnitProperty_StreamFormat,
                kAudioUnitScope_Output,
                0,
                &self.currentInputASBD,
                ASBDSize)
            self.checkErr(err, "Set converter out's output stream format")
            
            //Initialize the graph
            err = AUGraphInitialize(self.auGraph)
            self.checkErr(err, "Initialize audio graph")
            
            self.checkAllASBD()
            
        }
        
        self.currentSampleTime += Double(numSamplesInBuffer)
        
        var timeStamp = AudioTimeStamp()
        memset(&timeStamp, 0, sizeof(AudioTimeStamp))
        timeStamp.mSampleTime = self.currentSampleTime
        timeStamp.mFlags = AudioTimeStampFlags.SampleTimeValid
        
        var flags = AudioUnitRenderActionFlags(rawValue: 0)
        
        err = AudioUnitRender(self.converterOutAudioUnit,
            &flags,
            &timeStamp,
            0,
            UInt32(numSamplesInBuffer),
            audioBufferList)
        self.checkErr(err, "Render Call on converterOutAU")
        
    }

AudioUnitRender 调用到达输入转换器单元后调用的 Audio Unit Render Callback 如下

func pushCurrentInputBufferIntoAudioUnit(inRefCon : UnsafeMutablePointer<Void>, ioActionFlags : UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp : UnsafePointer<AudioTimeStamp>, inBusNumber : UInt32, inNumberFrames : UInt32, ioData : UnsafeMutablePointer<AudioBufferList>) -> OSStatus {

let bufferRef = UnsafeMutablePointer<AudioBufferList>(inRefCon)
ioData.memory = bufferRef.memory
print(inNumberFrames);

return noErr
}

Blah,这是一个巨大的脑洞,但我真的很感谢 任何 帮助。如果您需要任何其他信息,请告诉我。

通常,您可以通过将传入样本放入无锁循环 fifo 来处理缓冲区大小的微小变化(但输入和输出的采样率恒定),并且直到您从该循环 fifo 中删除任何样本块有一个全尺寸块加上潜在的一些安全填充来覆盖未来的尺寸抖动。

大小的变化可能与采样率转换器比率不是简单的倍数、所需的重采样滤波器以及重采样过程所需的任何缓冲有关。

1024 * (44100/48000) = 940.8

因此,速率转换可以解释 940 和 941 样本之间的抖动。如果硬件总是以 48 kHz 的固定速率发送 1024 个样本块,并且您需要尽快将该块重新采样到 44100 以进行回调,则转换后的样本的一小部分最终只需要在某些输出回调中输出.