C Examples¶
Standalone project¶
A minimal working project lives at
examples/standalone/.
It generates 4096 AWGN samples with 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.0168 + 0.0288i (expect ≈ 0)
std dev : 0.9952 (Re) 1.0090 (Im) (expect ≈ 1.0)
Consumer project — find_package¶
examples/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 examples/consumer/.
One CMakeLists.txt builds against both link targets:
doppler::doppler (shared) and doppler::doppler-static (the pure-C static
archive — links only -lm, no C++ runtime, no zmq). 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
doppler_wfmgen(argc, argv) (header 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 <lo/lo_core.h>
#include <complex.h>
#include <stdio.h>
int main(void) {
lo_state_t *lo = lo_create(0.25); // quarter-rate tone
float complex out[8];
lo_steps(lo, 8, out);
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
// out[2]: -1.000 + 0.000i
// out[3]: 0.000 - 1.000i
// out[4]: 1.000 + 0.000i (repeats every 4 samples)
// ...
lo_destroy(lo);
return 0;
}
FM modulation via control port¶
#include <lo/lo_core.h>
#include <complex.h>
#include <math.h>
lo_state_t *lo = lo_create(0.1); // base freq f_n = 0.1
float ctrl[1024];
for (int i = 0; i < 1024; i++)
ctrl[i] = 0.002f * sinf(2.0f * (float)M_PI * 0.01f * i);
float complex out[1024];
lo_steps_ctrl(lo, ctrl, 1024, out);
// base freq unchanged; reset restores clean phase
lo_destroy(lo);
AWGN — Additive White Gaussian Noise¶
One-shot (no persistent state)¶
#include <awgn/awgn_core.h>
#include <complex.h>
float complex out[1024];
awgn(0, 1.0f, 1024, out); /* seed=0, amplitude=1.0 — 0 on success, -1 on failure */
Stateful generator (streaming / reproducible replay)¶
#include <awgn/awgn_core.h>
#include <complex.h>
#include <stdio.h>
int main(void) {
awgn_state_t *g = awgn_create(42, 1.0f); /* seed, amplitude */
float complex buf[4096];
awgn_generate(g, 4096, buf); /* fill buf */
/* Retune amplitude without disturbing RNG state */
awgn_set_amplitude(g, 0.5f);
awgn_generate(g, 4096, buf);
/* Deterministic replay */
awgn_reset(g);
awgn_generate(g, 4096, buf); /* identical to first call */
awgn_destroy(g);
return 0;
}
Noisy carrier¶
#include <awgn/awgn_core.h>
#include <lo/lo_core.h>
#include <complex.h>
#define N 4096
int main(void) {
lo_state_t *lo = lo_create(0.1f);
awgn_state_t *noise = awgn_create(0, 0.3f); /* σ=0.3 per component */
float complex carrier[N], n[N], rx[N];
lo_steps(lo, N, carrier);
awgn_generate(noise, N, n);
for (size_t i = 0; i < N; i++)
rx[i] = carrier[i] + n[i];
lo_destroy(lo);
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 <nco/nco_core.h>
nco_state_t *nco = nco_create(0.25, 0); // nmax=0 → raw [0, 2^32)
uint32_t phase[16];
uint8_t carry[16];
nco_steps_u32_ovf(nco, 16, phase, carry);
// carry fires at indices 3, 7, 11, 15 (once per full cycle)
nco_destroy(nco);
FIR filter¶
#include <fir/fir_core.h>
#include <complex.h>
#include <math.h>
#define N_TAPS 19
int main(void) {
// Windowed-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);
}
fir_state_t *fir = fir_create_real(taps, N_TAPS);
float complex in[1024], out[1024];
fir_execute(fir, in, 1024, out, 1024);
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 "fft/fft_core.h"
#include <complex.h>
#include <math.h>
#include <stdio.h>
int main(void) {
const size_t N = 1024;
fft_state_t *fft = 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;
fft_execute_cf64(fft, in, N, out);
printf("DC bin: %.4f + %.4fi\n", creal(out[0]), cimag(out[0]));
fft_destroy(fft);
return 0;
}
1D FFT (single precision / CF32, ~2× faster)¶
#include "fft/fft_core.h"
#include <complex.h>
#include <math.h>
const size_t N = 1024;
fft_state_t *fft = fft_create(N, -1, 1);
float complex in32[N], out32[N];
for (size_t i = 0; i < N; i++)
in32[i] = cosf(2.0f * M_PI * 10.0f * i / N) + 0.0f * I;
fft_execute_cf32(fft, in32, N, out32); // out-of-place
fft_execute_inplace_cf32(fft, in32, N); // in-place
fft_destroy(fft);
2D FFT¶
#include "fft2d/fft2d_core.h"
#include <complex.h>
fft2d_state_t *fft2d = fft2d_create(64, 64, -1, 1);
double complex in2d[64 * 64], out2d[64 * 64];
fft2d_execute_cf64(fft2d, in2d, 64 * 64, out2d);
float complex in32_2d[64 * 64], out32_2d[64 * 64];
fft2d_execute_cf32(fft2d, in32_2d, 64 * 64, out32_2d);
fft2d_destroy(fft2d);
Halfband decimator¶
2:1 decimation with a symmetric FIR. Input length must be even;
output length is exactly n_in / 2.
#include <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) {
HalfbandDecimator_state_t *dec = HalfbandDecimator_create(N_TAPS, H_FIR);
float _Complex in[N_IN], out[N_IN / 2];
/* ... fill in[] with your signal ... */
size_t n_out = HalfbandDecimator_execute(dec, in, N_IN, out);
printf("output samples: %zu\n", n_out); /* 16 */
HalfbandDecimator_destroy(dec);
return 0;
}
Build and run the full demo:
AGC — automatic gain control¶
The AGC drives output power to ref_db using a first-order loop filter.
agc_step() processes one sample at a time; the loop is linear in the
dB domain so settling time is independent of the step size.
#include <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) {
agc_state_t *agc = 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 = agc_step(agc, x);
(void)y;
}
printf("gain_db = %.2f\n", agc->gain_db);
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 a ZMQ PUSH/PULL socket; consumer receives and prints power.
#include <doppler.h>
#include <stream/stream.h>
#include <complex.h>
/* Producer thread — rate/freq travel in the header the send builds */
dp_push_t *ctx = dp_push_create("ipc:///tmp/dp.ipc", CF64);
dp_push_send_cf64(ctx, samples, 1024, 1e6, 0.0); /* samples, n, fs, fc */
dp_push_destroy(ctx);
/* Consumer thread — recv hands back a library-owned message + header */
dp_pull_t *rx = dp_pull_create("ipc:///tmp/dp.ipc");
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_free(msg);
}
dp_pull_destroy(rx);
Build and run the in-process demo (producer + consumer threads, 100 batches):