C Examples¶
Standalone project¶
A minimal working project lives at
example-projects/standalone/.
It generates 4096 AWGN samples with dp_awgn() and prints empirical statistics.
The same example is also available as a one-liner Python script.
Get the code¶
The quickest path — no compiler required. The wheel bundles all native code; no system libraries needed.
Clone and build. This produces libdoppler.a, libdoppler.so, and
(with BUILD_PYTHON=ON) the Python extension in src/doppler/.
C — static linking¶
No runtime .so dependency. Recommended for embedded use and distribution.
C — dynamic linking¶
Links against libdoppler.so. The rpath is baked in so the binary
runs without setting LD_LIBRARY_PATH.
After cmake --install the find_package path picks up whichever
library variant was installed:
Python extension¶
Expected output (all paths):
samples : 4096
mean : 0.0237 + -0.0007i (expect ≈ 0)
std dev : 0.9847 (Re) 0.9858 (Im) (expect ≈ 1.0)
Consumer project — find_package¶
example-projects/standalone/ above links against a doppler build tree. For the
idiomatic installed-library setup — find_package(doppler) against a system
install or an extracted release tarball —
see example-projects/consumer/.
One CMakeLists.txt builds against both link targets:
doppler::doppler (shared) and doppler::doppler-static (the pure-C static
archive — links -lm and -lpthread, no C++ runtime). The
C Library install guide has the full find_package and
pkg-config commands.
Generating waveforms in-process¶
The wfmgen CLI is archived in the library as the callable
dp_doppler_wfmgen(argc, argv) (header doppler/wfm/wfmgen.h), so a C program can
produce the same captures without spawning a subprocess — see
Embedding the wfmgen generator.
LO — complex phasor generator¶
The LO type chains a 32-bit NCO with a 2¹⁶-entry sin/cos LUT to produce
CF32 IQ phasors at ~96 dBc SFDR.
Free-running IQ¶
#include <doppler/lo/lo_core.h>
#include <complex.h>
#include <stdio.h>
int main(void) {
dp_lo_state_t *lo = dp_lo_create(0.25); // quarter-rate tone
float complex out[8];
dp_lo_steps(lo, 8, out, 8);
for (int i = 0; i < 8; i++)
printf("out[%d]: %.3f + %.3fi\n", i, crealf(out[i]), cimagf(out[i]));
// out[0]: 1.000 + 0.000i
// out[1]: -0.000 + 1.000i (the 16-bit LUT leaves ~1e-8 residue)
// out[2]: -1.000 + -0.000i
// out[3]: 0.000 + -1.000i
// out[4]: 1.000 + 0.000i (repeats every 4 samples)
// ...
dp_lo_destroy(lo);
return 0;
}
FM modulation via control port¶
#include <doppler/lo/lo_core.h>
#include <complex.h>
#include <math.h>
int main(void) {
dp_lo_state_t *lo = dp_lo_create(0.1); // base freq f_n = 0.1
double ctrl[1024];
for (int i = 0; i < 1024; i++)
ctrl[i] = 0.002 * sin(2.0 * M_PI * 0.01 * i);
float complex out[1024];
dp_lo_steps_ctrl(lo, ctrl, 1024, out, 1024);
// base freq unchanged; reset restores clean phase
dp_lo_destroy(lo);
return 0;
}
AWGN — Additive White Gaussian Noise¶
One-shot (no persistent state)¶
#include <doppler/awgn/awgn_core.h>
#include <complex.h>
int main(void) {
float complex out[1024];
dp_awgn(0, 1.0f, 1024, out); /* seed=0, amplitude=1.0 — DP_OK (0) on success; DP_ERR_INVALID for a NaN, infinite or negative amplitude; DP_ERR_MEMORY (-6) on allocation failure */
return 0;
}
Stateful generator (streaming / reproducible replay)¶
#include <doppler/awgn/awgn_core.h>
#include <complex.h>
#include <stdio.h>
int main(void) {
dp_awgn_state_t *g = dp_awgn_create(42, 1.0f); /* seed, amplitude */
float complex buf[4096];
dp_awgn_generate(g, 4096, buf, 4096); /* fill buf */
/* Retune amplitude without disturbing RNG state */
dp_awgn_set_amplitude(g, 0.5f);
dp_awgn_generate(g, 4096, buf, 4096);
/* Deterministic replay: reset reseeds the RNG but keeps amplitude
0.5, so this is the first call's stream at half the amplitude */
dp_awgn_reset(g);
dp_awgn_generate(g, 4096, buf, 4096);
dp_awgn_destroy(g);
return 0;
}
Noisy carrier¶
#include <doppler/awgn/awgn_core.h>
#include <doppler/lo/lo_core.h>
#include <complex.h>
#include <stdio.h>
#define N 4096
int main(void) {
dp_lo_state_t *lo = dp_lo_create(0.1f);
dp_awgn_state_t *noise = dp_awgn_create(0, 0.3f); /* σ=0.3 per component */
float complex carrier[N], n[N], rx[N];
dp_lo_steps(lo, N, carrier, N);
dp_awgn_generate(noise, N, n, N);
for (size_t i = 0; i < N; i++)
rx[i] = carrier[i] + n[i];
printf("rx[0]: %.3f + %.3fi\n", crealf(rx[0]), cimagf(rx[0]));
dp_lo_destroy(lo);
dp_awgn_destroy(noise);
return 0;
}
NCO — raw phase accumulator¶
NCO exposes the bare uint32 phase accumulator — useful for driving
a polyphase resampler clock or generating carry events.
Raw uint32 phase + overflow carry¶
#include <doppler/nco/nco_core.h>
int main(void) {
dp_nco_state_t *nco = dp_nco_create(0.25, 0); // nmax=0 → raw [0, 2^32)
uint32_t phase[16];
uint8_t carry[16];
dp_nco_steps_u32_ovf(nco, 16, phase, carry, 16);
// carry fires at indices 3, 7, 11, 15 (once per full cycle)
dp_nco_destroy(nco);
return 0;
}
FIR filter¶
#include <doppler/fir/fir_core.h>
#include <complex.h>
#include <math.h>
#define N_TAPS 19
int main(void) {
// Windowed-dp_sinc low-pass filter (fc = 0.2 * fs) — real taps
float taps[N_TAPS];
int half = N_TAPS / 2;
for (int k = 0; k < N_TAPS; k++) {
int n = k - half;
double sinc = (n == 0) ? 1.0
: sin(M_PI * 0.2 * n) / (M_PI * 0.2 * n);
double win = 0.5 * (1.0 - cos(2.0 * M_PI * k / (N_TAPS - 1)));
taps[k] = (float)(sinc * win);
}
dp_fir_state_t *fir = dp_fir_create_real(taps, N_TAPS);
float complex in[1024], out[1024];
dp_fir_execute(fir, in, 1024, out);
dp_fir_destroy(fir);
return 0;
}
FFT¶
Each FFT instance holds its own plan — create once, reuse across calls. CF32 is ~2× faster than CF64 for the same transform length.
1D FFT (double precision)¶
#include "doppler/fft/fft_core.h"
#include <complex.h>
#include <math.h>
#include <stdio.h>
int main(void) {
const size_t N = 1024;
dp_fft_state_t *fft = dp_fft_create(N, -1, 1);
double complex in[N], out[N];
for (size_t i = 0; i < N; i++)
in[i] = cos(2.0 * M_PI * 10.0 * i / N) + 0.0 * I;
dp_fft_execute_cf64(fft, in, N, out, N);
printf("DC bin: %.4f + %.4fi\n", creal(out[0]), cimag(out[0]));
dp_fft_destroy(fft);
return 0;
}
1D FFT (single precision / CF32, ~2× faster)¶
#include "doppler/fft/fft_core.h"
#include <complex.h>
#include <math.h>
int main(void) {
const size_t N = 1024;
dp_fft_state_t *fft = dp_fft_create(N, -1, 1);
float complex in32[N], out32[N], out32b[N];
for (size_t i = 0; i < N; i++)
in32[i] = cosf(2.0f * M_PI * 10.0f * i / N) + 0.0f * I;
dp_fft_execute_cf32(fft, in32, N, out32, N); // out-of-place
dp_fft_execute_inplace_cf32(fft, in32, N, out32b, N); // copy in -> out32b, then transform
dp_fft_destroy(fft);
return 0;
}
2D FFT¶
#include "doppler/fft2d/fft2d_core.h"
#include <complex.h>
int main(void) {
dp_fft2d_state_t *fft2d = dp_fft2d_create(64, 64, -1, 1);
double complex in2d[64 * 64], out2d[64 * 64];
dp_fft2d_execute_cf64(fft2d, in2d, 64 * 64, out2d, 64 * 64);
float complex in32_2d[64 * 64], out32_2d[64 * 64];
dp_fft2d_execute_cf32(fft2d, in32_2d, 64 * 64, out32_2d, 64 * 64);
dp_fft2d_destroy(fft2d);
return 0;
}
Halfband decimator¶
2:1 decimation with a symmetric FIR. Input length must be even;
output length is exactly n_in / 2.
#include <doppler/HalfbandDecimator/HalfbandDecimator_core.h>
#include <complex.h>
#include <stdio.h>
#define N_TAPS 4
#define N_IN 32
/* Minimal 4-tap symmetric halfband coefficients. */
static const float H_FIR[N_TAPS] = { -0.2122f, 0.6366f, 0.6366f, -0.2122f };
int main(void) {
dp_HalfbandDecimator_state_t *dec = dp_HalfbandDecimator_create(H_FIR, N_TAPS);
float _Complex in[N_IN], out[N_IN / 2];
/* ... fill in[] with your signal ... */
/* Last argument is out's capacity, not the input count: a 2:1
decimator emits N_IN/2, which is exactly how out is sized. */
size_t n_out = dp_HalfbandDecimator_execute(dec, in, N_IN, out, N_IN / 2);
printf("output samples: %zu\n", n_out); /* 16 */
dp_HalfbandDecimator_destroy(dec);
return 0;
}
These commands run from a clone of the repository at the release you
installed; the make targets and the ./build/ binaries are not in the wheel.
Skip this if you already have one:
Build and run the full demo:
AGC — automatic gain control¶
The AGC drives output power to ref_db using a first-order loop filter.
dp_agc_step() processes one sample at a time. The loop filter is linear in
the dB domain, settling in roughly 1/(4*loop_bw) samples, but the power
detector inside the loop makes the object's settling level-dependent: a
quiet input settles more slowly, up to about 3x at small alpha
(agc_core.h; AGC design).
#include <doppler/agc/agc_core.h>
#include <complex.h>
#include <math.h>
#include <stdio.h>
#define N 6000
#define N_STEP 3000
#define F_TONE 0.02
int main(void) {
dp_agc_state_t *agc = dp_agc_create(
0.0, /* ref_db — target output power */
0.00125, /* loop_bw — noise bandwidth, cycles/sample */
0.02 /* alpha — power-detector EMA coefficient */
);
for (int n = 0; n < N; n++) {
double amp = (n < N_STEP) ? pow(10, -10.0/20) : pow(10, 10.0/20);
float _Complex x = (float)(amp * cos(2*M_PI*F_TONE*n))
+ (float)(amp * sin(2*M_PI*F_TONE*n)) * I;
float _Complex y = dp_agc_step(agc, x);
(void)y;
}
printf("gain_db = %.2f\n", agc->gain_db);
dp_agc_destroy(agc);
return 0;
}
Build and run the full demo (prints a convergence table and writes
agc_step_response.csv):
PUSH/PULL pipeline¶
Two threads in-process — producer pushes 100 batches of 1024 CF64 samples over the NATS JetStream PUSH/PULL work-queue tier; consumer receives and prints power.
#include <doppler/doppler.h>
#include <doppler/stream/stream.h>
#include <complex.h>
/* Producer thread — rate/freq travel in the header the send builds */
dp_push_t *ctx = dp_push_create("nats://127.0.0.1:4222/work", CF64);
if (dp_push_send_cf64(ctx, samples, 1024, 1e6, 0.0) != DP_OK) /* n, fs, fc */
fprintf(stderr, "send refused: %s\n", dp_ctx_last_error(ctx));
dp_push_destroy(ctx);
/* Consumer thread — recv hands back a library-owned message + header */
dp_pull_t *rx = dp_pull_create("nats://127.0.0.1:4222/work");
dp_msg_t *msg;
dp_header_t rhdr;
if (dp_pull_recv(rx, &msg, &rhdr) == DP_OK) {
size_t n = dp_msg_num_samples(msg);
const double _Complex *cf64 = dp_msg_data(msg);
/* ... use cf64[0..n) ... */
dp_msg_ack(msg); /* processed: otherwise redelivered, and kept queued */
dp_msg_free(msg);
}
dp_pull_destroy(rx);
A nats:// endpoint needs a running NATS broker; one command starts one.
Build and run the in-process demo (producer + consumer threads, 100 batches):
Composing a scene — who owns the buffer¶
wfmgen the tool and doppler.wfm the Python face both hand you a finished
array. Underneath, the caller holds the buffer, and four things the Python
face hides become the API:
- You own the output.
dp_wfm_compose_execute()never allocates. It fills what you give it and returns how much it used. - The stream ends with a short read, not an error, so composing means
draining in a loop until
execute()returns less than you asked for — and then0from every call after that. - The segment list is borrowed.
dp_wfm_compose_segments()points into the composer;dp_wfm_compose_destroy()takes it with it. - A declaration is reproducible. The same scene composed twice is byte-identical, which is what makes a capture replayable.
native/examples/wfmgen_demo.c
is the worked version: two segments, the first summing two clean tones, each
followed by a gap. Clean sources carry no AWGN, so the gaps are exact zeros
— which is why its checks are equalities rather than thresholds. It
self-validates and exits non-zero if any of them stops holding.
For the frame side of the same API — building a wfm_frame_desc_t and
pointing wfm_source_t.frame at it, so a layout no flag spells reaches the
samples — see
A Frame You Built, Generated and its
demo, native/examples/wfmgen_frame_demo.c.
A burst waveform end to end¶
example-projects/burst-pipeline/
is a downstream project — its own CMakeLists.txt, find_package(doppler),
an install prefix rather than this source tree — that walks the whole path:
describe a frame, generate a sixty-burst train, sweep it, write BLUE, read it
back. Copy it as a starting point. Four things it measures rather than
asserts:
repeatsis not shorthand. Sixty listed segments restart the source from its seed each time, so all sixty bursts carry identical noise; one segment withrepeats = 60draws fresh noise per instance. The program checks both, because the difference is invisible in a plot and fatal to a Monte-Carlo run.- Where SNR shows up. The gap is not silence — it carries the segment's noise floor — so burst power over gap power recovers the declared SNR.
- Prepare once, sweep many. A 24-point SNR sweep, re-composed at every
point against
dp_wfm_plan_prepare+dp_wfm_plan_at. - BLUE round-trips. cf32 out and back is byte-exact, with both halves timed in Msample/s.
make burst-pipeline-check builds and runs it in both link modes against a
scratch install, and CI runs the same target, so what you copy is known to
work.
An RTL-SDR front end on a small ARM board¶
example-projects/uno-q/
is the first stages of a receiver fed by an RTL-SDR, as a downstream project:
cu8 bytes through U8ToF32, a DDC that mixes an offset-tuned channel to DC
and decimates, and a PSD. Run with no input, it synthesises an RTL-SDR
capture and checks the chain; run on -, it reads a live cu8 stream. It was
measured on an Arduino UNO Q, a Cortex-A53-class board, and its README carries
that board's setup and tuning, including why -mcpu=cortex-a53 helps where
-march=native does not. Two points it demonstrates:
- Offset tuning removes the DC terms.
U8ToF32's fast mode reads 0.5/128 low, and the dongle adds its own DC spike. Mixing the wanted channel to DC moves both to−offset, where the DDC's filter removes them. The self-test measures the bias before the DDC and checks it is gone after. - Measure noise where the signal is. The plain DDC's CIC is uncompensated, so its output band slopes. A band-wide median floor read the SNR 3.6 dB high; the in-channel noise matches the prediction to 0.15 dB.
make uno-q-check builds and runs it in both link modes against a scratch
install, and CI runs it on Linux x86-64, Linux aarch64 and macOS.