Streaming — real-time pacing¶
By default wfmgen emits as fast as the CPU allows — fs is only metadata (the
BLUE xdelta, the ZMQ header). Add --realtime to throttle the output to
fs, so blocks leave on an epoch + n/fs schedule — mimicking a hardware
sample clock feeding the sink. This is what you want when a downstream consumer
expects samples to arrive at the real rate (a live spectrum display, an SDR
playback emulation):
# Stream QPSK to a live receiver at the true 1 MS/s, not as fast as possible
wfmgen --type qpsk --fs 1e6 --sps 8 --continuous --realtime \
--output zmq://tcp://*:5555
The schedule is drift-free: each deadline is recomputed from the cumulative
sample count against a fixed epoch, so sleep jitter never accumulates — the
long-run rate is exactly fs. Pacing does not alter the samples; a file
written with and without --realtime is byte-identical.
If the producer can't keep up (a block takes longer than its N/fs period — an
underrun), wfmgen keeps the absolute timeline and prints a summary to stderr
at exit (wfmgen: 3 underrun(s) — worst 1.2 ms behind real time). Use
--realtime-resync instead to re-anchor the clock to "now" on each underrun,
staying near real time going forward at the cost of an inserted gap.
Software pacing is average-rate, not sample-accurate
On a non-realtime OS you get a drift-free average rate with bounded
per-block jitter, never true sample-clock fidelity. Keep blocks large enough
that the period N/fs comfortably exceeds scheduler jitter, and let the
consumer's buffer absorb the rest.
The same clock in Python — SampleClock¶
The same C core is exposed as SampleClock, which paces and timestamps a stream
against an ideal fs-Hz clock — throttle a producer to real time and tag blocks
with their ideal timestamp:
from doppler.wfm import Composer, SampleClock, ZmqSink
comp = Composer(type="qpsk", sps=8, continuous=True)
clk = SampleClock(fs=1e6)
with ZmqSink("tcp://0.0.0.0:5555") as sink:
while True:
blk = comp.execute(4096)
ts = clk.stamp() # ideal ns timestamp of this block
sink.send(blk, fs=1e6, fc=0.0)
clk.pace(len(blk)) # sleep to epoch + n/fs (GIL released)
The schedule is drift-free (deadlines come from the cumulative sample count, not
summed sleeps); underruns are counted in clk.underruns / clk.max_lateness,
and SampleClock(fs, resync=True) re-anchors to "now" on each underrun.
SampleClock and ZmqSink are POSIX-only. See the
Python API for the full class surface.