Skip to content

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.

pip install doppler-dsp
python examples/standalone/example.py

Clone and build. This produces libdoppler.a, libdoppler.so, and (with BUILD_PYTHON=ON) the Python extension in src/doppler/.

git clone https://github.com/doppler-dsp/doppler
cmake -B doppler/build doppler -DCMAKE_BUILD_TYPE=Release
cmake --build doppler/build -j$(nproc)

Download a pre-built release from GitHub and install to a prefix of your choice.

cmake --install doppler/build --prefix ~/.local
export CMAKE_PREFIX_PATH=~/.local
export PKG_CONFIG_PATH=~/.local/lib/pkgconfig

C — static linking

No runtime .so dependency. Recommended for embedded use and distribution.

cmake -B examples/standalone/build examples/standalone \
      -DDOPPLER_BUILD_DIR=$(pwd)/build
cmake --build examples/standalone/build
./examples/standalone/build/awgn_example
cmake -B examples/standalone/build examples/standalone
cmake --build examples/standalone/build
./examples/standalone/build/awgn_example
gcc -o awgn_example examples/standalone/main.c \
    -Inative/inc -Ibuild/native/inc \
    build/libdoppler.a -lm
./awgn_example

C — dynamic linking

Links against libdoppler.so. The rpath is baked in so the binary runs without setting LD_LIBRARY_PATH.

cmake -B examples/standalone/build examples/standalone \
      -DDOPPLER_BUILD_DIR=$(pwd)/build \
      -DDOPPLER_LINK=shared
cmake --build examples/standalone/build
./examples/standalone/build/awgn_example

After cmake --install the find_package path picks up whichever library variant was installed:

cmake -B examples/standalone/build examples/standalone
cmake --build examples/standalone/build
./examples/standalone/build/awgn_example
gcc -o awgn_example examples/standalone/main.c \
    -Inative/inc -Ibuild/native/inc \
    -Lbuild -ldoppler -Wl,-rpath,$(pwd)/build -lm
./awgn_example

Python extension

pip install doppler-dsp
python examples/standalone/example.py
cmake -B build -DBUILD_PYTHON=ON && cmake --build build -j$(nproc)
python examples/standalone/example.py

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:

make build
./build/examples/c/hbdecim_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):

make build
./build/examples/c/agc_demo

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):

make build
./build/examples/c/pipeline_demo