File wfm_synth_core.h¶
FileList > doppler > wfm_synth > wfm_synth_core.h
Go to the source code of this file
Synth component API. More...
#include "doppler/clib_common.h"#include "doppler/dp_state.h"#include "doppler/jm_perf.h"#include "doppler/fir/fir_core.h"#include "doppler/lo/lo_core.h"#include "doppler/awgn/awgn_core.h"#include "doppler/pn/pn_core.h"#include "doppler/resamp/resamp_core.h"#include <math.h>#include "doppler/gold/gold_core.h"#include "doppler/wfm/wfm_dsp.h"#include "doppler/mpsk/mpsk_core.h"#include "doppler/cvt/cvt_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_wfm_synth_state_t |
| struct | wfm_synth_refill_state_t A frame source's own state triplet, over its user pointer. |
Public Types¶
| Type | Name |
|---|---|
| enum | wfm__synth__core_8h_1a05589fbab0657f08285ebdfe93f5ec9e |
| enum | wfm__synth__core_8h_1a16af7b253440dadd46a80a4b9fddba4d |
| typedef int(* | wfm_synth_refill_fn Synth state. |
Public Functions¶
| Type | Name |
|---|---|
| dp_wfm_synth_state_t * | dp_wfm_synth_create (int type, double fs, double freq, double snr, int snr_mode, uint32_t seed, int sps, int pn_length, uint64_t pn_poly, int lfsr, double f_end) Allocate and configure a waveform synthesiser. The synthesiser combines a local oscillator (LO), optional AWGN, and an optional PN LFSR into a single streaming source. One call to dp_wfm_synth_step() ordp_wfm_synth_steps() advances all sub-components in lock-step. SNR >= WFM_SYNTH_SNR_CLEAN (100 dB) skips AWGN entirely — clean waveforms pay no noise overhead. When snr_mode is "auto" the library picks the natural reference: Es/No for modulated types (BPSK, QPSK), fs-band SNR for tone/noise/PN. |
| int | dp_wfm_synth_data_ended (const dp_wfm_synth_state_t * state) Non-zero once an attached frame source has reported its end. |
| void | dp_wfm_synth_destroy (dp_wfm_synth_state_t * state) Destroy a synth instance and release all memory. Recursively frees the LO, AWGN, and PN sub-objects, then the struct itself. Safe to call with NULL (no-op). |
| float | dp_wfm_synth_get_cur_im (const dp_wfm_synth_state_t * state) Return the imaginary part of the current held symbol. For QPSK this is the Q component (±1/√2); for BPSK/PN it is always 0; for tone/noise it is 0. |
| float | dp_wfm_synth_get_cur_re (const dp_wfm_synth_state_t * state) Return the real part of the current held symbol. For modulated types this is the I component latched at the last symbol boundary (±1 for BPSK/PN, ±1/√2 for QPSK). For tone the synthesiser initialises cur_re to 1.0 so that the held symbol is a clean unit-power carrier; for noise it is 0.0 (noise has no held symbol). |
| int | dp_wfm_synth_get_nsps (const dp_wfm_synth_state_t * state) Return the samples-per-symbol count. For modulated types (BPSK, QPSK, PN) each symbol is held for nsps consecutive output samples. For tone/noise this field is present but unused by the synthesis path. |
| void | dp_wfm_synth_get_state (const dp_wfm_synth_state_t * state, void * blob) |
| int | dp_wfm_synth_get_sym_pos (const dp_wfm_synth_state_t * state) Return the current position within the current symbol (0..nsps-1). Reaches nsps and wraps to 0 each time a new symbol is consumed from the PN LFSR. Useful for frame alignment: sym_pos==0 on a step boundary means the very next sample begins a fresh symbol. |
| int | dp_wfm_synth_get_wtype (const dp_wfm_synth_state_t * state) Return the active waveform type discriminant. Maps to the WFM_SYNTH_* enum: 0=tone, 1=noise, 2=pn, 3=bpsk, 4=qpsk. Use this to inspect which synthesis path is active at runtime. |
| void | dp_wfm_synth_noise_steps (dp_wfm_synth_state_t * state, float _Complex * output, size_t n) Generate n noise-only samples — the synth's additive-AWGN term with no signal — continuing the same noise RNG stream dp_wfm_synth_steps() draws from (no reseed, identical chunked awgn call pattern, so a gap rendered here is the seamless continuation of the on-time noise). Writes exact zeros and advances nothing for a clean synth (no AWGN child). Used by the composer to carry a segment's noise floor through its off-time gap. |
| void | dp_wfm_synth_prod (dp_wfm_synth_state_t * state, const float _Complex * in, size_t n, float _Complex * out) Multiply a block by the synth's next output samples. out[i] = in[i] * y[i] fori = 0 .. n-1 , wherey is exactly whatdp_wfm_synth_steps() would emit next. The synth advances byn samples just as dp_wfm_synth_steps(state, ., n) would, EXACTLY: a split call sequence (3 + 5 + n) leaves the same synth state as one call, and the synth samplesy are the same, andn == 0 is a no-op that does not advance it. The PRODUCT agrees across such sequences to rounding, not to the bit: the compiler may contract the multiply differently on the vector body and the scalar tail, split calls and in place vs out of place (the -ffast-math builds do on arm64; see the note indp_wfm_synth_steps() ), so compare products with a few ulp of|in| * |y| , and drive a single call path where byte-for-byte matters.out may equalin (in place). No new state, so the state blob is unchanged. |
| void | dp_wfm_synth_reseed_noise (dp_wfm_synth_state_t * state, uint32_t seed) Reseed only the additive-noise (AWGN) generator, leaving the signal (LO / PN code / data / pulse shaping) untouched. A no-op for a synth with no noise. Used by the composer to give each repeat a fresh noise realization while the underlying waveform stays bit-identical. |
| void | dp_wfm_synth_reset (dp_wfm_synth_state_t * state) Reset Synth to its post-create state. Resets the LO phase accumulator, AWGN internal state, and PN LFSR register to their initial values so the output sequence is perfectly reproducible from sample 0. |
| int | dp_wfm_synth_set_bits (dp_wfm_synth_state_t * state, const uint8_t * bits, size_t n, int modulation) Attach a user bit pattern to a type=bits synth (no-op otherwise). |
| void | dp_wfm_synth_set_chirp_span (dp_wfm_synth_state_t * state, size_t span) Pin a chirp's sweep span to span samples (no-op for non-chirp). |
| void | dp_wfm_synth_set_cur_im (dp_wfm_synth_state_t * state, float val) Override the held-symbol imaginary (Q) component in-place. Takes effect on the next dp_wfm_synth_step() within the current symbol hold. |
| void | dp_wfm_synth_set_cur_re (dp_wfm_synth_state_t * state, float val) Override the held-symbol real (I) component in-place. Takes effect on the next dp_wfm_synth_step() within the current symbol hold. |
| int | dp_wfm_synth_set_dsss_chips (dp_wfm_synth_state_t * state, const uint8_t * chips, size_t n_chips) Install an assembled two-code DSSS burst as the chip pattern. |
| int | dp_wfm_synth_set_dsss_cont (dp_wfm_synth_state_t * state, const uint8_t * code, size_t code_len, double chips_per_symbol, int data_mode, const uint8_t * data, size_t n_data) Configure a type=dsss synth for CONTINUOUS ASYNCHRONOUS generation. |
| int | dp_wfm_synth_set_dsss_window (dp_wfm_synth_state_t * state, size_t code_only_symbols, size_t frame_symbols) Give the continuous DSSS stream a frame with a pure-code window. |
| void | dp_wfm_synth_set_nsps (dp_wfm_synth_state_t * state, int val) Override the samples-per-symbol count in-place. Does not flush the symbol-position counter (sym_pos); set sym_pos=0 as well when changing sps mid-stream. |
| int | dp_wfm_synth_set_refill (dp_wfm_synth_state_t * state, wfm_synth_refill_fn fn, void * user, void(*)(void *) free_user) Pull each frame from fn instead of cycling the pattern. |
| int | dp_wfm_synth_set_refill_state (dp_wfm_synth_state_t * state, const wfm_synth_refill_state_t * ops) Give the attached refill its state triplet, so the synth serializes. |
| int | dp_wfm_synth_set_rrc (dp_wfm_synth_state_t * state, const float * taps, size_t ntaps) Enable RRC pulse shaping on a symbol synth (pn/bpsk/qpsk/bits). |
| int | dp_wfm_synth_set_state (dp_wfm_synth_state_t * state, const void * blob) |
| void | dp_wfm_synth_set_sym_pos (dp_wfm_synth_state_t * state, int val) Override the symbol-position counter in-place. Injecting 0 forces the next dp_wfm_synth_step() to latch a new PN chip; any other value fast-forwards into the middle of the current symbol hold. |
| int | dp_wfm_synth_set_symbols (dp_wfm_synth_state_t * state, const float _Complex * symbols, size_t n) Attach a complex-symbol stream to a type=symbols synth (no-op else). |
| void | dp_wfm_synth_set_wtype (dp_wfm_synth_state_t * state, int val) Override the waveform type discriminant in-place. Changing wtype does not reinitialise sub-objects; use with care. |
| size_t | dp_wfm_synth_state_bytes (const dp_wfm_synth_state_t * state) |
| const char * | dp_wfm_synth_state_refusal (const dp_wfm_synth_state_t * state) Why the synth's state cannot be serialized, or NULL when it can. |
| JM_FORCEINLINE JM_HOT float _Complex | dp_wfm_synth_step (dp_wfm_synth_state_t * state) Generate one output sample from internal state. Advances the PN LFSR (modulated types only, on symbol boundaries), the LO phase accumulator, and the AWGN engine, then returns the mixed result: sym * carrier + noise . Inlined and hot-path annotated so tight per-sample loops pay no call overhead. |
| void | dp_wfm_synth_steps (dp_wfm_synth_state_t * state, float _Complex * output, size_t n) Generate a block of output samples. Calls dp_wfm_synth_step() in a tight loop, writing each cf32 sample into output . The Python binding returns a freshly allocated NumPy complex64 array; ownership is transferred to the caller. |
| void | dp_wfm_synth_sweep_ends (double fs, double f_mid, double rate, size_t n, double center, double * f0, double * f_end) Start and end frequency of the chirp that sweeps about a center. A chirp with freq = *f0 ,f_end = *f_end and dp_wfm_synth_set_chirp_span(n) has instantaneous frequencyf_mid + rate*u (Hz) at samplei ,u = (i - center)/fs seconds, and so multiplies a block byexp(j*2*pi*(f_mid*u + rate*u^2/2)) up to one constant phase (the synth's phase is 0 at sample 0, the formula's atu = 0 ). The synth's increment lands after its sample, hence the half-sample in*f0 . Pure arithmetic: no state, nothing allocated. |
| JM_FORCEINLINE int | wfm_synth_bit_ahead (dp_wfm_synth_state_t * s) Whether the data has a next bit, without taking it. |
| JM_FORCEINLINE int | wfm_synth_bit_next (dp_wfm_synth_state_t * s, unsigned * bit) The next bit of the pattern, or none once the data has ended. |
| JM_FORCEINLINE float _Complex | wfm_synth_bit_symbol (dp_wfm_synth_state_t * s) Next symbol from the user bit pattern — one mapping, every M. |
| JM_FORCEINLINE int | wfm_synth_bps (int type) Bits carried by one symbol of type — thebps an Eb/No needs. |
| JM_FORCEINLINE float | wfm_synth_cont_dsss_chip (dp_wfm_synth_state_t * s) One continuous-DSSS chip: code[n % n_code] ^ data , as a BPSK sign. |
| JM_FORCEINLINE uint64_t | wfm_synth_mls_poly (uint32_t n) The MLS primitive polynomial table — pn's, reached by its old name. |
| JM_FORCEINLINE float _Complex | wfm_synth_next_symbol (dp_wfm_synth_state_t * s) Pull the next constellation symbol from the active shaped source. |
| JM_FORCEINLINE void | wfm_synth_shape (dp_wfm_synth_state_t * s, float _Complex * out, size_t m, float _Complex * syms) Produce m polyphase-shaped baseband samples intoout . |
| JM_FORCEINLINE void | wfm_synth_shaper_prime (dp_wfm_synth_state_t * s) Prime the shaper's delay line so its output aligns with the dense FIR. |
| JM_FORCEINLINE double | wfm_synth_snr_over_fs (int mode, int bps, double span, double snr) Convert a per-symbol or per-bit SNR to SNR over the full sample rate. |
Macros¶
| Type | Name |
|---|---|
| define | WFM_DSSS_ENDED 2u |
| define | WFM_SYNTH_SNR_CLEAN 100.0 |
| define | WFM_SYNTH_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('W','F','M','S') |
| define | WFM_SYNTH_STATE_VERSION 5u |
Detailed Description¶
Lifecycle: create -> [step / steps / reset]* -> destroy
Example:
dp_wfm_synth_state_t *obj = dp_wfm_synth_create(0, 1000000.0, 0.0, 100.0, 0, 1, 8, 7, 0);
float _Complex y = dp_wfm_synth_step(obj);
dp_wfm_synth_destroy(obj);
Public Types Documentation¶
enum wfm__synth__core_8h_1a05589fbab0657f08285ebdfe93f5ec9e¶
enum wfm__synth__core_8h_1a05589fbab0657f08285ebdfe93f5ec9e {
WFM_SYNTH_TONE = 0,
WFM_SYNTH_NOISE = 1,
WFM_SYNTH_PN = 2,
WFM_SYNTH_BPSK = 3,
WFM_SYNTH_QPSK = 4,
WFM_SYNTH_CHIRP = 5,
WFM_SYNTH_BITS = 6,
WFM_SYNTH_SYMBOLS = 7,
WFM_SYNTH_DSSS = 8
};
Waveform type discriminant (the type create argument / type choice).
enum wfm__synth__core_8h_1a16af7b253440dadd46a80a4b9fddba4d¶
enum wfm__synth__core_8h_1a16af7b253440dadd46a80a4b9fddba4d {
WFM_DSSS_DATA_NONE = 0,
WFM_DSSS_DATA_BITS = 1,
WFM_DSSS_DATA_PRBS = 2
};
Continuous-DSSS data-symbol source (dp_wfm_synth_set_dsss_cont's data_mode).
typedef wfm_synth_refill_fn¶
Synth state.
Allocate with dp_wfm_synth_create().
Where a BITS synth's next frame (or a dsss burst's next burst) comes from, in place of the cycle.
Called when the synth has read the last of its n bits: write the next frame's n bits into bits and return 0, or return non-zero when the data has ended. The frame length never changes, which is what lets the synth refill its own buffer in place. See dp_wfm_synth_set_refill().
Public Functions Documentation¶
function dp_wfm_synth_create¶
Allocate and configure a waveform synthesiser. The synthesiser combines a local oscillator (LO), optional AWGN, and an optional PN LFSR into a single streaming source. One call to dp_wfm_synth_step() ordp_wfm_synth_steps() advances all sub-components in lock-step. SNR >= WFM_SYNTH_SNR_CLEAN (100 dB) skips AWGN entirely — clean waveforms pay no noise overhead. Whensnr_mode is "auto" the library picks the natural reference: Es/No for modulated types (BPSK, QPSK), fs-band SNR for tone/noise/PN.
dp_wfm_synth_state_t * dp_wfm_synth_create (
int type,
double fs,
double freq,
double snr,
int snr_mode,
uint32_t seed,
int sps,
int pn_length,
uint64_t pn_poly,
int lfsr,
double f_end
)
Parameters:
typeWaveform type: 0=tone, 1=noise, 2=pn, 3=bpsk, 4=qpsk, 5=chirp, 6=bits, 7=symbols, 8=dsss. The Python binding accepts strings "tone"|"noise"|"pn"|"bpsk"|"qpsk"|"chirp"|"bits"|"symbols"|"dsss". For "bits" attach the pattern with dp_wfm_synth_set_bits(); for "symbols" attach the complex stream with dp_wfm_synth_set_symbols(); for "dsss" attach the burst with dp_wfm_synth_set_dsss_chips() after create().fsSample rate in Hz. Sets the carrier frequency normalisation and the noise bandwidth. Default 1 000 000.0.freqCarrier frequency offset in Hz (−fs/2 … fs/2). A complex LO is created only when freq != 0. For a chirp this is the start frequency f_start (the instantaneous frequency at t=0). Default 0.0.snrTarget SNR in dB, interpreted persnr_mode. Values >= WFM_SYNTH_SNR_CLEAN (100) disable AWGN. Default 100.0.snr_modeSNR reference: 0=auto, 1=fs (full-band), 2=ebno, 3=esno. The Python binding accepts strings "auto"|"fs"|"ebno"|"esno". Default 0.seedPRNG seed shared by AWGN and the PN LFSR. Default 1.spsSamples per symbol for modulated types (BPSK, QPSK, PN). Ignored for tone/noise. Default 8.pn_lengthLFSR register length (1..64); period = 2^pn_length - 1. Default 7 (period 127).pn_polyGalois tap polynomial for the LFSR. 0 means "look up the canonical MLS polynomial for pn_length" from the wfm_synth_mls_poly table. Default 0.lfsrLFSR realization: PN_GALOIS (0) or PN_FIBONACCI (1).f_endChirp end frequency in Hz (type=chirp only; ignored otherwise). Withfreqas the start, the instantaneous frequency sweeps linearly fromfreqtof_endover the span set by dp_wfm_synth_set_chirp_span(), then holds atf_end. Until a span is pinned the slope is 0 (a CW tone atfreq).f_end < freqis a down-chirp. Default 0.0.
Returns:
Heap-allocated state, or NULL on allocation failure.
Note:
Caller must call dp_wfm_synth_destroy() when done.
>>> from doppler.wfm import _SynthEngine
>>> import numpy as np
>>> s = _SynthEngine(type="tone", fs=1.0, freq=0.0, snr=100.0)
>>> x = s.steps(4)
>>> x.dtype
dtype('complex64')
>>> x.tolist()
[(1+0j), (1+0j), (1+0j), (1+0j)]
function dp_wfm_synth_data_ended¶
Non-zero once an attached frame source has reported its end.
function dp_wfm_synth_destroy¶
Destroy a synth instance and release all memory. Recursively frees the LO, AWGN, and PN sub-objects, then the struct itself. Safe to call with NULL (no-op).
Parameters:
statePointer to heap-allocated state; may be NULL.
function dp_wfm_synth_get_cur_im¶
Return the imaginary part of the current held symbol. For QPSK this is the Q component (±1/√2); for BPSK/PN it is always 0; for tone/noise it is 0.
Parameters:
stateMust be non-NULL.
Returns:
Current symbol imaginary (Q) component.
function dp_wfm_synth_get_cur_re¶
Return the real part of the current held symbol. For modulated types this is the I component latched at the last symbol boundary (±1 for BPSK/PN, ±1/√2 for QPSK). For tone the synthesiser initialises cur_re to 1.0 so that the held symbol is a clean unit-power carrier; for noise it is 0.0 (noise has no held symbol).
Parameters:
stateMust be non-NULL.
Returns:
Current symbol real (I) component.
function dp_wfm_synth_get_nsps¶
Return the samples-per-symbol count. For modulated types (BPSK, QPSK, PN) each symbol is held for nsps consecutive output samples. For tone/noise this field is present but unused by the synthesis path.
Parameters:
stateMust be non-NULL.
Returns:
Samples per symbol (nsps >= 1).
function dp_wfm_synth_get_state¶
function dp_wfm_synth_get_sym_pos¶
Return the current position within the current symbol (0..nsps-1). Reaches nsps and wraps to 0 each time a new symbol is consumed from the PN LFSR. Useful for frame alignment: sym_pos==0 on a step boundary means the very next sample begins a fresh symbol.
Parameters:
stateMust be non-NULL.
Returns:
Symbol position counter (0 <= sym_pos < nsps).
function dp_wfm_synth_get_wtype¶
Return the active waveform type discriminant. Maps to the WFM_SYNTH_* enum: 0=tone, 1=noise, 2=pn, 3=bpsk, 4=qpsk. Use this to inspect which synthesis path is active at runtime.
Parameters:
stateMust be non-NULL.
Returns:
Integer waveform type index (WFM_SYNTH_TONE .. WFM_SYNTH_QPSK).
function dp_wfm_synth_noise_steps¶
Generate n noise-only samples — the synth's additive-AWGN term with no signal — continuing the same noise RNG stream dp_wfm_synth_steps() draws from (no reseed, identical chunked awgn call pattern, so a gap rendered here is the seamless continuation of the on-time noise). Writes exact zeros and advances nothing for a clean synth (no AWGN child). Used by the composer to carry a segment's noise floor through its off-time gap.
Parameters:
stateSynth state (may be NULL — no-op).outputn complex samples out.nSample count.
function dp_wfm_synth_prod¶
Multiply a block by the synth's next output samples. out[i] = in[i] * y[i] fori = 0 .. n-1 , wherey is exactly whatdp_wfm_synth_steps() would emit next. The synth advances byn samples just as dp_wfm_synth_steps(state, ., n) would, EXACTLY: a split call sequence (3 + 5 + n) leaves the same synth state as one call, and the synth samplesy are the same, andn == 0 is a no-op that does not advance it. The PRODUCT agrees across such sequences to rounding, not to the bit: the compiler may contract the multiply differently on the vector body and the scalar tail, split calls and in place vs out of place (the -ffast-math builds do on arm64; see the note indp_wfm_synth_steps() ), so compare products with a few ulp of|in| * |y| , and drive a single call path where byte-for-byte matters.out may equalin (in place). No new state, so the state blob is unchanged.
void dp_wfm_synth_prod (
dp_wfm_synth_state_t * state,
const float _Complex * in,
size_t n,
float _Complex * out
)
This is the general waveform-multiplication operator: with a tone synth it is a rotation, with a chirp synth a swept rotation, with noise a noise multiplier, with PN/BPSK a modulation.
Parameters:
stateInitialized Synth state returned bydp_wfm_synth_create.inInput block ofncf32 samples.nNumber of samples.outOutput block ofncf32 samples (may be the input block itself).
function dp_wfm_synth_reseed_noise¶
Reseed only the additive-noise (AWGN) generator, leaving the signal (LO / PN code / data / pulse shaping) untouched. A no-op for a synth with no noise. Used by the composer to give each repeat a fresh noise realization while the underlying waveform stays bit-identical.
Parameters:
stateSynth state (may be NULL).seedNew noise RNG seed.
function dp_wfm_synth_reset¶
Reset Synth to its post-create state. Resets the LO phase accumulator, AWGN internal state, and PN LFSR register to their initial values so the output sequence is perfectly reproducible from sample 0.
Parameters:
stateMust be non-NULL.
function dp_wfm_synth_set_bits¶
Attach a user bit pattern to a type=bits synth (no-op otherwise).
int dp_wfm_synth_set_bits (
dp_wfm_synth_state_t * state,
const uint8_t * bits,
size_t n,
int modulation
)
Copies n bits (each 0/1) into the synth; modulation is the bits per symbol, mapping them to symbols (0=none → 0/1 amplitude, 1=bpsk → ±1, 2=qpsk, 3=8psk: each symbol's bits read MSB-first as a Gray label into mpsk_constellation(), see wfm_synth_bit_symbol). The pattern is oversampled by the create-time sps and sent ONCE: one pass is n * sps samples (n / modulation * sps for a PSK), and the output is silent after it (doppler#1718 deleted the cycle; a payload that goes on is a data source, dp_wfm_synth_set_refill). Replaces any previous pattern; resets the read position. Safe to call repeatedly.
Parameters:
stateMust be non-NULL.bitsArray ofnbytes, each 0 or 1.nNumber of bits (> 0).modulation0=none, 1=bpsk, 2=qpsk, 3=8psk.
Returns:
0 on success; -1 on bad args or allocation failure.
function dp_wfm_synth_set_chirp_span¶
Pin a chirp's sweep span to span samples (no-op for non-chirp).
A linear chirp's slope is (f_end − f_start) / span, so the span — the number of samples the sweep occupies — must be known before generation. The composer calls this with the source's declared span or the segment length. A synth that is never pinned does not sweep: it holds the start frequency on dp_wfm_synth_step() and dp_wfm_synth_steps() alike, so the waveform never depends on how reads are chunked. Only the first pin (while the span is still 0) takes effect, so it is safe to call unconditionally after dp_wfm_synth_create(); span 0 is a no-op.
The span is configuration, not running state: dp_wfm_synth_get_state() does not carry it, so pin a resumed instance exactly as the original was pinned.
Parameters:
stateMust be non-NULL.spanSweep length in samples (> 0).
function dp_wfm_synth_set_cur_im¶
Override the held-symbol imaginary (Q) component in-place. Takes effect on the next dp_wfm_synth_step() within the current symbol hold.
Parameters:
stateMust be non-NULL.valNew cur_im value.
function dp_wfm_synth_set_cur_re¶
Override the held-symbol real (I) component in-place. Takes effect on the next dp_wfm_synth_step() within the current symbol hold.
Parameters:
stateMust be non-NULL.valNew cur_re value.
function dp_wfm_synth_set_dsss_chips¶
Install an assembled two-code DSSS burst as the chip pattern.
int dp_wfm_synth_set_dsss_chips (
dp_wfm_synth_state_t * state,
const uint8_t * chips,
size_t n_chips
)
The burst is assembled from a frame DESCRIPTION by dp_wfm_dsss_desc_chips() (wfm/wfm_frame.h) an unmodulated preamble (acq_code repeated acq_reps times, the coherent acquisition target) followed by every bit of the assembled frame XOR-spread by the distinct data_code and installed here as the synth's BPSK chip stream, each chip held for the create-time sps samples, i.e. sps is samples per chip here. The common frame sync | payload | CRC-16 is dp_wfm_frame_fixed(); a coded burst is any other description. This is the transmit side of BurstDemod's frame contract: the same codes, sync word, and payload length hand to dp_burst_demod_create_desc (as a frame description) and dp_burst_demod_set_preamble on receive.
One pass of the pattern is one burst (n_chips * sps samples), sent once like the bits pattern, and silence after it the composer sizes a dsss segment's on-time to exactly one burst, or one per frame of a data source (dp_wfm_synth_set_refill). Replaces any previous pattern; resets the read position. Chips are copied; chips stays the caller's.
NOTE: snr_mode semantics — the raw engine's create-time esno refers to the chip (the output symbol). The Segment/Synth faces convert a data-symbol Es/N0 (snr_mode="esno") to the over-fs value with 10*log10(sf*sps) before create; see dp_wfm_snr_over_fs().
Parameters:
stateSynth (no-op unlesswtype == WFM_SYNTH_DSSS).chipsBurst chips, one per byte (0/1), BPSK-mapped by the synth.n_chipsChip count; must be non-zero.
Returns:
0 on success, -1 on a NULL/empty pattern or allocation failure.
function dp_wfm_synth_set_dsss_cont¶
Configure a type=dsss synth for CONTINUOUS ASYNCHRONOUS generation.
int dp_wfm_synth_set_dsss_cont (
dp_wfm_synth_state_t * state,
const uint8_t * code,
size_t code_len,
double chips_per_symbol,
int data_mode,
const uint8_t * data,
size_t n_data
)
The continuous counterpart to dp_wfm_synth_set_dsss_chips(): the same type="dsss" waveform, switched to the endless mode by supplying chips_per_symbol (= chip_rate / symbol_rate). One waveform type, one discriminator — no tenth entry in the five hand-maintained name tables wfm_names.h records rotting once already.
Lazy, not materialised. Chips are generated per sample by wfm_synth_cont_dsss_chip off a running counter, so the stream is genuinely endless — there is no pattern length to pick and the standalone Synth face works unbounded. The data-symbol source is chosen by data_mode:
* WFM_DSSS_DATA_NONE — code-only: the pure spreading code, no data.
* WFM_DSSS_DATA_BITS — data, one bit per data symbol, sent ONCE: the chips are silent after its last bit (doppler#1718).
* WFM_DSSS_DATA_PRBS — the synth's own seeded PN (create it in create(); a receiver regenerates the bits via doppler.wfm.PN).
The burst frame parameters have no meaning here (no preamble, sync, or CRC); the caller rejects that combination upstream rather than ignoring it (see wfmgen's --symbol-rate validation), so this function does not revisit it.
Parameters:
stateSynth (no-op unlesswtype == WFM_SYNTH_DSSS).codeSpreading code chips (0/1), lengthcode_len; copied.code_lenSpreading code length in chips (> 0) — the SF.chips_per_symbolChips per data symbol (>= 1),chip_rate / symbol_rate. Non-integer is the normal, asynchronous case.data_modeWFM_DSSS_DATA_{NONE,BITS,PRBS}.dataPayload bits (0/1) for WFM_DSSS_DATA_BITS, lengthn_data; copied. Ignored (may be NULL) otherwise.n_dataPayload length in bits (> 0 for WFM_DSSS_DATA_BITS).
Returns:
0 on success; -1 on invalid geometry or allocation failure.
function dp_wfm_synth_set_dsss_window¶
Give the continuous DSSS stream a frame with a pure-code window.
int dp_wfm_synth_set_dsss_window (
dp_wfm_synth_state_t * state,
size_t code_only_symbols,
size_t frame_symbols
)
The frame is on the DATA clock: of every frame_symbols symbols, the first code_only_symbols carry the pure spreading code and no data, and the rest carry the payload, running on from the previous frame. The symbol clock is the stream's own free-running one (see wfm_synth_cont_dsss_chip), so a frame edge lands at whatever chip phase it lands at: the chip and data clocks have no fixed relation, and no frame edge is synchronous with a code epoch. This is the multi-emitter waveform's frame — 450 code-only symbols then 4500 of data in the application it was written for — and the searcher's coherent depth is what the window makes possible. Configuration, not running state: reset() keeps it. Like every setter's value it travels in the state blob (v5), so a synth restored without this call resumes the framed stream; a blob whose window breaks the rule below is refused. The order against dp_wfm_synth_set_dsss_cont() does not matter.
Parameters:
stateSynth (no-op unlesswtype == WFM_SYNTH_DSSS).code_only_symbolsPure-code symbols opening each frame, at mostframe_symbols. Equal to it means code only, for ever.frame_symbolsFrame length in symbols; 0 means no window — the stream exactly as without this call.
Returns:
0 on success (and for a non-dsss synth); -1 if code_only_symbols exceeds a non-zero frame_symbols.
function dp_wfm_synth_set_nsps¶
Override the samples-per-symbol count in-place. Does not flush the symbol-position counter (sym_pos); set sym_pos=0 as well when changing sps mid-stream.
Parameters:
stateMust be non-NULL.valNew nsps value (>= 1).
function dp_wfm_synth_set_refill¶
Pull each frame from fn instead of cycling the pattern.
int dp_wfm_synth_set_refill (
dp_wfm_synth_state_t * state,
wfm_synth_refill_fn fn,
void * user,
void(*)(void *) free_user
)
The pattern set by dp_wfm_synth_set_bits() is the FIRST frame; when its last bit has been read, fn writes the next frame's bits into the synth's buffer (the same n), and so on. When fn reports the end, the synth latches it (dp_wfm_synth_data_ended()) and emits zero silence from then on. A later dp_wfm_synth_set_bits() detaches the refill.
A dsss BURST plays its chips through the same cursor, so it takes a refill too: the pattern is the burst dp_wfm_synth_set_dsss_chips() installed, and fn writes the next burst's chips (doppler#1719). A later dp_wfm_synth_set_dsss_chips() detaches it. A CONTINUOUS dsss stream set up with WFM_DSSS_DATA_BITS takes one as well: each data symbol reads the next bit through the cursor instead of cycling its payload, the refill writing the next n bits, and the chips are silent once it reports the end.
A refill attached here has no state of its own, so the synth REFUSES serialization dp_wfm_synth_state_bytes() returns 0 and dp_wfm_synth_set_state() is DP_ERR_INVALID until the source's triplet is attached with dp_wfm_synth_set_refill_state(): the pulled frame and the source's position are state, and a blob without them would resume the wrong data. dp_wfm_synth_state_refusal() says why.
Parameters:
statea type=bits synth with a pattern set, a type=dsss burst with its chips set, or a continuous one whose data_mode isWFM_DSSS_DATA_BITS.fnthe frame source; NULL detaches.userpassed tofn; owned by the synth whenfree_useris given, which it calls on detach or destroy.free_userfreesuser, or NULL.
Returns:
0, or -1 if the synth is none of those, or has no pattern.
#include "doppler/wfm_synth/wfm_synth_core.h"
#include <complex.h>
// One more 4-bit frame of ones after the first, then the end.
static int
one_more (void *u, uint8_t *bits, size_t n)
{
int *left = u;
if ((*left)-- <= 0)
return 1; // the end
for (size_t i = 0; i < n; i++)
bits[i] = 1;
return 0;
}
int
main (void)
{
dp_wfm_synth_state_t *s = dp_wfm_synth_create (
WFM_SYNTH_BITS, 1e6, 0.0, 200.0, 0, 1, 1, 7, 0, 0, 0.0);
int left = 1;
dp_wfm_synth_set_bits (s, (const uint8_t[]){ 0, 1, 0, 1 }, 4, 0);
dp_wfm_synth_set_refill (s, one_more, &left, NULL);
float _Complex x[12];
dp_wfm_synth_steps (s, x, 12); // 0101, then 1111, then silence
const int ok = crealf (x[1]) > 0.5f && crealf (x[4]) > 0.5f
&& cabsf (x[11]) < 1e-3f && dp_wfm_synth_data_ended (s);
dp_wfm_synth_destroy (s);
return ok ? 0 : 1;
}
function dp_wfm_synth_set_refill_state¶
Give the attached refill its state triplet, so the synth serializes.
int dp_wfm_synth_set_refill_state (
dp_wfm_synth_state_t * state,
const wfm_synth_refill_state_t * ops
)
The blob then carries the frame in play (bits, which a refill rewrites and so is state, not config) and nests ops' sub-blob over the refill's user. A later dp_wfm_synth_set_refill() any attach or detach drops ops with the refill it belonged to.
Parameters:
statea synth with a refill attached.opsthe triplet, in static storage; NULL takes it away.
Returns:
0, or -1 when no refill is attached.
function dp_wfm_synth_set_rrc¶
Enable RRC pulse shaping on a symbol synth (pn/bpsk/qpsk/bits).
Replaces the default rectangular sample-and-hold with a root-raised-cosine pulse: the symbol-rate impulse train is filtered by taps (a real FIR of ntaps coefficients, typically dp_wfm_rrc_taps(beta, sps, span)). The taps are scaled by sqrt(sps) internally for unit transmit power, so every caller passes the raw taps and gets byte-identical shaping. No-op for types with no symbol stream (tone/noise/chirp). Replaces any existing shaper and clears its delay line.
Parameters:
stateMust be non-NULL.tapsReal FIR taps (copied).ntapsNumber of taps (> 0).
Returns:
0 on success; -1 on bad args / allocation failure.
function dp_wfm_synth_set_state¶
function dp_wfm_synth_set_sym_pos¶
Override the symbol-position counter in-place. Injecting 0 forces the next dp_wfm_synth_step() to latch a new PN chip; any other value fast-forwards into the middle of the current symbol hold.
Parameters:
stateMust be non-NULL.valNew sym_pos value (0 <= val < nsps).
function dp_wfm_synth_set_symbols¶
Attach a complex-symbol stream to a type=symbols synth (no-op else).
int dp_wfm_synth_set_symbols (
dp_wfm_synth_state_t * state,
const float _Complex * symbols,
size_t n
)
Copies n complex symbols into the synth. Each symbol is the constellation point — there is no bit→symbol mapping, so this generalises every modulation (pi/4-QPSK, QAM, custom shaping) into "compute the symbols,
pass them in". The stream is oversampled by the create-time sps and cycled to fill whatever length dp_wfm_synth_steps() requests (one pass is n * sps samples), and is RRC-shaped when dp_wfm_synth_set_rrc() is active. Replaces any previous stream; resets the read position. Safe to call repeatedly.
Parameters:
stateMust be non-NULL.symbolsArray ofncomplex symbols (copied).nNumber of symbols (> 0).
Returns:
0 on success; -1 on bad args or allocation failure.
>>> import numpy as np
>>> from doppler.wfm import _SynthEngine, rrc_taps
>>> s = _SynthEngine(
... type="symbols", fs=1.0, freq=0.0, snr=100.0, sps=4)
>>> s.set_symbols(np.array([1+0j, 1j, -1+0j, -1j], np.complex64))
>>> s.steps(4)[::4].tolist() # symbol centres (rect hold)
[(1+0j), (1+0j), (1+0j), (1+0j)]
function dp_wfm_synth_set_wtype¶
Override the waveform type discriminant in-place. Changing wtype does not reinitialise sub-objects; use with care.
Parameters:
stateMust be non-NULL.valNew wtype value (WFM_SYNTH_TONE .. WFM_SYNTH_QPSK).
function dp_wfm_synth_state_bytes¶
function dp_wfm_synth_state_refusal¶
Why the synth's state cannot be serialized, or NULL when it can.
A refill with no triplet (dp_wfm_synth_set_refill_state()), or a source whose triplet refuses a pipe, whose delivered octets are gone. The reason is static, as every refusal's is.
function dp_wfm_synth_step¶
Generate one output sample from internal state. Advances the PN LFSR (modulated types only, on symbol boundaries), the LO phase accumulator, and the AWGN engine, then returns the mixed result: sym * carrier + noise . Inlined and hot-path annotated so tight per-sample loops pay no call overhead.
Parameters:
stateMust be non-NULL.
Returns:
Next output sample (float _Complex).
>>> from doppler.wfm import _SynthEngine
>>> s = _SynthEngine(type="tone", fs=1.0, freq=0.0, snr=100.0)
>>> s.step()
(1+0j)
function dp_wfm_synth_steps¶
Generate a block of output samples. Calls dp_wfm_synth_step() in a tight loop, writing each cf32 sample intooutput . The Python binding returns a freshly allocated NumPy complex64 array; ownership is transferred to the caller.
Parameters:
stateInitialised Synth state returned bydp_wfm_synth_create.outputOutput buffer of at leastncf32 elements.nNumber of samples to generate.
function dp_wfm_synth_sweep_ends¶
Start and end frequency of the chirp that sweeps about a center. A chirp with freq = *f0 ,f_end = *f_end and dp_wfm_synth_set_chirp_span(n) has instantaneous frequencyf_mid + rate*u (Hz) at samplei ,u = (i - center)/fs seconds, and so multiplies a block byexp(j*2*pi*(f_mid*u + rate*u^2/2)) up to one constant phase (the synth's phase is 0 at sample 0, the formula's atu = 0 ). The synth's increment lands after its sample, hence the half-sample in*f0 . Pure arithmetic: no state, nothing allocated.
void dp_wfm_synth_sweep_ends (
double fs,
double f_mid,
double rate,
size_t n,
double center,
double * f0,
double * f_end
)
Parameters:
fsSample rate, Hz (> 0).f_midFrequency at the center, Hz.rateSweep, Hz per second.nSamples the sweep spans (pass to set_chirp_span).centerSample index ofu = 0(need not be an integer).f0Out: chirp start frequency, Hz.f_endOut: chirp end frequency, Hz.
function wfm_synth_bit_ahead¶
Whether the data has a next bit, without taking it.
The bounds check and refill wfm_synth_bit_next() is built on, so the two cannot disagree about where the data ends. A spent pattern draws its next frame here, so a caller that asks ahead draws that frame early: the continuous DSSS code-only window does, once a window, because a window is sent only before more data (wfm_synth_cont_dsss_chip()).
Parameters:
sthe synth; a type=bits synth with a pattern set.
Returns:
1 if a bit remains (the cursor at it), or 0: the data has ended.
function wfm_synth_bit_next¶
The next bit of the pattern, or none once the data has ended.
The cursor STOPS at n_bits; it never wraps (doppler#1718: the cycle that sent one pattern again and again is gone). With a refill, that one bounds check finds the frame boundary: the next frame is drawn lazily, when its first bit is due the source's counts are frames started, and a paced source is asked when the frame is due. A refill that reports the end latches data_ended, and so does the end of a pattern with no refill: it was the whole of the data, sent once. Either way every later call is the slow path's immediate "no bit", and the caller sends silence never a line nobody sent. The one place the cursor moves, so the per-sample and block paths cannot disagree about a frame boundary.
Parameters:
sthe synth; a type=bits synth with a pattern set.bitreceives the next bit, 0 or 1, when one is returned.
Returns:
1 with the bit in *bit, or 0: the data has ended.
function wfm_synth_bit_symbol¶
Next symbol from the user bit pattern — one mapping, every M.
The single home for the bits->symbol map. It had four copies: two in this header (wfm_synth_next_symbol and dp_wfm_synth_step) and two in dp_wfm_synth_steps(). wfm_synth_next_symbol's own comment says the kernel is shared "so the single-sample and block paths cannot diverge -- they call
the SAME function rather than each inlining the arithmetic", and the arithmetic was inlined four times anyway.
bit_mod is BITS PER SYMBOL, which is what its existing values already mean (1 = BPSK, 2 = QPSK), so M = 1 << bit_mod and 3 = 8PSK extends the numbering rather than reinterpreting it. One symbol's bits are read MSB-first into a Gray label and handed to mpsk_constellation() the library's canonical mapping, and the one dp_ber_score() inverts to score bit errors.
That shared mapping is the point. The QPSK branches this replaces put b0 on the I sign and b1 on the Q sign: the same CONSTELLATION, but two of the four labels swapped against mpsk_constellation(). Nothing scored a QPSK bit pattern against truth, so it never produced a wrong number but a framed QPSK stream read through the canonical scorer would have shown about half its symbols wrong on a perfectly working receiver, which is the plausible-number failure docs/design/rx-test.md exists to stop.
bit_mod == 0 is not PSK it is the 0/1 amplitude line this type has always emitted so it keeps its own branch.
Parameters:
sSynth state;bits/n_bitsmust be non-empty,bit_idxadvances.
Returns:
Unit-modulus constellation point (a unit-amplitude line at bit_mod == 0), which is what Synth's unit-power SNR reference needs.
function wfm_synth_bps¶
Bits carried by one symbol of type — thebps an Eb/No needs.
QPSK carries two, everything else one. DSSS is one because its payload is BPSK, which is what makes ebno == esno for a DSSS source.
function wfm_synth_cont_dsss_chip¶
One continuous-DSSS chip: code[n % n_code] ^ data , as a BPSK sign.
The per-chip kernel shared by dp_wfm_synth_step and dp_wfm_synth_steps (and the manifest impl), so the single-sample and block paths cannot diverge — they call the SAME function rather than each inlining the arithmetic. Advances the code clock (n % n_code) and the INDEPENDENT symbol clock (floor(n / chips_per_symbol)) off one running chip counter; at each symbol boundary it refreshes the data bit from the configured source (constant 0 for code-only, the next payload bit, or the next PN bit). Non-integer chips_per_symbol is what makes symbol edges land mid-epoch — the asynchronicity.
With a frame set (dp_wfm_synth_set_dsss_window), the frame lives on the SYMBOL clock: of every frame_symbols symbols, the first code_only_symbols carry data 0 — the pure code — and the rest carry the payload, whose index counts data symbols only, so the bits run on across frames. The symbol clock never restarts: it is the same free-running floor(n / chips_per_symbol) with or without a window, so a frame edge falls at whatever chip phase that clock puts it — the chip and data clocks have no fixed relation, and no frame edge is synchronous with a code epoch. frame_symbols == 0 is the windowless stream, bit for bit. A window opens only before more data: once a payload has sent its last bit the stream is silent from the next symbol on, window or not, so it ends where dp_wfm_source_data_samples() says its run does.
Requires chips_per_symbol >= 1 (chip rate >= symbol rate, always true for a real DSSS waveform), so the symbol index advances by 0 or 1 per chip and the PN is never asked to skip.
function wfm_synth_mls_poly¶
The MLS primitive polynomial table — pn's, reached by its old name.
The table itself moved to pn/pn_core.h (pn_mls_poly), because the convention it encodes is dp_pn_create()'s tap mask and not the synth's. This spelling is retained for the call sites that already use it; it forwards and holds no table of its own, so the two cannot disagree.
function wfm_synth_next_symbol¶
Pull the next constellation symbol from the active shaped source.
The single symbol-generation point the polyphase pulse shaper feeds from, dispatching on the waveform type exactly as dp_wfm_synth_step's symbol latch does — the PN LFSR (pn/bpsk one chip, qpsk two Gray chips), the user bit pattern (bits, per bit_mod, sent once), the continuous asynchronous DSSS chip, or the cycled complex-symbol stream — and advancing that source's read cursor by one symbol. Only the shaped types (pn/bpsk/qpsk/bits/symbols/dsss, the set dp_wfm_synth_set_rrc accepts) reach here, so the shaper draws the same symbol sequence the dense-FIR path would; only the pulse-shaping filter differs.
function wfm_synth_shape¶
Produce m polyphase-shaped baseband samples intoout .
JM_FORCEINLINE void wfm_synth_shape (
dp_wfm_synth_state_t * s,
float _Complex * out,
size_t m,
float _Complex * syms
)
The one shaping kernel shared by dp_wfm_synth_step (m == 1) and dp_wfm_synth_steps (m == block): prime once, generate exactly the dp_resamp_interp_inputs_needed(shaper, m) symbols this call consumes into the caller's syms scratch, and fill m outputs. Because the resampler is block-boundary invariant and both faces call this identical routine, a single m-sample call and m one-sample calls produce bit-identical output — the step()==steps() guarantee. Carrier mix and noise are applied by the caller.
Parameters:
sShaper-attached synth state (s->shaper != NULL).outOutput buffer, capacity >=m.mNumber of baseband samples to produce.symsCaller scratch, capacity >= dp_resamp_interp_inputs_needed(s, m).
function wfm_synth_shaper_prime¶
Prime the shaper's delay line so its output aligns with the dense FIR.
The polyphase interpolator emits its first meaningful sample only after the delay line fills, so its output lags the dense-FIR path by exactly nsps samples. Discarding that many leading outputs once, at stream start (which consumes exactly the first source symbol into the delay line), realigns the shaped waveform to the dense path to float precision — so switching a source to polyphase shaping does not shift downstream sample timing. Idempotent via the primed flag; re-armed by dp_wfm_synth_reset.
function wfm_synth_snr_over_fs¶
Convert a per-symbol or per-bit SNR to SNR over the full sample rate.
The one place this arithmetic lives. A noise amplitude is always referenced to fs, so every SNR mode is a conversion into that: an Es/N0 spreads the symbol's energy over span samples, and an Eb/No does the same after first multiplying by the bits the symbol carries. Getting it wrong is silent — the waveform is still a waveform, at an SNR nobody asked for — so having it written twice is how a generator and a composer come to place different noise for the same requested number.
Parameters:
modeRESOLVED mode: 1 fs, 2 Eb/No, 3 Es/No. Never 0 (auto) — see below.bpsBits per symbol, from wfm_synth_bps().spanSamples one symbol's energy is spread over.snrThe requested figure, in dB, inmode'sreference.
Returns:
SNR in dB over fs, ready for dp_awgn_amplitude_for_snr().
**auto and span are deliberately the CALLER's**, and that is not an oversight: they are the two things that legitimately differ. wfm_synth resolves auto to fs for a DSSS source because at create() time it cannot do better — the codes attach afterwards, so the spreading factor that sets the symbol span is not yet known — while the composer resolves the same source to Es/No and passes the true span (sf * sps for a burst, or fs/symbol_rate for a continuous asynchronous stream, which coincide only in the synchronous case that mode exists to avoid). Those differences are inputs, not a second formula.
// Es/No 12 dB at 8 samples/symbol -> 2.969 dB over fs
double fs_db = wfm_synth_snr_over_fs (3, 1, 8.0, 12.0);
// the same figure read as Eb/No on QPSK is 3.010 dB hotter
double eb_db = wfm_synth_snr_over_fs (2, wfm_synth_bps (WFM_SYNTH_QPSK),
8.0, 12.0);
Macro Definition Documentation¶
define WFM_DSSS_ENDED¶
cur_data once a continuous stream's data has ended: not a bit, so the chips are silent from then on, and it is a byte of the serialized state like the bit it replaces.
define WFM_SYNTH_SNR_CLEAN¶
define WFM_SYNTH_STATE_MAGIC¶
define WFM_SYNTH_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/doppler/wfm_synth/wfm_synth_core.h