◀ Back to Theory of operation

Ring buffer & consumers

One capture device feeds several independent consumers - the oscilloscope paints "now", the FFT accumulates a gap-free stream, a save dialog grabs the last seconds. The structure that makes this work is a single shared ring buffer[1] with an independent read cursor per consumer, owned by a reference-counted capture session.

The ring

The ring holds the most recent 22 seconds of stereo capture as normalised values in [−1, +1] - enough for two full oscilloscope windows at the slowest 1 s/div × 10 divisions, plus margin so the trigger-position control can sweep the whole window without falling off the buffer. Its capacity in samples is

C = fs · 22 s(1)

- at 384 kHz about 8.4 million samples per channel (≈ 67 MB).

The only position the ring itself maintains is the write position w: the total number of samples ever captured. All addressing is done with such absolute sample indices - sample n means "the n-th sample since capture started", and the ring stores it at array index n mod C. At any moment exactly the span

[wC,  w)(2)

is still resident; anything older has been overwritten.

w mod C index 0 C − 1 older ... newer ... newest oldest (about to be overwritten) writes wrap around absolute sample index w w − C only this span is still resident in the ring

Why writer and readers never get in each other's way

The writer is the capture thread; the readers are the display and the FFT worker[2]. If a reader could stall the writer for even a few milliseconds, an exclusive-mode device would drop samples. Two measures keep the coordination cost negligible:

The three ways to read

Read modeSemantics
Latest window The most recent N samples, ending at the live w - "show me now". Inherently safe: it always reads the newest data.
Window ending at a chosen instant A window ending at any absolute index - the oscilloscope's scroll-back-in-time. Parts of the request older than wC are clipped.
Forward from the cursor The next contiguous samples after the consumer's own cursor - the gap-free stream (next section).

The read cursor

Consumers fall into two camps, and the cursor exists for the second one:

t w latest window Scope: reads the latest window every paint - it always ends at w, jumps forward between paints, can never fall behind. FFT: w frame k frame k+1 frame k+2 ... cursor r consuming, contiguous reads: every frame starts exactly where the previous one ended; the hop between frames is exact and uniform, which coherent averaging requires. Unread backlog stays readable.
a = wr(3)

Overrun: when a consumer falls behind

A cursor consumer can lag - a very large FFT, a heavily loaded machine. The writer does not wait for anyone (the freshest data must always win), so if the lag ever exceeds the ring's whole capacity,

r < wC(4)

then samples the cursor never read are gone, and the stream has a hole. Silently continuing would hand the consumer a window torn across a discontinuity - poison for coherent averaging. Instead the read reports an explicit overrun and delivers nothing:

resident span = C (22 s) w r lapped -> overrun data here is gone - overwritten before it was read

On overrun the consumer discards its accumulated state, re-anchors the cursor at the newest sample and starts fresh - the FFT view surfaces this as the blinking capture re-sync warning. One subtlety: because readers copy without holding the ring, a cursor lagging almost a full ring could be lapped during its copy. The read therefore checks condition (4) again after copying, against the new w, and reports overrun rather than returning torn data.

Frozen copies and file-fed buffers

The capture session: who owns the device

The input device and the live ring have exactly one owner - the capture session. Views never open the device themselves; they ask the session for access and get their own read cursor back:

Scope (Record) join / leave FFT (Record) join / leave Capture session first join -> device opens further joins -> share the stream last leave -> device closes converts PCM -> [−1, +1) Input device open exclusively Ring buffer (shared) one stream, every consumer reads it each consumer receives its own read cursor "new data" notification per block - views update event-driven, no polling

References

  1. Circular (ring) buffers in general - Wikipedia: Circular buffer.
  2. The underlying coordination pattern - Wikipedia: Producer-consumer problem.

◀ Audio backend · Theory of operation · next: Signal generator ▶