Skip to content

File ddc_core.h

FileList > ddc > ddc_core.h

Go to the source code of this file

Digital Down-Converter — composes LO + RateConverter cascade. More...

  • #include <complex.h>
  • #include <stddef.h>
  • #include "lo/lo_core.h"
  • #include "RateConverter/RateConverter_core.h"
  • #include "resamp/resamp_core.h"
  • #include "hbdecim/hbdecim_core.h"
  • #include "cic/cic_core.h"
  • #include "fir/fir_core.h"
  • #include "resample/resample_core.h"

Classes

Type Name
struct ddc_extra_t
struct ddcr_extra_t

Public Types

Type Name
typedef struct ddc_state ddc_state_t
typedef struct ddcr_state ddcr_state_t

Public Functions

Type Name
ddc_state_t * ddc_create (double norm_freq, double rate)
Create a complex-input Digital Down-Converter. Allocates internal state for the LO and RateConverter cascade. The RateConverter selects the cheapest multi-stage decimation chain (CIC + optional halfband + polyphase resampler) for the given rate.
void ddc_destroy (ddc_state_t * state)
Free all resources held by a DDC instance. Releases the RateConverter and LO substructures, then the struct itself. Passing NULL is a no-op.
size_t ddc_execute (ddc_state_t * state, const float complex * x, size_t x_len, float complex * out, size_t max_out)
Mix and resample a block of CF32 samples. Multiplies each input sample by the current LO phasor (advancing the NCO phase per sample), then feeds the mixed block into the RateConverter. The resampler maintains history across calls, so arbitrary block sizes produce contiguous output with no edge artefacts. Output length ≈ x_len * rate (varies by ±1 due to polyphase indexing).
size_t ddc_execute_max_out (ddc_state_t * state)
Return the maximum output samples for one execute call.
double ddc_get_norm_freq (const ddc_state_t * state)
Return the current LO normalised frequency (cycles/sample).
double ddc_get_rate (const ddc_state_t * state)
Return the configured output/input rate ratio (read-only). The rate is fixed at create time; change it by destroying and recreating the DDC with the new value.
void ddc_get_state (const ddc_state_t * state, void * blob)
Serialize state's LO + RateConverter state intoblob .
void ddc_reset (ddc_state_t * state)
Zero LO phase and resampler history. After reset, the next execute call produces the same output as the first execute after create — useful for reproducible block-by-block processing or looped test fixtures.
size_t ddc_run (ddc_state_t * state, const void * state_in, void * state_out, const float complex * in, size_t n_in, float complex * out, size_t max_out)
Pure run: (state_in, input) -> (state_out, output) ; either blob may be NULL (NULL in = current; NULL out = discard).
void ddc_set_norm_freq (ddc_state_t * state, double val)
Retune the LO without resetting phase or resampler history. Updates the NCO phase increment atomically so the carrier shift changes seamlessly across block boundaries. The resampler history and LO phase accumulator are left intact, avoiding the transient that a full reset would cause.
int ddc_set_state (ddc_state_t * state, const void * blob)
Restore LO + RateConverter state from blob .
size_t ddc_state_bytes (const ddc_state_t * state)
Byte size of state's blob (envelope + extra + lo + rc).
ddcr_state_t * ddcr_create (double norm_freq, double rate)
Create a real-input Digital Down-Converter (Architecture D2). The signal chain is: halfband R2C (2:1, bakes in +fs/4 shift) → fine LO mix at the intermediate rate (fs_in/2) → RateConverter → CF32 output. The halfband stage uses ±1/0 coefficients (no multiplications), making DDCR roughly 2× cheaper than DDC at the same total decimation ratio.
void ddcr_destroy (ddcr_state_t * s)
Free all resources held by a DDCR instance. Releases the halfband, RateConverter, and LO substructures, then the struct itself. Passing NULL is a no-op.
size_t ddcr_execute (ddcr_state_t * s, const float * in, size_t n_in, float _Complex * out, size_t max_out)
Process a block of real float32 samples through the full DDCR signal chain: halfband R2C → LO mix → RateConverter → CF32. The halfband decimates by 2 and applies a built-in +fs/4 frequency shift; the fine NCO then completes the tuning. State is maintained across calls for contiguous streaming. Output length ≈ n_in * rate (±1 from polyphase indexing). A real tone at input normalised frequency f_c has amplitude 0.5 in the baseband output (one-sided spectrum), consistent with analytic signal theory.
double ddcr_get_norm_freq (const ddcr_state_t * s)
Return the current fine NCO normalised frequency at the intermediate rate (fs_in/2, cycles/sample).
double ddcr_get_rate (const ddcr_state_t * s)
Return the total configured rate (fs_out / fs_in, read-only). This is the end-to-end ratio from ADC input to CF32 output. Change it by destroying and recreating the DDCR.
void ddcr_get_state (const ddcr_state_t * s, void * blob)
Serialize s's full-chain state intoblob .
void ddcr_reset (ddcr_state_t * s)
Zero halfband filter history, LO phase, and resampler history. After reset, the next execute call reproduces the output of the first call after create, enabling repeatable block-by-block tests.
size_t ddcr_run (ddcr_state_t * s, const void * state_in, void * state_out, const float * in, size_t n_in, float _Complex * out, size_t max_out)
Pure run: inject state_in , processin , exportstate_out (state_in, input) -> (state_out, output) over an engine treated as immutable config. Either state may be NULL (NULL in = use current; NULL out = discard).state_in /state_out may alias.
void ddcr_set_norm_freq (ddcr_state_t * s, double norm_freq)
Retune the fine NCO without resetting halfband or resampler history. Updates the LO phase increment only; state is preserved for seamless tuning across block boundaries.
int ddcr_set_state (ddcr_state_t * s, const void * blob)
Restore full-chain state from blob intos .
size_t ddcr_state_bytes (const ddcr_state_t * s)
Byte size of s's state blob (envelope + extra + chain).

