Field Guide · concept

Also known as: push vs pull API, callback vs blocking read

Callback vs stream API is the choice between two ways an SDR driver hands samples to your program: a callback (push) model, where the driver calls a function you registered each time a buffer of samples is ready, versus a stream (pull) model, where your code calls a blocking read-style function to fetch the next samples when it wants them.1 Every SDR library exposes one, the other, or both, and the pattern you build around shapes threading, buffering, and how you handle overruns.

callback (push): driver drives the timing driver calls you your callback stream (pull): your code drives the timing your loop read() driver samples
In the push model the driver's own thread invokes your callback whenever a buffer fills; in the pull model your thread blocks in a read call and the driver hands back samples on request.

How it works

In the callback model you register a function and start the stream; the driver spawns (or borrows) a thread that, each time a USB or network buffer of samples arrives, invokes your function with a pointer to that buffer. Control is inverted — the driver decides when your code runs, and it runs in the driver’s thread context. The iron rule is that a callback must return fast: it runs on the thread that also has to service the next buffer, so any heavy DSP done inline stalls the driver and causes an overrun. The correct pattern is to copy or hand off the samples into a ring buffer and return immediately, letting your own worker threads do the real processing.

In the stream model you own the loop. Your thread calls a blocking readStream / read_samples function that returns the next N samples (or blocks until they are available), and you process them and loop. Control stays with your code, which many people find easier to reason about — it reads like ordinary sequential I/O — and it composes naturally with a threading model you design yourself. The driver still buffers internally, and if your loop is too slow to keep calling read, that buffer overflows and you drop samples, so the same overrun discipline applies, just detected at the read call instead of inside a callback.

Key differences that drive the design:

  • Who controls timing. Callback: the driver. Stream: your loop. This decides where back-pressure and pacing live.
  • Thread context. Callback code runs on the driver’s thread and must be short and non-blocking; stream code runs on your own thread and may block.
  • Error surfacing. Streams return an overrun status from the read call; callbacks usually signal it out-of-band, so you must watch for it.
  • Composability. A stream API drops cleanly into a language’s normal blocking-I/O and concurrency idioms; a callback often needs an adapter to bridge into them.

In practice

The two are easy to convert between, and robust programs usually normalize to one internal model. A common bridge is to wrap a callback API in a small shim that pushes each delivered buffer into a ring buffer, then expose a blocking read on the other side — turning push into pull so the rest of the application sees a clean stream. This is exactly why libraries differ: librtlsdr is fundamentally callback-driven (rtlsdr_read_async calls your function per buffer), while SoapySDR presents a unified readStream pull interface across many devices. Whichever the hardware offers, the goal is the same — get samples off the driver’s thread and into your own buffered pipeline before doing real work, so a slow DSP stage applies back-pressure or drops deliberately rather than corrupting the driver’s timing.

Relevance to SDR

This choice is the first thing you meet when writing SDR software, because it is the seam between the vendor driver and your DSP. GopherTrunk faces it at every source: some device back-ends are callback-based and others stream-based, so it adapts each into a consistent internal sample-stream feeding its decode chain, and its file/replay source is naturally a pull stream. Framing this honestly, the pattern is not GopherTrunk-specific — it is universal to SDR applications, and understanding it explains why nearly every real receiver puts a ring buffer immediately behind the source: it decouples the driver’s delivery model from the program’s processing model, so a callback that must return in microseconds and a decode loop that may pause for milliseconds can coexist without dropping samples.

Sources

  1. Callback (computer programming) — Wikipedia, on inversion of control and registered functions invoked by a driver or framework. 

See also