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
- 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
is still resident; anything older has been overwritten.
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:
- Block transfers. Samples move in chunks of thousands, not one by one - each transfer is one or two of the processor's fastest block-copy operations (two when the chunk straddles the wrap point). The writer claims the ring roughly 30 times per second for tens of microseconds, instead of hundreds of thousands of times.
- Readers don't hold the ring while copying. A read first takes an instantaneous note of w - that is the only moment of coordination - and then copies its window while the writer keeps running. The writer could only disturb the copy after producing C − N further samples (many seconds), while the copy itself completes in milliseconds. The one consumer for which this margin can vanish - a cursor lagging almost a full ring - re-validates after the copy; see overrun.
The three ways to read
| Read mode | Semantics |
|---|---|
| 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 w − C 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:
- "Show me now" consumers - the oscilloscope, the measurement table, save-to-file - use the latest-window reads. No state, nothing to fall behind.
- Stream consumers - above all the FFT's coherent averaging - need every frame to start exactly where the previous one ended. Coherent averaging aligns frames by their absolute position in the stream; if the position were taken from "whenever the worker woke up", the frame spacing would jitter and the averaged fundamental would smear. So each such consumer owns a cursor r: a read returns the samples starting at r and advances it, turning the ring into that consumer's private gap-free pipeline. The amount waiting at the cursor is simply
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,
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:
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
- A consumer can request a frozen copy - a standalone duplicate of the ring's current contents with no live writer. This is how the oscilloscope keeps showing the last captured frame after Record stops, while the shared device keeps running for other views.
- The same ring + cursor structure also serves loaded signal files: the loader fills a private buffer once, and the consumers read it through exactly the same cursors - the views cannot tell a file from a live capture.
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:
- Join / leave counting. The first view to record opens the device (with the configured rate and depth), creates the 22 s ring and starts the capture thread. Further views just join the running session; when the last one leaves, the device closes. Scope and FFT can therefore start and stop recording in any order - the device stays open exactly as long as someone needs it.
- One stream, private positions. Every join hands back a fresh cursor over the same ring - consistent data for all, independent progress for each.
- Event-driven consumers. After each block lands in the ring, the session broadcasts a "new data" notification. Scope repaints and FFT analysis ticks ride on these notifications instead of timers, so the views are exactly as fast as the data - never faster, never lagging a polling interval behind.
- Conversion at the entrance. The session converts the raw integer samples to normalised values, formula (1) of the audio-backend chapter, in one block operation per chunk - everything downstream already speaks the same unit.
References
- Circular (ring) buffers in general - Wikipedia: Circular buffer.
- The underlying coordination pattern - Wikipedia: Producer-consumer problem.
◀ Audio backend · Theory of operation · next: Signal generator ▶