Macros

Type Name
define DDCR_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'D', 'C', 'R')
define DDCR_STATE_VERSION 1u
define DDC_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('D', 'D', 'C', '\_')
define DDC_STATE_VERSION 1u

Detailed Description

Two types:

Ddc — complex (CF32) input. Chain: LO mix → RateConverter. DdcR — real (float32) input. Chain: halfband R2C → LO mix → RateConverter.

Both are streaming (variable block size per execute call).

RateConverter selects the cheapest cascade (CIC + optional halfband + polyphase resampler) for the requested rate at create time. This makes large-ratio decimation (e.g., 100:1) significantly cheaper than a single polyphase stage.

Ddc signal chain

CF32 in (fs_in)    LO mix    RateConverter    CF32 out (fs_out)

norm_freq: NCO normalised frequency (cycles/sample at fs_in). Set to -f_carrier to shift a carrier at f_carrier to DC.

DdcR signal chain

float in (fs_in)    halfband R2C (2:1, embedded fs/4 shift)
                    LO mix at intermediate rate (fs_in/2)
                    RateConverter    CF32 out (fs_out)

norm_freq: Fine NCO frequency at the INTERMEDIATE rate (fs_in/2). To tune a real tone at f_carrier (input normalised) to DC: set norm_freq = -(2*f_carrier + 0.5). Total output rate: fs_out = rate * fs_in (rate < 0.5).

DdcR is approximately 2x cheaper than Ddc at equivalent total decimation because the halfband R2C step has an fs/4 frequency shift baked in at zero extra multiplications — the +/-1/0 coefficients multiply for free.

Retuning vs. rebuilding

  • Retune (centre-frequency change): call ddc_set_norm_freq / ddcr_set_norm_freq. Cheap — updates the LO phase increment without disturbing the resampler history. Seamless across block boundaries.
  • Rate change (span / decimation change): destroy and recreate the DDC for the new rate.

Usage

// Complex DDC: shift a carrier at +0.1·fs to DC, decimate by 4
ddc_state_t *ddc = ddc_create(-0.1, 0.25);
float _Complex out[4096];
size_t n = ddc_execute(ddc, in, 1024, out, 4096);
ddc_destroy(ddc);

