环形缓冲区用于在各方(通常在不同进程中)之间传递音频,从而实现并发的异步数据访问,而无需使用锁。这种模式之所以有效,是因为各方都了解哪些缓冲区可以安全访问,以及这些缓冲区会随时间发生哪些变化。
以下说明介绍了音频数据帧如何从一方移动到另一方。下面举两个例子详细说明:(1) 音频从客户端到驱动程序的移动(播放到相应驱动程序的硬件),以及 (2) 音频从驱动程序到客户端的移动(从相应驱动程序的硬件录制)。 不过,环形缓冲区也可用于在两个应用级客户端之间或两个驱动程序之间传送音频。
有些音频硬件可以在没有软件(例如基于主机的驱动程序)参与的情况下将数据传输到系统内存/从系统内存传输数据;其他音频硬件则通过在硬件本身上运行的软件(例如 DSP 固件)来实现此目的。不过,为了保持一致性,我们今后会将音频数据的这种移动称为由“驱动程序”完成。如需详细了解这种区别,请参阅本文档末尾的“硬件与软件”部分。
环形缓冲区用户无需使用互斥锁或其他主动同步机制,因为它们共享三条重要信息。第一种是环形缓冲区本身的内存边界。第二种是必须生成和消耗音频的速率;此速率由 CreateRingBuffer
命令中指定的格式定义。第三,双方必须就环形缓冲区的开始时间达成共识。
在环形缓冲区未启动时,时间对环形缓冲区的状态没有影响。当环形缓冲区启动时,会有一个环形缓冲区位置以预定义格式设置的恒定速度在环形缓冲区中持续移动。根据定义,在“开始时间”,此环形缓冲区位置“R”从环形缓冲区的开头(即帧 0)开始。
在环形缓冲区启动期间,驱动程序在单次 I/O 操作中可进行的数据传输量会受到限制。对于播放(驱动程序为使用方,客户端为生产方),驱动程序在传输中消耗的音频帧可以高达 driver_transfer_bytes
。对于捕获(驱动程序为 Producer,客户端为 Consumer),驱动程序在传输中生成音频帧,传输大小可高达 driver_transfer_bytes
。
这些驱动程序数据传输意味着,环形缓冲区中始终有一部分对客户端来说是不安全的(如果环形缓冲区用于捕获,则对客户端来说是不安全的)。此不安全缓冲区区域的一侧由当前环形缓冲区位置“R”定义,另一侧由“安全指针”位置定义。根据环形缓冲区是用于播放还是捕获,这分别是“生产者写入的安全帧位置”('P') 或“消费者读取的安全帧位置”('C')。在下图中,这些指针分别标记为“R”“P”“C”。
对于播放,客户端不得在此时写入“R”和“P”之间的区域。对于捕获,在相应时间段内不得读取“C”和“R”之间的区域。
环形缓冲区启动后,这些指针会开始以固定速率移动。'R' 从 RingBuffer::Start
返回的 start_time
开始移动,从较低地址到较高地址,并在到达环形缓冲区的末尾时立即重新从开头开始。通过这种“R”“P”和“C”的移动,消费者可以在适当的时间段过去后安全地读取之前由生产者写入的环形缓冲区内容。
为了使音频通过环形缓冲区,生产者必须在消费者传输发生之前写入数据。相反,消费者必须在生产者转移发生后读取数据。因此,在播放期间定义的“P”始终领先于“R”,而在拍摄期间定义的“C”始终落后于“R”。换句话说,“P”是指比“R”所指的帧更早的帧,“C”是指比“R”所指的帧更晚的帧。在通过“R”引用给定帧之前,会先通过“P”引用该帧;在通过“C”引用该帧之前,会先通过“R”引用该帧。在下图中,环形缓冲区帧 0 位于左侧,环形缓冲区位置从左向右移动。因此,从左向右看,我们预计图表会显示“C”,然后是“R”,然后是“P”(取模环绕效应)。
播放
在启动环形缓冲区之前,播放客户端可以安全地写入任何环形缓冲区位置。因此,尚未定义“P”。
Ring Buffer
+-----------------------------------------------------------------------------+
[<-- safe to write -->)
[ (to pre-populate the ring buffer before starting it) )
+-----------------------------------------------------------------------------+
0=R 0
为了尽快播放音频,客户端应将音频的第一帧写入到环形缓冲区的开头,这样当环形缓冲区启动时,驱动程序会首先读取这些帧。如果客户端有足够的音频可用(可能整个音频文件都可用),则可以选择在开始播放之前预先填充整个环形缓冲区。其他客户端将音频作为实时流接收;这些客户端仍然可以将音频预先填充到铃声缓冲区的开头,但必须写入超过 driver_transfer_bytes
的音频,因为在 Start
时,驱动程序可能会立即从铃声缓冲区中消耗这么多数据。这意味着,客户端必须在驱动程序读取音频之前(在“P”到达之前)继续写入来自该帧(以下标记为“s”)的音频。
Ring Buffer
+-------------------------+---------+-----------------------------------------+
[<-- Pre-populated by the client -->) not yet written by the client )
[< driver_transfer_bytes >) )
+-------------------------+---------+-----------------------------------------+
0=R s 0
如果客户端无法预先填充足够的音频,则应从偏移量处开始音频,而不是从环形缓冲区的开头开始。这依赖于 VMO 的清零内容成为驱动程序读取的第一个音频。与上文类似,此偏移量(同样称为“s”)必须足以让客户端在驱动程序使用音频帧之前提供后续音频帧。例如:
Ring Buffer
+-------------------------+---------+-----------------------------------------+
[ Offset [<-- Pre-populated -->) not yet written by the client )
[< driver_transfer_bytes >) )
+-------------------------+---------+-----------------------------------------+
0=R s 0
环形缓冲区启动后,客户端在“R”和“P”之间向环形缓冲区写入数据是不安全的,因为这表示数据已在使用中(可能已被驱动程序消耗)。客户端可以安全地写入环形缓冲区的其余部分(介于“P”和“0/R”之间)。
与往常一样,客户端绝不应写入过于接近“P”的位置,因为“P”是一个瞬时假设指针,即使在执行单条 CPU 指令的延迟期间也可能会前进。客户端的有效“可安全写入”区域始终在变化,因为“P”在不断移动。因此,客户端应提前(在较高的内存地址)写入,以便始终有足够的时间在“P”之前写入更多数据。
这是播放环形缓冲区的状态(在开始播放时):
Ring Buffer
+-------------------------+---------------------------------------------------+
[<-- unsafe to write -->[<-- safe to write (not yet -->)
[< driver_transfer_bytes >[ consumed by the driver) )
+-------------------------+---------------------------------------------------+
0=R P 0
随着时间的推移,驱动程序会以 CreateRingBuffer
中指定的速率读取数据,每次读取的数据块大小不超过 driver_transfer_bytes
。许多驱动程序使用“乒乓”模式,即每次读取分配的环形缓冲区区域的一半,以便为这些读取操作安全地发生留出时间。无论驱动程序传输的大小如何,位置指针和安全指针(“R”和“P”)都会以相同的速率向右移动,但会平稳移动。因此,“不安全写入”区域会逐渐在环形缓冲区中移动,同时保持等于 driver_transfer_bytes
的恒定大小。因此,经过一段时间后,我们现在有:
Ring Buffer
+------------+-------------------------+--------------------------------------+
[<-- safe -->[<-- unsafe to write -->[<-- safe to write (not yet -->)
[ to write [< driver_transfer_bytes >[ consumed by the driver) )
+------------+-------------------------+--------------------------------------+
0 R P 0
之后,在“R”之前,“P”会环绕环形缓冲区。请注意,从 0 到“P”的区域加上从“R”到环形缓冲区末尾的区域,总共为 driver_transfer_bytes
:
Ring Buffer
+---------------+--------------------------------------------------+----------+
[<-- unsafe -->[<-- safe to write (to overwrite -->[<-unsafe->)
[ransfer_bytes >[ already-consumed data [< driver_t)
+---------------+--------------------------------------------------+----------+
0 P R 0
在稳态下(即进程已环绕环形缓冲区),任何大于或等于“P”(上限为“R + ring_buffer_size”)的帧都可以安全地供客户端写入。换句话说,考虑到环形缓冲区回绕,生产者可以安全地写入范围 [0, R) + [P, ring_buffer_size),也可以写入范围 [P, R),具体取决于“R”相对于环形缓冲区回绕点的位置 - 上图或(更常见)下图:
Ring Buffer
+--------------------------+-------------------------+------------------------+
[<-- safe to write -->[<-- unsafe to write -->[<-- safe to write -->)
[ [< driver_transfer_bytes >[ )
+--------------------------+-------------------------+------------------------+
0 R P 0
请注意边界要求:“对于生产者而言不安全的写入”区域为 [R, P),因此生产者无法安全地写入位置“R”(相当于“R + ring_buffer_size”,即生产者高水位位置)。同样,“生产者可安全写入”区域为 [P, R)(环绕),因此消费者无法安全地读取位置“P”。
但在实践中,任何一方都无法安全地访问该精确帧。 帧指针位置“P”和“R”是理论上的瞬时位置。当驱动程序从“R”读取时,该指针会略微移动,导致该位置不适合读取;当客户端写入“P”时,该指针会略微移动,导致该位置不适合写入。生产者和消费者必须始终在“安全”指针位置之前保持一定程度的安全填充。
驱动程序指定的 driver_transfer_bytes
值对于确保客户端不会写入驱动程序仍在主动读取的内存至关重要。采用上述“乒乓”模式时,驱动程序会为 driver_transfer_bytes
指定一个值,该值是实际传输大小的两倍。实际上,它会反映提供额外安全边衬区时长的内部双缓冲区的尺寸。
正在录制
在录制期间,客户端只能安全地读取驱动程序未同时写入的环形缓冲区部分。在捕获开始之前,驱动程序尚未写入任何内容供客户端读取。
在捕获开始时(由 RingBuffer::Start
报告),驱动程序无法立即将帧传输到环形缓冲区,因为这些帧尚未获取。驱动程序必须先累积足够的帧以进行传输,然后才会将该数量的帧移动到从帧“0”开始的环形缓冲区。许多驱动程序使用双缓冲(或“乒乓”)模式,在每次传输中传输一半的缓冲量。由于在“开始”时,客户端尚无音频帧可供读取,因此“C”实际上是未定义的。不过,不妨考虑一个位置“b”(将变为“C”)。此“b”帧比“R”帧滞后一个固定偏移量,尚未到达帧位置 0。以下是环形缓冲区在启动时的状态:
Ring Buffer
+---------------------------------------------------+-------------------------+
[<-- safe to read (but empty, not yet written by driver) -->)
[ [< driver_transfer_bytes >)
+---------------------------------------------------+-------------------------+
0=R b 0
在环形缓冲区启动后但在“R”前进 driver_transfer_bytes
之前,客户端尚无法安全地读取任何新捕获的帧,因为这些帧可能尚未传输到环形缓冲区中。虽然“R”在前进,但驱动程序可能尚未将任何内容传输到缓冲区中。采用“乒乓”模式时,驱动程序会等待其内部缓冲区的前半部分已满,然后将其内容转移到环形缓冲区中;在转移过程中,内部缓冲区的另一半仍可安全地接收后续帧。
实际捕获到环形缓冲区中的音频量会随着每次驱动程序传输而变化,因此它会以“块状”方式在环形缓冲区中移动。相比之下,根据定义,“R”和“C”将以完全平滑的方式移动;它们保证始终绑定到最近捕获的实际帧所在的位置。
此时,由于“R”尚未前进 driver_transfer_bytes
,“C”实际上仍未定义。标记“b”继续前进,与“R”的距离保持固定偏移量,但仍未达到 0:
Ring Buffer
+--------------+--------------------------------------------------+-----------+
[<-- unsafe -->[<-- empty, not yet written by driver -->)
[ansfer_bytes >) [< driver_tr)
+--------------+--------------------------------------------------+-----------+
0 R b 0
一旦环形缓冲区位置“R”前进了正好
driver_transfer_bytes
,驱动程序就保证至少已将初始音频帧传输到环形缓冲区中。采用“乒乓”模式时,驱动程序之前已将其前半部分(“乒”)缓冲区传输到环形缓冲区,而其后半部分(“乓”)缓冲区刚刚填满,现在可以写入环形缓冲区。位置“b”已到达环形缓冲区的开头,因此现在定义了“C”,并开始以与“R”相同的速率平稳前进(由环形缓冲区的帧速率和采样格式决定)。因此,在这一时刻,我们有:
Ring Buffer
+-------------------------+---------------------------------------------------+
[<-- unsafe -->[<-- empty, not yet written by driver -->)
[< driver_transfer_bytes >[ )
+-------------------------+---------------------------------------------------+
0=C R b=0
随着环形缓冲区位置“R”进一步推进,客户端可以安全地读取“0”和“C”之间的区域中的帧。客户端从“C”读取数据到“R”是不安全的,因为驱动程序同时正在写入这些位置。此区域在环形缓冲区中不断前进,始终保持 driver_transfer_bytes
的大小。从概念上讲,环形缓冲区现在处于以下状态:
Ring Buffer
+--------------------+-------------------------+------------------------------+
[< safe to read >[<-- unsafe to read -->[<-- empty, not yet -->)
[newly-captured audio[< driver_transfer_bytes >[ written by driver )
+--------------------+-------------------------+------------------------------+
0 C R 0
之后,在“C”之前,“R”会环绕环形缓冲区。请注意,从 0 到“R”的区域加上从“C”到环形缓冲器末尾的区域,总共为 driver_transfer_bytes
。
与往常一样,客户端绝不应读取过于接近“R”的值,因为“R”是一个瞬时假设指针,即使在单个 CPU 指令的延迟期间也可能会前进。客户端的有效“可安全读取”区域始终在变化,因为“R”在不断移动。因此,客户端应提前读取(在较高的内存地址处),以便始终有足够的时间在“R”之前读取更多数据。
这是环形缓冲区在首次环绕后某个时间的状态:
Ring Buffer
+-----------+--------------------------------------------------+--------------+
[<--unsafe->[<-- safe to read -->[<-- unsafe -->)
[fer_bytes >[ (captured audio) [< driver_trans)
+-----------+--------------------------------------------------+--------------+
0 R C 0
在稳定状态下(即进程已环绕环形缓冲区),任何小于“C”(最多为“R - ring_buffer_size”)的帧都可以安全地供客户端读取。重新表述一下,并考虑环绕,消费者可以安全地读取范围 [0, C) + [R, ring_buffer_size),或者交替读取范围 [R, C) - 具体取决于“R”相对于环绕点的位置 - 请参阅上图或(更常见)下图:
Ring Buffer
+--------------------------+-------------------------+------------------------+
[<-- safe to read -->[<-- unsafe -->[<-- safe to read -->)
[ [< driver_transfer_bytes >[ )
+--------------------------+-------------------------+------------------------+
0 C R 0
请注意边界要求:“不安全供消费者读取”区域为 [C, R),因此消费者无法安全地读取位置“C”(消费者低水位帧位置)。同样,“可供消费者安全读取”的区域为 [R, C)(带环绕),因此生产者无法安全地写入位置“R”。
但在实践中,任何一方都无法安全地访问该精确帧。 帧指针位置“R”和“C”是理论上的瞬时位置。当驱动程序写入“C”时,该指针会略微移动,导致该位置不适合写入;当客户端从“R”读取时,该指针会略微移动,导致该位置不适合读取。生产者和消费者必须始终在“安全”指针位置之前保持一定程度的安全填充。
驱动程序指定的 driver_transfer_bytes
值对于确保客户端不会读取驱动程序仍在积极更新的内存至关重要。采用上述“乒乓”模式时,驱动程序会为 driver_transfer_bytes
指定一个值,该值是实际传输大小的两倍。实际上,它会反映其内部双缓冲区的尺寸,该缓冲区可提供额外的安全边衬区时长。
硬件与软件(或硬件传输与驱动程序处理和复制)
环形缓冲区数据帧可由音频硬件直接使用/生成:即 driver_transfer_bytes
可能直接映射到硬件 FIFO 块的大小,因为该 FIFO 块将决定预读或延迟的数据量的上限。请注意,如果 FIFO 缓冲区以传统的“高水位”方式使用(例如“乒乓”设计,其中任何时候只使用 FIFO 的一半 - 在 Start
时间首次填充整个 FIFO 后),则 driver_transfer_bytes
将设置为内部 FIFO 缓冲区的大小,如果使用“乒乓”模式,则该大小将是内部传输大小的两倍。即使使用较小的传输,如果使用 FIFO 的完整大小(例如,在填充最初为空的硬件 FIFO 时,达到 Start
),则 driver_transfer_bytes
必须设置为此 FIFO 缓冲区的完整大小。
环形缓冲区数据可能会被音频驱动程序软件(在概念上位于环形缓冲区和音频硬件之间)消耗/生成。在这种情况下,以播放为例,driver_transfer_bytes
预读量必须足够大,以便驱动程序根据客户端要求以 CreateRingBuffer
指定的速率和从 Start
的 start_time
派生的位置生成数据,从而保证不会出现未检测到的欠载。相反,对于捕获,driver_transfer_bytes
必须足够大,以便驱动程序在生成数据时保证不会出现欠载,这由 CreateRingBuffer
和 Start
确定。此外,预计在这些情况下,driver_transfer_bytes
将大于传输本身的大小,因为它还必须包括任何安全填充,以应对调度和执行此驱动程序处理过程中的延迟。