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 positionw: 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
[w − C, w)(2)
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
a = w − r(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 < w − C(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:
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.