// Real DDC: same carrier and decimation from a real ADC stream
// norm_freq at intermediate rate: -(2 * 0.1 + 0.5) = -0.7
ddcr_state_t *ddcr = ddcr_create(-0.7, 0.25);
size_t m = ddcr_execute(ddcr, real_in, 1024, out, 4096);
ddcr_destroy(ddcr);

Public Types Documentation

typedef ddc_state_t

typedef struct ddc_state ddc_state_t;

typedef ddcr_state_t

typedef struct ddcr_state ddcr_state_t;

Public Functions Documentation

function ddc_create

Create a complex-input Digital Down-Converter. Allocates internal state for the LO and RateConverter cascade. The RateConverter selects the cheapest multi-stage decimation chain (CIC + optional halfband + polyphase resampler) for the given rate.

ddc_state_t * ddc_create (
    double norm_freq,
    double rate
) 

Parameters:

  • norm_freq LO frequency in cycles/sample at the input rate. Set to -f_carrier to shift a carrier at f_carrier to DC. Any real value is accepted.
  • rate Output rate / input rate. Must be > 0. Values ≥ 1 are up-sampling; typical use is decimation (0 < rate < 1).

Returns:

Non-NULL on success, NULL on OOM or invalid args.

>>> from doppler.ddc import DDC
>>> ddc = DDC(norm_freq=-0.1, rate=0.25)
>>> ddc.norm_freq
-0.1
>>> ddc.rate
0.25

function ddc_destroy

Free all resources held by a DDC instance. Releases the RateConverter and LO substructures, then the struct itself. Passing NULL is a no-op.

void ddc_destroy (
    ddc_state_t * state
) 

>>> from doppler.ddc import DDC
>>> ddc = DDC(norm_freq=0.0, rate=0.25)
>>> ddc.destroy()   # releases C memory immediately

function ddc_execute

Mix and resample a block of CF32 samples. Multiplies each input sample by the current LO phasor (advancing the NCO phase per sample), then feeds the mixed block into the RateConverter. The resampler maintains history across calls, so arbitrary block sizes produce contiguous output with no edge artefacts. Output length ≈ x_len * rate (varies by ±1 due to polyphase indexing).

size_t ddc_execute (
    ddc_state_t * state,
    const float complex * x,
    size_t x_len,
    float complex * out,
    size_t max_out
) 

Parameters:

  • state Must be non-NULL.
  • x CF32 input block; accepted as float32 (auto-cast).
  • x_len Number of input samples (C-only, hidden from Python).
  • out CF32 output buffer (C-only, hidden from Python).
  • max_out Output buffer capacity (C-only, hidden from Python).

Returns:

Number of output samples written (C-only).

>>> from doppler.ddc import DDC
>>> import numpy as np
>>> ddc = DDC(norm_freq=-0.1, rate=0.25)
>>> t = np.arange(4096)
>>> x = np.exp(1j * 2 * np.pi * 0.1 * t).astype(np.complex64)
>>> y = ddc.execute(x)
>>> y.shape
(1024,)
>>> y.dtype
dtype('complex64')
>>> round(float(abs(y[500])), 2)   # shifted to DC; amplitude  1
1.0

function ddc_execute_max_out

Return the maximum output samples for one execute call.

size_t ddc_execute_max_out (
    ddc_state_t * state
) 

Returns 0, signalling the Python extension to fall back to allocating n_in samples — always sufficient for a decimating DDC.


function ddc_get_norm_freq

Return the current LO normalised frequency (cycles/sample).

double ddc_get_norm_freq (
    const ddc_state_t * state
) 

>>> from doppler.ddc import DDC
>>> ddc = DDC(norm_freq=-0.1, rate=0.25)
>>> ddc.norm_freq
-0.1

function ddc_get_rate

Return the configured output/input rate ratio (read-only). The rate is fixed at create time; change it by destroying and recreating the DDC with the new value.

double ddc_get_rate (
    const ddc_state_t * state
) 

>>> from doppler.ddc import DDC
>>> ddc = DDC(norm_freq=0.0, rate=0.25)
>>> ddc.rate
0.25

