870920 Menu

SwingCoder之C++备忘录·9

流和缓冲

流:一堆数据(以字节为最小单位,比如:32位平台下,一个float数值占4个字节)从某处到另一处。可能是从内存某处到另一处,也可能是内存到硬件设备,硬件设备到内存,或者硬件设备经内存再到硬件设备。亦即:流就是不断改变存储位置,由此达彼的一堆数据。

计算机各硬件之间的处理速度差别较大,而且外部设备的处理速率相对恒定,而程序内部的处理速率则忽快忽慢,为了保证效率,往往使用缓冲。缓冲是处理流数据的重要技术,其实现原理是FIFO先进先出队列或循环队列,即内存中的一块相对固定的区域,该区域相当于一个“蓄水池”,临时性的存储进出数据。为防止填充和刷新缓冲区造成阻塞,往往使用双缓冲或多缓冲技术,即:填充缓冲区后,立即将此缓冲中的数据复制到另一个内部缓冲中,读取和处理期间,原始的缓冲区又开始填入新数据。缓冲数据的流入和流出,可同步顺序执行,也可异步并发执行。固定大小和进出比率一致的数据,一般采用同步执行(比如音视频数据),非固定大小或进出比率不一致的数据,比如MIDI,则往往采用异步执行。

FIFO缓冲有两种类型:输入缓冲和输出缓冲。输入缓冲接收外部设备传过来的数据(入队,直到被填满。一般不会出现被填满的现象,除非数据量大,而计算机速度不够快,资源不够用,提取和处理的速度慢于填充缓冲区的速度,此即“缓冲过载”)。输入缓冲中的数据由程序进行读取和处理(出队),程序将输入缓冲中的数据全部提取之后,临时性的停止处理,等待缓冲中进来新数据。

输出缓冲用于存储程序生成和压入的数据,供输出设备使用。不同于输入缓冲,输出缓冲通常都是填满后才被输出设备所用,而且一般不会出现“空”的现象(除非计算机速度慢,回填不及时,此即“缓冲欠载”)。

简单说,输入缓冲由外部输入设备以恒定的速率不断压入数据,程序以忽快忽慢的速率提取并处理这些数据(弹出),如果提取的不及时,输入设备传过来的新数据无处安置,就会丢失这些新数据。而输出缓冲由程序负责填充数据,填充时的速率忽快忽慢,输出设备以恒定的速率提取和处理这些数据,如果程序填充的不及时,输出设备就无数据可读,如果是播放音视频数据,就会断续或爆音。

更大的缓冲区可避免缓冲过载和欠载,但是,对DSP和音频处理来说,更大的缓冲区意味着更大的信号延迟。计算公式(理论值。该值总是小于实际延迟,也就是说,实际延迟时间往往大于公式计算出的结果):

 最大延迟(毫秒) = 1000 * 缓冲区的个数 * 缓冲区的大小 / 采样率

 最小延迟(毫秒) = 1000 * (缓冲区个数 – 1) * 缓冲区大小 / 采样率

 平均延迟(毫秒) = (最大延迟 + 最小延迟) / 2

选择合适的缓冲对音频程序来说很重要,一般做成可选项,由使用此程序的用户来设置。

为进一步解决缓冲进出、多个缓冲区复制数据所造成的阻塞问题,可使用多线程、多缓冲、宿主驱动和不间断回调等机制。多线程编程比较复杂而难以维护(但是,即使一个最简单的音频程序,也至少需要两个线程,前台显示与后台的音频处理),宿主驱动则依赖于操作系统,回调机制基于回调函数,使用起来难度小、成本低,通常是编程时优先考虑的方案。

出于性能和效率因素,缓冲在编程时通常以数组来代表,而不使用标准库中的数据结构。稍复杂的缓冲可使用二维数组,比如JUCE类库音频内核中最重要的AudioSampleBuffer,其本质就是一个二维float数组,外层数组代表音频采样的通道数,每个通道为一个数组(内层数组),所包含的元素是一个音频采样(float数值)。