function ddc_get_state

Serialize state's LO + RateConverter state intoblob .

void ddc_get_state (
    const ddc_state_t * state,
    void * blob
) 


function ddc_reset

Zero LO phase and resampler history. After reset, the next execute call produces the same output as the first execute after create — useful for reproducible block-by-block processing or looped test fixtures.

void ddc_reset (
    ddc_state_t * state
) 

>>> from doppler.ddc import DDC
>>> import numpy as np
>>> ddc = DDC(norm_freq=0.0, rate=0.25)
>>> x = np.ones(64, dtype=np.complex64)
>>> y1 = ddc.execute(x)
>>> ddc.reset()
>>> y2 = ddc.execute(x)
>>> bool(np.array_equal(y1, y2))
True

function ddc_run

Pure run: (state_in, input) -> (state_out, output) ; either blob may be NULL (NULL in = current; NULL out = discard).

size_t ddc_run (
    ddc_state_t * state,
    const void * state_in,
    void * state_out,
    const float complex * in,
    size_t n_in,
    float complex * out,
    size_t max_out
) 


function ddc_set_norm_freq

Retune the LO without resetting phase or resampler history. Updates the NCO phase increment atomically so the carrier shift changes seamlessly across block boundaries. The resampler history and LO phase accumulator are left intact, avoiding the transient that a full reset would cause.

void ddc_set_norm_freq (
    ddc_state_t * state,
    double val
) 

Parameters:

  • state Must be non-NULL.
  • val New normalised frequency (cycles/sample at input rate).
>>> from doppler.ddc import DDC
>>> ddc = DDC(norm_freq=-0.1, rate=0.25)
>>> ddc.norm_freq = -0.2
>>> ddc.norm_freq
-0.2

function ddc_set_state

Restore LO + RateConverter state from blob .

int ddc_set_state (
    ddc_state_t * state,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if the envelope/rate rejects.


function ddc_state_bytes

Byte size of state's blob (envelope + extra + lo + rc).

size_t ddc_state_bytes (
    const ddc_state_t * state
) 


function ddcr_create

Create a real-input Digital Down-Converter (Architecture D2). The signal chain is: halfband R2C (2:1, bakes in +fs/4 shift) → fine LO mix at the intermediate rate (fs_in/2) → RateConverter → CF32 output. The halfband stage uses ±1/0 coefficients (no multiplications), making DDCR roughly 2× cheaper than DDC at the same total decimation ratio.

ddcr_state_t * ddcr_create (
    double norm_freq,
    double rate
) 

Parameters:

  • norm_freq Fine NCO frequency at the intermediate rate (fs_in/2, cycles/sample). To tune a real tone at normalised input frequency f_c to DC, set norm_freq = -(2*f_c + 0.5).
  • rate Total output/input rate. Must be in (0, 0.5) because the halfband pre-decimates by 2.

Returns:

Non-NULL on success, NULL on OOM or invalid args.

>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> ddcr.norm_freq
-0.7
>>> ddcr.rate
0.25

function ddcr_destroy

Free all resources held by a DDCR instance. Releases the halfband, RateConverter, and LO substructures, then the struct itself. Passing NULL is a no-op.

void ddcr_destroy (
    ddcr_state_t * s
) 

>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=0.0, rate=0.25)
>>> ddcr.close()   # releases C memory immediately

function ddcr_execute

Process a block of real float32 samples through the full DDCR signal chain: halfband R2C → LO mix → RateConverter → CF32. The halfband decimates by 2 and applies a built-in +fs/4 frequency shift; the fine NCO then completes the tuning. State is maintained across calls for contiguous streaming. Output length ≈ n_in * rate (±1 from polyphase indexing). A real tone at input normalised frequency f_c has amplitude 0.5 in the baseband output (one-sided spectrum), consistent with analytic signal theory.

size_t ddcr_execute (
    ddcr_state_t * s,
    const float * in,
    size_t n_in,
    float _Complex * out,
    size_t max_out
) 

Parameters:

  • s Must be non-NULL.
  • in Real float32 input block.
  • n_in Number of input samples (C-only, hidden from Python).
  • out CF32 output buffer (C-only, hidden from Python).
  • max_out Output buffer capacity (C-only, hidden from Python).

Returns:

Number of output samples written (C-only).

>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> t = np.arange(4096)
>>> x = np.cos(2 * np.pi * 0.1 * t).astype(np.float32)
>>> out = np.empty(len(x), dtype=np.complex64)
>>> y = ddcr.execute(x, out)
>>> y.shape
(1024,)
>>> y.dtype
dtype('complex64')
>>> round(float(abs(y[500])), 2)   # one-sided cosine amplitude  0.5
0.5

function ddcr_get_norm_freq

Return the current fine NCO normalised frequency at the intermediate rate (fs_in/2, cycles/sample).

double ddcr_get_norm_freq (
    const ddcr_state_t * s
) 

>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> ddcr.norm_freq
-0.7

function ddcr_get_rate

Return the total configured rate (fs_out / fs_in, read-only). This is the end-to-end ratio from ADC input to CF32 output. Change it by destroying and recreating the DDCR.

double ddcr_get_rate (
    const ddcr_state_t * s
) 

>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=0.0, rate=0.25)
>>> ddcr.rate
0.25

function ddcr_get_state

Serialize s's full-chain state intoblob .

void ddcr_get_state (
    const ddcr_state_t * s,
    void * blob
) 


function ddcr_reset

Zero halfband filter history, LO phase, and resampler history. After reset, the next execute call reproduces the output of the first call after create, enabling repeatable block-by-block tests.

void ddcr_reset (
    ddcr_state_t * s
) 

>>> from doppler.ddc import Ddcr
>>> import numpy as np
>>> ddcr = Ddcr(norm_freq=0.0, rate=0.25)
>>> x = np.ones(64, dtype=np.float32)
>>> out = np.empty(64, dtype=np.complex64)
>>> y1 = ddcr.execute(x, out).copy()
>>> ddcr.reset()
>>> y2 = ddcr.execute(x, out)
>>> bool(np.array_equal(y1, y2))
True

function ddcr_run

Pure run: inject state_in , processin , exportstate_out (state_in, input) -> (state_out, output) over an engine treated as immutable config. Either state may be NULL (NULL in = use current; NULL out = discard).state_in /state_out may alias.

size_t ddcr_run (
    ddcr_state_t * s,
    const void * state_in,
    void * state_out,
    const float * in,
    size_t n_in,
    float _Complex * out,
    size_t max_out
) 

Returns:

Number of CF32 output samples written.


function ddcr_set_norm_freq

Retune the fine NCO without resetting halfband or resampler history. Updates the LO phase increment only; state is preserved for seamless tuning across block boundaries.

void ddcr_set_norm_freq (
    ddcr_state_t * s,
    double norm_freq
) 

Parameters:

  • s Must be non-NULL.
  • norm_freq New frequency at the intermediate rate (fs_in/2).
>>> from doppler.ddc import Ddcr
>>> ddcr = Ddcr(norm_freq=-0.7, rate=0.25)
>>> ddcr.norm_freq = -0.5
>>> ddcr.norm_freq
-0.5

function ddcr_set_state

Restore full-chain state from blob intos .

int ddcr_set_state (
    ddcr_state_t * s,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if the envelope/rate disagree with s (rebuild the engine from the matching descriptor first).


function ddcr_state_bytes

Byte size of s's state blob (envelope + extra + chain).

size_t ddcr_state_bytes (
    const ddcr_state_t * s
) 


Macro Definition Documentation

define DDCR_STATE_MAGIC

#define DDCR_STATE_MAGIC `DP_FOURCC ('D', 'D', 'C', 'R')`

define DDCR_STATE_VERSION

#define DDCR_STATE_VERSION `1u`

define DDC_STATE_MAGIC

#define DDC_STATE_MAGIC `DP_FOURCC ('D', 'D', 'C', '_')`

define DDC_STATE_VERSION

#define DDC_STATE_VERSION `1u`


The documentation for this class was generated from the following file native/inc/ddc/ddc_core.h