File wfm_compose.h¶
FileList > doppler > wfm > wfm_compose.h
Go to the source code of this file
Multi-segment waveform composer (Phase B). More...
#include "doppler/clib_common.h"#include "doppler/wfm_synth/wfm_synth_core.h"#include "doppler/wfm/wfm_frame.h"#include "doppler/wfm/wfm_data.h"#include "doppler/doppler_channel/doppler_channel_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | wfm_draw_t One rendered source instance: its timing AND the values it was actually rendered with. |
| struct | wfm_segment_t One composer segment: one or more sources summed over the same span, then a trailing off-time gap. |
| struct | wfm_source_t One additive source within a segment: a synth config + its level. |
| struct | wfm_span_t One rendered segment instance's exact timing: where it lands in the composed stream and how its delay | on | off spans divide it. |
Public Types¶
| Type | Name |
|---|---|
| typedef struct wfm_compose_state | dp_wfm_compose_state_t |
| enum | wfm__compose_8h_1abc5c98fcc1211af2b80116dd6e0a035d Per-field "draw uniformly each repeat" flags ( ranged bitmask). |
| enum | wfm_bitmod_t How a WFM_SYNTH_BITS source maps its payload to symbols. |
| enum | wfm_doppler_lifetime_t When a source's Doppler channel restarts. |
| typedef void(* | wfm_feed_fn Where a channel renderer's INPUT comes from, when it is not a synth. |
| typedef struct wfm_render | wfm_render_t One source's renderer: its synth, plus its Doppler channel. |
| enum | wfm_seed_advance_t Per-repeat seed policy for a looped/continuous stream. |
| enum | wfm_snr_mode_t What a source's snr is measured against. |
Public Attributes¶
| Type | Name |
|---|---|
| const char | dp_wfm_why_count_derived The reason dp_wfm_scene_error() gives a count beside a segment whose sources set its length (dp_wfm_segment_sets_length() ) exported so the wfmgen CLI can name its flag beside it. |
| const char | dp_wfm_why_dsss_cont_no_data_code The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity. |
| const char | dp_wfm_why_dsss_cont_rate The reason dp_wfm_scene_error() gives a continuous dsss source whose chip rate is below its symbol rate exported so the wfmgen CLI can name the values beside it, by identity. |
| const char | dp_wfm_why_dsss_empty The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity. |
| const char | dp_wfm_why_dsss_frame_no_data_code The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity. |
| const char | dp_wfm_why_pn_poly The reason dp_wfm_source_error() gives for a pn_poly wider than its register exported so a face that knows the values (the wfmgen CLI) can name them beside it, by identity rather than by matching text. |
| const char | dp_wfm_why_retired_bits The reason dp_wfm_source_error() gives for retired_bits set: Python's retiredbits= (andpayload= ,pattern= ), named once. The CLI's and a scene's RETIRED tables say the same in their own spelling (doppler#1718). |
| const char | dp_wfm_why_retired_crc |
| const char | dp_wfm_why_retired_sync The reasons dp_wfm_source_error() gives for retired_sync andretired_crc set. |
Public Functions¶
| Type | Name |
|---|---|
| wfm_render_t * | dp_wfm_compose_build_render (const wfm_source_t * src, double fs, size_t on_len, double freq, double snr, double f_end, double doppler, double doppler_rate, unsigned epoch, int seed_advance, size_t instance, dp_doppler_channel_state_t * borrow) Build a source's renderer — dp_wfm_compose_build_synth plus the clock-Doppler channel the source declares, if it declares one. |
| dp_wfm_synth_state_t * | dp_wfm_compose_build_synth (const wfm_source_t * src, double fs, size_t on_len, double freq, double snr, double f_end, unsigned epoch, int seed_advance, size_t instance) Construct + configure the synth for one resolved source. |
| dp_wfm_compose_state_t * | dp_wfm_compose_create (const wfm_segment_t * segs, size_t n_segs, int repeat, int continuous) Build a composer over a copy of segs . |
| dp_wfm_compose_state_t * | dp_wfm_compose_create_why (const wfm_segment_t * segs, size_t n_segs, int repeat, int continuous, const char ** why) dp_wfm_compose_create() , able to say why the scene was refused. |
| void | dp_wfm_compose_destroy (dp_wfm_compose_state_t * state) Destroy a composer and its active synth. |
| size_t | dp_wfm_compose_draws (const wfm_segment_t * segs, size_t n_segs, wfm_draw_t * out, size_t cap) Replay the (epoch 0) instance timeline AND its drawn source values. |
| size_t | dp_wfm_compose_execute (dp_wfm_compose_state_t * state, float _Complex * out, size_t max) Emit up to max samples of the composed stream. |
| size_t | dp_wfm_compose_execute_rate (dp_wfm_compose_state_t * state, float _Complex * out, size_t max, double * fs) Emit up to max samples, all at ONE sample rate, and say which. |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_file (const char * path) |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_file_why (const char * path, const char ** why) dp_wfm_compose_from_file , able to say why a scene was refused. |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_json (const char * json) Build a composer from a JSON spec string (for from-file). |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_json_at (const char * json, const char * base, const char ** why) Build a composer from a JSON spec file. |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_json_data (const char * json, const char * base, const char * data_file, const char ** why) dp_wfm_compose_from_json_at() , replaying a record by its data. |
| dp_wfm_compose_state_t * | dp_wfm_compose_from_json_why (const char * json, const char ** why) The same, but able to say why a FRAME was refused. |
| dp_doppler_channel_state_t * | dp_wfm_compose_persist_channel (const wfm_source_t * src, double fs, double doppler, double doppler_rate, dp_doppler_channel_state_t ** slot) The PERSIST channel for one (segment, source) slot, created on first use, or NULL when the source does not persist. |
| int | dp_wfm_compose_seed_advance (const dp_wfm_compose_state_t * state) The composer's current seed-advance mode (a wfm_seed_advance_t ). |
| const wfm_segment_t * | dp_wfm_compose_segments (const dp_wfm_compose_state_t * state, size_t * n_out, int * repeat, int * continuous) Borrow the composer's stored segment list (for record / SigMF). |
| void | dp_wfm_compose_set_data_pacing (dp_wfm_compose_state_t * state, wfm_data_pacing_t pacing) Pace a composer's data sources: WFM_DATA_PACED under --realtime . |
| void | dp_wfm_compose_set_seed_advance (dp_wfm_compose_state_t * state, int mode) Choose how the seed advances on each repeat of a looped/continuous stream (a wfm_seed_advance_t ): |
| size_t | dp_wfm_compose_spans (const wfm_segment_t * segs, size_t n_segs, wfm_span_t * out, size_t cap) Replay the (epoch 0) instance timeline of a resolved segment list. |
| char * | dp_wfm_draws_json (const wfm_segment_t * segs, size_t n_segs) The same rows dp_wfm_compose_draws() reports, as a JSON array. |
| wfm_frame_desc_t * | dp_wfm_frame_copy (const wfm_frame_desc_t * d) Deep-copy a description: the struct and each literal field's bits. |
| void | dp_wfm_frame_free (wfm_frame_desc_t * d) Free a description returned by dp_wfm_frame_from_json() , bits and all. NULL is a no-op. |
| wfm_frame_desc_t * | dp_wfm_frame_from_json (const char * json, const char ** why) Read a frame description from its JSON form. |
| wfm_frame_desc_t * | dp_wfm_frame_refuse_text (const char * text, const char ** why) Refuse text for a source's frame= : it takes a description. |
| char * | dp_wfm_frame_to_json (const wfm_frame_desc_t * d) Write a description as its JSON frame object, the text dp_wfm_frame_from_json() reads back. |
| const char * | dp_wfm_framing_flags_error (const wfm_source_t * src) Why --sync /--crc /--acq-code cannot frame this source, or NULL. |
| void | dp_wfm_render_destroy (wfm_render_t * r) Free a renderer and everything it owns. NULL-safe. |
| wfm_render_t * | dp_wfm_render_from_feed (const wfm_source_t * src, double fs, double doppler, double doppler_rate, dp_doppler_channel_state_t * borrow, size_t delay, size_t on, wfm_feed_fn feed, void * feed_ctx) A renderer whose input is a caller-supplied feed, not a live synth. |
| void | dp_wfm_render_noise_steps (wfm_render_t * r, float _Complex * dst, size_t n) Pull n samples of the source's NOISE FLOOR only, through the same channel. |
| void | dp_wfm_render_set_input_timeline (wfm_render_t * r, size_t delay, size_t on) Tell a Doppler renderer where its INPUT's phases fall. |
| void | dp_wfm_render_steps (wfm_render_t * r, float _Complex * dst, size_t n) Pull exactly n samples fromr , through its channel if any. |
| int | dp_wfm_resolve_noise (wfm_segment_t * segs, size_t n) Resolve a segment list's noise model in place (Phase 4b). |
| const char * | dp_wfm_scene_error (const wfm_segment_t * segs, size_t n_segs, int repeat, int continuous) Why a scene cannot be composed, or NULL: the one validator. |
| int | dp_wfm_segment_sets_length (const wfm_segment_t * seg) Whether a segment's sources SET its on-time, so its num_samples is derived and a count given beside them refused. |
| double | dp_wfm_snr_over_fs (int snr_mode, int type, int sps, size_t sf, double sym_span, double snr) SNR (dB) referred to fs, from a source's snr/snr_mode/sps/type. |
| int | dp_wfm_source_attach_dsss (dp_wfm_synth_state_t * syn, const wfm_source_t * src, double fs) Attach a dsss source's data to a freshly-created synth. |
| int | dp_wfm_source_attach_frame (dp_wfm_synth_state_t * syn, const wfm_source_t * src) Attach an unspread source's bit pattern, framed or not. |
| size_t | dp_wfm_source_bits_refuse_text (const char * text, uint8_t * out, size_t max_out, const char ** why) Refuse text for a source's bit field: an object takes bits. |
| int | dp_wfm_source_common_frame (const wfm_source_t * src, const wfm_seq_t * sync, int crc, wfm_frame_desc_t * d) The common frame as a description: [preamble x reps | sync | data | crc] , built by the ONE function that builds it. |
| size_t | dp_wfm_source_crc_refuse_text (const char * text, uint8_t * out, size_t max_out, const char ** why) Refuse text for the retired crc= : a CRC is a stage of the frame description. |
| double | dp_wfm_source_create_snr (const wfm_source_t * src, double fs, double snr, int * snr_mode) Resolve a source's (snr, snr_mode) into the pair to hand to dp_wfm_synth_create() . |
| uint64_t | dp_wfm_source_data_frames (const wfm_source_t * src) Frames a FINITE data source makes, ceil(bits / LEN) ; 0 for a stream or none. On continuous dsss, which has no frame, its bits. |
| int | dp_wfm_source_data_is_stream (const wfm_source_t * src) Whether a source's data is a stream: data_from_file is- . |
| uint64_t | dp_wfm_source_data_samples (const wfm_source_t * src, double fs, uint64_t frames) Samples the first frames frames of a source's data occupy; 0 with no data. |
| double | dp_wfm_source_dsss_cps (const wfm_source_t * src, double fs) Chips per data symbol of a CONTINUOUS dsss source at fs . |
| size_t | dp_wfm_source_dsss_nchips (const wfm_source_t * src) Chips one DSSS BURST from this source occupies, description and all. |
| const char * | dp_wfm_source_error (const wfm_source_t * src) NULL when this source can be built; else why not, as a sentence. |
| const char * | dp_wfm_source_frame_error (const wfm_source_t * src) NULL when this source's frame fields can be honoured; else why not. |
| int | dp_wfm_source_has_frame (const wfm_source_t * src) Non-zero when this source describes a FRAME. |
| size_t | dp_wfm_source_sync_refuse_text (const char * text, uint8_t * out, size_t max_out, const char ** why) Refuse text for the retired sync= : a sync word is a field of the frame description. |
| int | dp_wfm_source_synth_type (const wfm_source_t * src) The synth type to create this source with. |
| const char * | dp_wfm_source_to_synth_error (const wfm_source_t * src, double fs) Why dp_wfm_source_to_synth() refused this source, or NULL. |
| double | dp_wfm_spec_headroom (const char * json) The top-level headroom (dB) from a spec JSON, or 0 if absent. |
| char * | dp_wfm_spec_template_json (void) A ready-to-edit example spec in the canonical from-file schema. |
| char * | dp_wfm_spec_to_json (const wfm_segment_t * segs, size_t n_segs, int repeat, int continuous, int seed_advance, double headroom) Serialise a spec to a JSON string (for record). |
| int | dp_wfm_synth_attach_data (dp_wfm_synth_state_t * syn, const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, wfm_data_src_t * src, wfm_data_pacing_t pacing, int modulation) Drive a type=bits synth from a frame whose payload is a data source. |
| const wfm_data_src_t * | dp_wfm_synth_data_source (const dp_wfm_synth_state_t * syn) The data source a synth pulls from (dp_wfm_synth_attach_data), or NULL: its stats are the run's truth for scoring. |
| void | dp_wfm_synth_set_data_pacing (dp_wfm_synth_state_t * syn, wfm_data_pacing_t pacing) Pace a synth's data source: WFM_DATA_PACED for --realtime . |
Public Static Functions¶
| Type | Name |
|---|---|
| double | dp_wfm_scene_fs (const wfm_segment_t * segs, size_t n_segs) The ONE answer to "what is this stream's sample rate": the fs every segment shares, or 0.0 when they differ. |
Macros¶
| Type | Name |
|---|---|
| define | WFM_NUM_SAMPLES_PLAIN ((size\_t)1024)A plain segment's on-time when its num_samples is 0 and no source sets one: what--count , a scene andSegment default to. |
Detailed Description¶
Sequences a list of segments — each one a synth configuration plus an on-time and a trailing off-time gap — into a single IQ stream, optionally repeating the whole sequence or running forever. The composer owns one synth at a time (the active segment) and reuses the Phase-A engine verbatim, so every waveform type / SNR mode / MLS behaviour is identical to the single-waveform path; a one-segment spec is byte-identical to calling synth directly.
Lifecycle: dp_wfm_compose_create -> dp_wfm_compose_execute* -> dp_wfm_compose_destroy
wfm_source_t tone = {.type = 0, .freq = 1e5, .snr = 100.0};
wfm_source_t qpsk = {.type = 4, .sps = 8, .snr = 9.0};
wfm_segment_t segs[2] = {
{.sources = &tone, .n_sources = 1, .fs = 1e6,
.num_samples = 1000, .off_samples = 500}, // tone, then a gap
{.sources = &qpsk, .n_sources = 1, .fs = 1e6,
.num_samples = 4096, .off_samples = 0}, // qpsk
};
dp_wfm_compose_state_t *c = dp_wfm_compose_create(segs, 2, 0, 0);
float _Complex buf[4096];
size_t n;
while ((n = dp_wfm_compose_execute(c, buf, 4096)) > 0) { ... }
dp_wfm_compose_destroy(c);
Public Types Documentation¶
typedef dp_wfm_compose_state_t¶
Opaque composer state.
enum wfm__compose_8h_1abc5c98fcc1211af2b80116dd6e0a035d¶
Per-field "draw uniformly each repeat" flags ( ranged bitmask).
enum wfm__compose_8h_1abc5c98fcc1211af2b80116dd6e0a035d {
WFM_RANGE_FREQ = 1u << 0,
WFM_RANGE_SNR = 1u << 1,
WFM_RANGE_LEVEL = 1u << 2,
WFM_RANGE_FEND = 1u << 3,
WFM_RANGE_NUM_SAMPLES = 1u << 4,
WFM_RANGE_OFF_SAMPLES = 1u << 5,
WFM_RANGE_DELAY_SAMPLES = 1u << 6,
WFM_RANGE_DOPPLER = 1u << 7,
WFM_RANGE_DOPPLER_RATE = 1u << 8
};
A scalar field is a constant; a ranged field carries a [lo, hi] span (the scalar holds lo, a companion *_hi holds hi) and is redrawn uniformly in [lo, hi] at the start of every repeat (composer epoch) — so a looped / continuous stream can vary Doppler (freq), arrival jitter (off_samples), etc. burst-to-burst while staying reproducible: the draw is a deterministic hash of the source seed, the epoch, the segment/source index, and the field, so --record stores the span (not a drawn value) and --from-file replays the same sequence byte-for-byte. Bits 0–3 and 7–8 live on wfm_source_t.ranged; bits 4–6 on wfm_segment_t.ranged.
enum wfm_bitmod_t¶
How a WFM_SYNTH_BITS source maps its payload to symbols.
Order IS the wire value; BITMOD_NAMES[] and the [[enum]] bitmod manifest are held to this by make lint-wfm-enum-tables.
enum wfm_doppler_lifetime_t¶
When a source's Doppler channel restarts.
Neither is a superset of the other, so it is declared rather than defaulted into an argument:
PER_INSTANCE(default) restarts the geometry for every burst instance, which is the repeated-trial shape — every burst sees the same pass, and it composes with the per-instance re-draw of a rangeddoppler.PERSISTcarries one emitter's motion across every REPEAT INSTANCE of its segment, and across the gaps between them, so burst k sees where the pass has got to. It is the only lifetime under whichdoppler_ratemeans anything over a multi-burst scene.
The channel is keyed by (segment, source), because that is the only source identity the composer has — a position. So a PERSIST source persists over its own segment's instances; two DIFFERENT segments each get their own pass, even where a reader might call them the same emitter. Sharing one across segments needs a declared source id, which nothing in the scene format carries yet; gh-942 says as much ("no per-source identity that survives it ... the repeats/epoch machinery is where one would hang").
typedef wfm_feed_fn¶
Where a channel renderer's INPUT comes from, when it is not a synth.
Called with the next n input samples wanted, signal non-zero for ON samples and zero for noise-only ones (the delay and the gap), in INPUT order: the ON calls together cover the burst exactly once, front to back.
Parameters:
ctxThe pointer given to dp_wfm_render_from_feed().dstWritensamples here.nSamples wanted.signalNon-zero: ON samples. Zero: noise-only.
typedef wfm_render_t¶
One source's renderer: its synth, plus its Doppler channel.
enum wfm_seed_advance_t¶
Per-repeat seed policy for a looped/continuous stream.
enum wfm_seed_advance_t {
WFM_SEED_ADVANCE_NONE = 0,
WFM_SEED_ADVANCE_NOISE = 1,
WFM_SEED_ADVANCE_ALL = 2
};
A source's single seed feeds two RNGs: the PN LFSR (spreading code and data bits — one register) and the AWGN generator. The clean cut is therefore signal (code+data) vs. noise, exposed as an ordered, cumulative level.
enum wfm_snr_mode_t¶
What a source's snr is measured against.
The scale a number in dB is quoted on is not a detail a caller can infer, and it changes the noise by 10log10(sps) between fs and esno. Naming the modes is what lets a downstream write the mode it means instead of a literal whose meaning lives in a comment. Order IS the wire value — the [[enum]] snr_mode manifest and MODE_NAMES[] in wfm_names.h are held to this by make lint-wfm-enum-tables.
Public Attributes Documentation¶
variable dp_wfm_why_count_derived¶
The reason dp_wfm_scene_error() gives a count beside a segment whose sources set its length (dp_wfm_segment_sets_length() ) exported so the wfmgen CLI can name its flag beside it.
variable dp_wfm_why_dsss_cont_no_data_code¶
The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity.
variable dp_wfm_why_dsss_cont_rate¶
The reason dp_wfm_scene_error() gives a continuous dsss source whose chip rate is below its symbol rate exported so the wfmgen CLI can name the values beside it, by identity.
variable dp_wfm_why_dsss_empty¶
The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity.
variable dp_wfm_why_dsss_frame_no_data_code¶
The reasons dp_wfm_source_error() gives a dsss source missing a code (doppler#1696): a burst whose frame has no data_code to spread it, a burst with neither a preamble nor a frame, and a continuous stream with no data_code. Exported so a test or a face can hold a refusal to its reason by identity.
variable dp_wfm_why_pn_poly¶
The reason dp_wfm_source_error() gives for apn_poly wider than its register exported so a face that knows the values (the wfmgen CLI) can name them beside it, by identity rather than by matching text.
variable dp_wfm_why_retired_bits¶
The reason dp_wfm_source_error() gives forretired_bits set: Python's retiredbits= (andpayload= ,pattern= ), named once. The CLI's and a scene's RETIRED tables say the same in their own spelling (doppler#1718).
variable dp_wfm_why_retired_crc¶
variable dp_wfm_why_retired_sync¶
The reasons dp_wfm_source_error() gives forretired_sync andretired_crc set.
One sentence each, naming frame=; the CLI and a scene say the same in their own spelling. Exposed so a test can pin the wording once.
Public Functions Documentation¶
function dp_wfm_compose_build_render¶
Build a source's renderer — dp_wfm_compose_build_synth plus the clock-Doppler channel the source declares, if it declares one.
wfm_render_t * dp_wfm_compose_build_render (
const wfm_source_t * src,
double fs,
size_t on_len,
double freq,
double snr,
double f_end,
double doppler,
double doppler_rate,
unsigned epoch,
int seed_advance,
size_t instance,
dp_doppler_channel_state_t * borrow
)
THE pull path. Both faces go through dp_wfm_render_steps() rather than calling dp_wfm_synth_steps() themselves, because a Doppler channel is a RESAMPLER: it consumes about n*(1+d) inputs per n outputs, so "pull
k, get k" only holds if something keeps the remainder. Two implementations that agreed today would drift the moment either grew a holdover the other did not.
A source with doppler == 0 && doppler_rate == 0 gets no channel and dp_wfm_render_steps() is then literally dp_wfm_synth_steps(), so every scene that does not ask for Doppler renders through exactly the path it always did — byte-identical, not merely equivalent.
doppler/doppler_rate arrive ranged-resolved, like freq/snr/f_end.
borrow is the channel a WFM_DOPPLER_PERSIST source keeps ACROSS segments: the composer owns it for the life of the scene and passes it in here, so the renderer uses it without adopting it and the geometry does not restart when the synth is torn down at a segment boundary. NULL means the ordinary case — the renderer creates and owns a channel if the source declares Doppler, and destroys it with itself.
Returns:
A heap renderer (caller dp_wfm_render_destroy()s it), or NULL.
function dp_wfm_compose_build_synth¶
Construct + configure the synth for one resolved source.
dp_wfm_synth_state_t * dp_wfm_compose_build_synth (
const wfm_source_t * src,
double fs,
size_t on_len,
double freq,
double snr,
double f_end,
unsigned epoch,
int seed_advance,
size_t instance
)
THE single synth-construction path (create + chirp-span pin + bits/symbols/RRC attach + per-repeat NOISE reseed) shared by the streaming composer and the Plan stimulus cache, so a cached per-source render is byte-identical to the composed one. freq/snr/f_end are passed already ranged-resolved by the caller; on_len pins a chirp's sweep to the on-time; epoch/seed_advance (a wfm_seed_advance_t) drive the per-repeat seed policy — epoch == 0 yields the unmodified seed. instance is the segment's repeats counter (0-based): a non-zero instance always reseeds the AWGN (fresh noise per burst instance, signal fixed, regardless of seed_advance); instance 0 is byte-identical to the pre-repeats behaviour.
Returns:
A heap synth (caller dp_wfm_synth_destroy()s it), or NULL on failure.
function dp_wfm_compose_create¶
Build a composer over a copy of segs .
dp_wfm_compose_state_t * dp_wfm_compose_create (
const wfm_segment_t * segs,
size_t n_segs,
int repeat,
int continuous
)
Parameters:
segsSegment list (copied; caller keeps ownership).n_segsNumber of segments (>= 1).repeatNon-zero: loop the whole sequence after the last segment.continuousNon-zero: never finish (implies repeat); execute always returnsmax.
Returns:
Heap state, or NULL on bad args / allocation / synth failure.
Note:
Caller must dp_wfm_compose_destroy() when done.
function dp_wfm_compose_create_why¶
dp_wfm_compose_create() , able to say why the scene was refused.
dp_wfm_compose_state_t * dp_wfm_compose_create_why (
const wfm_segment_t * segs,
size_t n_segs,
int repeat,
int continuous,
const char ** why
)
The scene is asked dp_wfm_scene_error() before anything is built, and its sentence is what why receives the same sentence the wfmgen CLI, a scene read by dp_wfm_compose_from_json_why() and the standalone Synth report, because it is the same validator. It is the create the generated Composer([...]) calls (just-makeit's create_why), so a refused composer raises ValueError(<the reason>).
Parameters:
segsas for dp_wfm_compose_create().n_segsas for dp_wfm_compose_create().repeatas for dp_wfm_compose_create().continuousas for dp_wfm_compose_create().whyoptional; receives a STATIC reason when the scene is refused, and is left as it was in every other case (success, bad arguments, an allocation or synth failure).
Returns:
Heap state, or NULL as for dp_wfm_compose_create().
wfm_source_t src = { .type = WFM_SYNTH_DSSS, .sps = 2 }; // no codes
wfm_segment_t seg = { .sources = &src, .n_sources = 1, .fs = 1e6,
.num_samples = 64 };
const char *why = NULL;
dp_wfm_compose_state_t *c = dp_wfm_compose_create_why (&seg, 1, 0, 0, &why);
if (c != NULL || why != dp_wfm_why_dsss_empty) // refused, and says why
return 1;
function dp_wfm_compose_destroy¶
Destroy a composer and its active synth.
Parameters:
stateMay be NULL.
function dp_wfm_compose_draws¶
Replay the (epoch 0) instance timeline AND its drawn source values.
size_t dp_wfm_compose_draws (
const wfm_segment_t * segs,
size_t n_segs,
wfm_draw_t * out,
size_t cap
)
Same size-then-fill protocol as dp_wfm_compose_spans(): call once with cap 0 to size, then again with a buffer. Emits one row per SOURCE per instance, in stream order, because that is the granularity a per-source annotation or a scoring pipeline needs. Pass the RESOLVED segments (dp_wfm_compose_segments() on a live composer) so intrinsic on-times are already folded in.
The rows are produced by the same dp_wfm_draw_segment()/dp_wfm_draw_source() calls the renderer resolves through, so a field added to the draw reaches both by construction rather than by a reviewer noticing.
Parameters:
segsResolved segment array.n_segsSegment count.outRow buffer (may be NULL when cap is 0).capCapacity of out in rows.
Returns:
Total rows in one pass of the spec (sum of n_sources over instances), regardless of cap.
function dp_wfm_compose_execute¶
Emit up to max samples of the composed stream.
Returns:
Number of samples written: < max (or 0) signals the sequence finished (never, when continuous).
function dp_wfm_compose_execute_rate¶
Emit up to max samples, all at ONE sample rate, and say which.
size_t dp_wfm_compose_execute_rate (
dp_wfm_compose_state_t * state,
float _Complex * out,
size_t max,
double * fs
)
dp_wfm_compose_execute() for an output that states a rate per block: it stops early where the next segment's fs differs from the samples already written, so every block it returns has one rate. A scene whose segments share an fs never stops early, and its samples are the same, byte for byte, as dp_wfm_compose_execute()'s. A short return therefore does NOT mean the scene finished; 0 does.
wfm_source_t src = { .type = WFM_SYNTH_TONE, .snr = 100.0 };
wfm_segment_t segs[2] = {
{ .sources = &src, .n_sources = 1, .fs = 6e6, .num_samples = 8 },
{ .sources = &src, .n_sources = 1, .fs = 2e6, .num_samples = 8 },
};
dp_wfm_compose_state_t *c = dp_wfm_compose_create (segs, 2, 0, 0);
float _Complex buf[64];
double fs = 0.0;
size_t a = dp_wfm_compose_execute_rate (c, buf, 64, &fs); // 8 at 6e6
int ok = a == 8 && fs == 6e6;
size_t b = dp_wfm_compose_execute_rate (c, buf, 64, &fs); // 8 at 2e6
ok = ok && b == 8 && fs == 2e6;
ok = ok && dp_wfm_compose_execute_rate (c, buf, 64, &fs) == 0;
dp_wfm_compose_destroy (c);
return ok ? 0 : 1;
Parameters:
statethe composer.outdestination,maxsamples.maxcapacity ofout.fsreceives the rate of the samples written (left untouched when none are).
Returns:
samples written; 0 when the scene has finished.
function dp_wfm_compose_from_file¶
function dp_wfm_compose_from_file_why¶
dp_wfm_compose_from_file , able to say why a scene was refused.
Reads path and hands its text to dp_wfm_compose_from_json_why, so the reason is that function's: a retired key or a refused frame, named. A file that cannot be read gives NULL and leaves why NULL.
Parameters:
paththe spec file.whyoptional; as for dp_wfm_compose_from_json_why.
Returns:
Composer state, or NULL on read/parse error / a refused scene.
function dp_wfm_compose_from_json¶
Build a composer from a JSON spec string (for from-file).
Returns:
Composer state, or NULL on parse error / bad type / no segments.
function dp_wfm_compose_from_json_at¶
Build a composer from a JSON spec file.
dp_wfm_compose_state_t * dp_wfm_compose_from_json_at (
const char * json,
const char * base,
const char ** why
)
Returns:
Composer state, or NULL on read/parse error.
dp_wfm_compose_from_json_why(), reading a scene that lives in base.
A source's relative "data_from_file" is the scene's: it resolves against base, the directory the scene was read from, so a scene and its data move together and replay from anywhere. NULL (as from_json_why passes) leaves a relative path relative to the caller's working directory.
Parameters:
jsonthe scene's text.basethe scene's directory, or NULL.whyoptional; receives a static reason for a refusal.
Returns:
the composer, or NULL.
const char *why = NULL;
dp_wfm_compose_state_t *c = dp_wfm_compose_from_json_at (
"{\"segments\":[{\"type\":\"tone\",\"num_samples\":8}]}", ".",
&why);
if (!c)
return 1;
dp_wfm_compose_destroy (c);
function dp_wfm_compose_from_json_data¶
dp_wfm_compose_from_json_at() , replaying a record by its data.
dp_wfm_compose_state_t * dp_wfm_compose_from_json_data (
const char * json,
const char * base,
const char * data_file,
const char ** why
)
A --record stores each data source's truth as "data_sent": the frames, the fill bits padding the last, the idle frames, and for a file or stdin the bits read and their dp_hash64 (payload-data-source.md §4.8). A replay identifies a file by that content, not by its name:
- a
"data_from_file"whose"data_sent"carries a hash is refused unless the file's length in bits and hash are the record's. The reason names the file and both hashes; - a
"data_from_file": "-"is a run read from stdin, whose octets are gone. It is refused unlessdata_filenames the file that held them, which is then checked the same way.-again is refused: a pipe could only be checked after it had been sent; - a
data_filethat no"-"source takes is refused: a scene carries its own data.
Every check is made before anything is built. A scene with no hash (one written by hand) replays its files unchecked.
Parameters:
jsonthe scene's text.basethe scene's directory, or NULL.data_filethe file a record's stdin is replayed from (--data-from-filegiven again), as typed: relative to the working directory, not tobase. NULL for none.whyoptional; receives the reason for a refusal. A mismatch's names the numbers, so it is formatted into a thread-local buffer, valid until this thread reads another scene; never freed by the caller.
Returns:
the composer, or NULL.
const char *why = NULL;
dp_wfm_compose_state_t *c = dp_wfm_compose_from_json_data (
"{\"segments\":[{\"type\":\"bits\",\"data_from_file\":\"-\"}]}",
NULL, NULL, &why);
if (c || !why) // stdin with no file given again: refused, with a reason
return 1;
function dp_wfm_compose_from_json_why¶
The same, but able to say why a FRAME was refused.
A spec is the interface most likely to be hand-written, and a NULL return is the one answer that cannot teach anything. This runs dp_wfm_source_frame_error over every parsed source before handing them to the composer — which asks the same question and would refuse either way — so the reason survives the boundary as a sentence instead of a pointer.
Only the frame rule reports this way. A parse error or a bad type is still a bare NULL, because those are cJSON's to describe and duplicating its diagnostics here would be a second opinion about the same text.
Parameters:
jsonthe spec.whyoptional; receives a STATIC message when a source's frame is refused, or NULL in every other case (including success). Passing NULL makes this exactly dp_wfm_compose_from_json.
Returns:
Composer state, or NULL on parse error / bad type / no segments / a refused frame.
function dp_wfm_compose_persist_channel¶
The PERSIST channel for one (segment, source) slot, created on first use, or NULL when the source does not persist.
dp_doppler_channel_state_t * dp_wfm_compose_persist_channel (
const wfm_source_t * src,
double fs,
double doppler,
double doppler_rate,
dp_doppler_channel_state_t ** slot
)
The one place that decides "does this instance borrow a scene-owned
channel": the composer and the Plan both call it, so they cannot disagree. NULL for a per_instance source, for a NULL slot, and for an instance that drew no Doppler at all (the channel then does not exist for it).
Parameters:
srcThe source.fsSample rate, Hz.dopplerThis instance's drawn Doppler, ppm.doppler_rateThis instance's drawn Doppler rate, ppm/s.slotThe scene-owned slot for this (segment, source).
function dp_wfm_compose_seed_advance¶
The composer's current seed-advance mode (a wfm_seed_advance_t ).
The composer is the SSOT for it: --from-file sets it from the spec and the flag path sets it from --seed-advance, so a serialiser must read it back from here rather than from whichever half happened to supply it.
Parameters:
stateCompose state (may be NULL →WFM_SEED_ADVANCE_NONE).
function dp_wfm_compose_segments¶
Borrow the composer's stored segment list (for record / SigMF).
const wfm_segment_t * dp_wfm_compose_segments (
const dp_wfm_compose_state_t * state,
size_t * n_out,
int * repeat,
int * continuous
)
Each source's data_sent holds what its data source has sent so far in the latest instance, brought up to date by this call, so a record or a SigMF sidecar written after a run carries the run's truth.
Parameters:
statethe composer.n_outreceives the segment count.repeatreceives the repeat flag (may be NULL).continuousreceives the continuous flag (may be NULL).
Returns:
Pointer to the internal segments (owned by the composer; valid until dp_wfm_compose_destroy).
function dp_wfm_compose_set_data_pacing¶
Pace a composer's data sources: WFM_DATA_PACED under --realtime .
Applied to the synths already built create builds the first segment's, before this can be called and to every synth built from here on, so a data stream with nothing yet sends an idle frame of fill rather than waiting (dp_wfm_data_frame, the one rule). The default, WFM_DATA_UNPACED, waits.
function dp_wfm_compose_set_seed_advance¶
Choose how the seed advances on each repeat of a looped/continuous stream (a wfm_seed_advance_t ):
WFM_SEED_ADVANCE_NONE(default): byte-identical repeats.WFM_SEED_ADVANCE_NOISE: advance only the AWGN seed → a fresh noise realization each pass while the signal (LO / PN code / data / pulse) stays bit-identical (so a fixed preamble/code re-acquires every burst).WFM_SEED_ADVANCE_ALL: advance the whole seed → code, data, and noise all change (a fully stochastic stream).
Set before the first execute(); the first pass is always unchanged. An out-of-range mode is ignored.
Parameters:
stateCompose state (may be NULL).modeA wfm_seed_advance_t value.
function dp_wfm_compose_spans¶
Replay the (epoch 0) instance timeline of a resolved segment list.
size_t dp_wfm_compose_spans (
const wfm_segment_t * segs,
size_t n_segs,
wfm_span_t * out,
size_t cap
)
Walks every segment's repeats instances, re-deriving each instance's drawn delay/on/off exactly as the streaming composer will (identical draw hash), and fills out with up to cap spans in stream order. Returns the TOTAL instance count regardless of cap — call once with cap 0 to size, then again with a buffer. Pass the RESOLVED segments (dp_wfm_compose_segments() on a live composer) so intrinsic on-times (dsss) are already folded in.
Assumes every segment builds: a segment that fails at render time (invalid burst geometry) degrades to its gaps only, so positions after it would shift relative to this replay.
Parameters:
segsResolved segment array.n_segsSegment count.outSpan buffer (may be NULL when cap is 0).capCapacity of out in spans.
Returns:
Total number of instances in one pass of the spec.
function dp_wfm_draws_json¶
The same rows dp_wfm_compose_draws() reports, as a JSON array.
One object per source per instance, in stream order, with the keys named after the wfm_draw_t fields. Exists so a binding can hand a caller its GROUND TRUTH without marshalling a struct array itself: a ranged field is only usable if what it drew can be read back, and scoring a receiver against a scene whose freq re-draws per instance means scoring against a number the caller does not otherwise have (doppler#1112).
Reads through dp_wfm_compose_draws(), so it cannot disagree with the SigMF annotations, which read through it too.
Parameters:
segsResolved segment array (dp_wfm_compose_segments()).n_segsSegment count.
Returns:
Heap JSON string the caller free()s; never NULL — the allocations go through the abort-on-OOM helpers. A spec with no rows yields [].
size_t n; int rp, ct;
const wfm_segment_t *segs = dp_wfm_compose_segments(c, &n, &rp, &ct);
char *js = dp_wfm_draws_json(segs, n);
puts(js);
free(js);
function dp_wfm_frame_copy¶
Deep-copy a description: the struct and each literal field's bits.
A source borrows its description; a holder that must outlive the caller's (the composer, a Python Synth) takes a copy instead, and releases it with dp_wfm_frame_free(). A later change to the original, or freeing it, does not reach the copy.
Parameters:
dthe description to copy; NULL gives NULL.
Returns:
the owned copy.
wfm_frame_desc_t *a
= dp_wfm_frame_from_json ("{\"fields\": [{\"spec\": \"1010\"}]}", NULL);
wfm_frame_desc_t *b = dp_wfm_frame_copy (a);
dp_wfm_frame_free (a); // b is unaffected
// b->field[0].seq.len == 4
dp_wfm_frame_free (b);
function dp_wfm_frame_free¶
Free a description returned by dp_wfm_frame_from_json() , bits and all. NULL is a no-op.
dp_wfm_frame_free (dp_wfm_frame_from_json ("{\"fields\": []}", NULL));
dp_wfm_frame_free (NULL); // nothing to free
function dp_wfm_frame_from_json¶
Read a frame description from its JSON form.
The form a scene's "frame" key holds, and what wfmgen --frame FILE reads — one reader for both: {"fields": [...], "stages": [...]}. A field with bits is its Field text, "spec" ("0x1ACFFC1D", "pn:31:5*4"); a derived field is its "bits" and the "derived_by" stage (index plus one). A stage names its "kind" ("crc16", "rs", "randomise", "conv", "interleave", or a number from WFM_STAGE_USER up) and its cover as "first_field"/"n_fields", plus "depth", "unit_bits" and "emit_num"/"emit_den" where the kind uses them.
A malformed description is REFUSED, never salvaged: a frame read wrong builds a waveform that looks fine and is not the one described. Whether it lays out is a separate question, asked by dp_wfm_source_frame_error() once a source carries it.
Parameters:
jsonthe frame object, NUL-terminated.whyreceives a static reason on failure; may be NULL.
Returns:
the description, owning its literal bits (free it with dp_wfm_frame_free()), or NULL if the text is not a frame object.
const char *why;
wfm_frame_desc_t *d = dp_wfm_frame_from_json (
"{\"fields\": [{\"name\": \"sync\", \"spec\": \"0x1ACFFC1D\"},"
" {\"name\": \"data\", \"spec\": \"pn:31:5*4\"}]}",
&why);
if (!d)
fprintf (stderr, "error: %s\n", why);
// d->n_fields == 2; d->field[1].reps == 4
dp_wfm_frame_free (d);
function dp_wfm_frame_refuse_text¶
Refuse text for a source's frame= : it takes a description.
The frame face of dp_wfm_source_bits_refuse_text(). A source's frame= takes a FrameDesc or a Frame on the Python face, and a str is refused rather than read as JSON; a description written as JSON is a scene's "frame" key, read by dp_wfm_frame_from_json(). It has the shape of that reader because the binding calls it where the reader would be called (just-makeit's owned-pointer parse_fn with parse_why).
Parameters:
textignored.whyreceives the static reason; may be NULL.
Returns:
NULL, always.
const char *why;
wfm_frame_desc_t *d = dp_wfm_frame_refuse_text ("{\"fields\": []}", &why);
// d == NULL; why names FrameDesc
function dp_wfm_frame_to_json¶
Write a description as its JSON frame object, the text dp_wfm_frame_from_json() reads back.
The same writer a scene's "frame" key uses, so the two cannot spell a description differently: a literal field is its Field text, a derived field its bits and derived_by, a stage its kind and cover.
Parameters:
dthe description; NULL gives NULL.
Returns:
a NUL-terminated string the caller releases with free().
wfm_frame_desc_t *d
= dp_wfm_frame_from_json ("{\"fields\": [{\"spec\": \"1010\"}]}", NULL);
char *text = dp_wfm_frame_to_json (d);
// text: {"fields":[{"spec":"0xa"}],"stages":[]}
free (text);
dp_wfm_frame_free (d);
function dp_wfm_framing_flags_error¶
Why --sync /--crc /--acq-code cannot frame this source, or NULL.
The CLI's flags spell the common frame, which needs a waveform that carries a bit stream and, off dsss, a data source to fill it. Said once here and used by the same refusal inside dp_wfm_source_frame_error, so the flags and a bare source give one answer. A dsss burst needs neither.
Parameters:
srcthe source.
Returns:
a static sentence naming the fix, or NULL.
function dp_wfm_render_destroy¶
Free a renderer and everything it owns. NULL-safe.
function dp_wfm_render_from_feed¶
A renderer whose input is a caller-supplied feed, not a live synth.
wfm_render_t * dp_wfm_render_from_feed (
const wfm_source_t * src,
double fs,
double doppler,
double doppler_rate,
dp_doppler_channel_state_t * borrow,
size_t delay,
size_t on,
wfm_feed_fn feed,
void * feed_ctx
)
This is what lets the Plan run a Doppler channel over its CACHED clean on-time through the one renderer the composer uses, so the dilation, the holdover and the input timeline are the same code rather than a second implementation of them. dp_wfm_render_steps() then yields the channel's output exactly as it does for a synth-backed renderer.
Parameters:
srcSource whosecarrier_hzthe channel uses.fsSample rate, Hz.dopplerDrawn Doppler, ppm.doppler_rateDrawn Doppler rate, ppm/s.borrowA PERSIST channel the caller owns, or NULL to make one that dies with the renderer.delayLeading noise-only INPUT samples (0 if the gaps are never pulled: see dp_wfm_render_set_input_timeline()).onON input samples.feedInput source.feed_ctxPassed tofeed.
Returns:
A renderer (dp_wfm_render_destroy() it), or NULL if the source has no Doppler to apply (doppler and doppler_rate both zero and no borrow) or the channel could not be built.
function dp_wfm_render_noise_steps¶
Pull n samples of the source's NOISE FLOOR only, through the same channel.
What a gap renders (gh-409). The channel runs here too, and deliberately: an emitter does not stop moving because its burst ended, so a pass is continuous and during a gap the thing propagating is the noise floor. Skip the channel over gaps and doppler_rate across a multi-burst scene quietly means "rate per unit of ON time" instead of per second.
function dp_wfm_render_set_input_timeline¶
Tell a Doppler renderer where its INPUT's phases fall.
A renderer with a channel pulls the synth in refills sized to the channel's input, not to what the caller asked for, so it cannot learn the phase boundaries from the pull calls: the channel's input and output clocks are decoupled. This states them, in INPUT samples delay noise-only, then on signal, then noise-only for ever and the renderer follows that timeline however the output is drained (doppler#1858).
Without it a channel renderer falls back to choosing per refill from the phase of the output being drained, which is wrong at every phase boundary. No effect on a renderer with no channel.
Parameters:
rRenderer from dp_wfm_compose_build_render().delayLeading noise-only samples.onSignal samples after the delay.
function dp_wfm_render_steps¶
Pull exactly n samples fromr , through its channel if any.
function dp_wfm_resolve_noise¶
Resolve a segment list's noise model in place (Phase 4b).
No-op for 1-source segments (keeps the bundled-synth path byte-identical). For a multi-source segment it sets one shared noise floor (from an explicit WFM_SYNTH_NOISE source, else the first snr-bearing source), cleans the signal sources, and appends a WFM_SYNTH_NOISE source at the floor — so the composer's accumulator just sums. May realloc each segment's sources. Idempotent.
dp_wfm_compose_create() calls this on its private copy, so every face (CLI, JSON, Python) resolves identically.
Returns:
0 on success; -1 if a non-anchor source over-specifies (snr + level) or on allocation failure.
function dp_wfm_scene_error¶
Why a scene cannot be composed, or NULL: the one validator.
const char * dp_wfm_scene_error (
const wfm_segment_t * segs,
size_t n_segs,
int repeat,
int continuous
)
Every source's dp_wfm_source_error(), then what only the scene can say:
- A continuous dsss source's chip rate
fs / sps, at its segment'sfs, is at least itssymbol_rateone chip per data symbol, the synth's own floor (dp_wfm_source_dsss_cps()). The defaultfs = 1.0with asymbol_ratein Hz is the case that finds it (doppler#1706); the reason is dp_wfm_why_dsss_cont_rate. - A count (
num_samples, non-zero or ranged) beside sources that set the segment's length (dp_wfm_segment_sets_length()) is refused with dp_wfm_why_count_derived: the length is theirs, so it would be dropped (doppler#1729). - A data STREAM (
--data-from-file -) has no end to repeat, sorepeat,continuousand a segment'srepeats > 1are refused, and stdin feeds at most one source.
dp_wfm_compose_create() refuses exactly these, and dp_wfm_compose_create_why() says which; a face calls this to say why.
Returns:
a static sentence naming the fault and its fix, or NULL.
function dp_wfm_segment_sets_length¶
Whether a segment's sources SET its on-time, so its num_samples is derived and a count given beside them refused.
Two kinds of source set a length (payload-data-source.md 4.6):
- A finite data source
data, a finite file, or a carried frame of fixed bits is its frames (dp_wfm_source_data_frames()); the longest of a segment's sets it. - A lone dsss burst one dsss source, no
symbol_rate, no data source is one burst (dp_wfm_source_dsss_nchips() timessps).
A stream sets none: it runs to its end, and a count may bound it. A non-zero num_samples (or a ranged one) beside a segment this answers 1 for is refused by dp_wfm_scene_error() with dp_wfm_why_count_derived, on every face.
static const uint8_t bits[16] = { 1 };
wfm_source_t src = { .type = WFM_SYNTH_BITS,
.modulation = 1, // bpsk
.sps = 1,
.pn_length = 7 };
src.data = (wfm_seq_t){ .kind = WFM_SEQ_LITERAL, .bits = bits,
.len = 16 };
wfm_segment_t seg = { .sources = &src, .n_sources = 1, .fs = 1e6 };
if (!dp_wfm_segment_sets_length (&seg)) // its frames are the run
return 1;
seg.num_samples = 1000; // so a count is refused
if (dp_wfm_scene_error (&seg, 1, 0, 0) != dp_wfm_why_count_derived)
return 1;
Parameters:
segthe segment, as the caller gave it.
Returns:
1 when its sources set its on-time, else 0.
function dp_wfm_snr_over_fs¶
SNR (dB) referred to fs, from a source's snr/snr_mode/sps/type.
double dp_wfm_snr_over_fs (
int snr_mode,
int type,
int sps,
size_t sf,
double sym_span,
double snr
)
The single source of truth for the Es/No, Eb/No, and over-fs conventions (snr_mode 0 auto / 1 fs / 2 ebno / 3 esno). dp_wfm_resolve_noise() uses it to place the shared noise floor at level(anchor) − dp_wfm_snr_over_fs(anchor), and the Plan stimulus engine reuses it to recompute the floor at an arbitrary swept SNR — so both agree to the bit.
For type=dsss the symbol is the outer data symbol. For a BURST that spans sf * sps samples (sf chips, sps samples per chip). For a CONTINUOUS async stream the data clock is independent of the code, so the span is fs / symbol_rate samples — passed as sym_span (non-integer), which OVERRIDES the sf·sps reconstruction when non-zero. auto picks esno, and esno/ebno convert as snr − 10·log10(span) (BPSK payload, so the two coincide). Every other type ignores sf and sym_span.
Parameters:
snr_mode0 auto, 1 fs, 2 ebno, 3 esno.typeA WFM_SYNTH_* waveform type (selects the auto convention).spsSamples per symbol/chip (≥1; <1 treated as 1).sfSpreading factor — chips per data symbol (burst dsss; ≥1, <1 treated as 1).sym_spanContinuous-dsss symbol span in samples (fs/symbol_rate); 0 = burst/non-dsss, derive fromsf·sps.snrThe declared SNR in dB.
Returns:
SNR over fs in dB.
function dp_wfm_source_attach_dsss¶
Attach a dsss source's data to a freshly-created synth.
The single dsss-attach path, called by BOTH synth-construction faces (dp_wfm_compose_build_synth and the standalone dp_wfm_source_to_synth), so the two cannot drift on how a dsss stream is configured. Selects on symbol_rate: 0 → the burst form (dp_wfm_synth_set_dsss_chips); > 0 → the continuous form (dp_wfm_synth_set_dsss_cont) with chips_per_symbol = (fs/sps)/symbol_rate, taking the data from the payload when one is supplied (bits) and otherwise from the seeded PN, and framing it with the per-frame code-only window (code_only_symbols of every frame_symbols, dp_wfm_synth_set_dsss_window) when one is set. A no-op for a non-dsss source.
Parameters:
synA synth from dp_wfm_synth_create() withwtype == WFM_SYNTH_DSSS.srcThe source (codes, payload, symbol_rate, pn config).fsSegment sample rate (Hz) — the continuous chip rate is fs/sps.
Returns:
0 on success (or non-dsss no-op); -1 on invalid geometry.
function dp_wfm_source_attach_frame¶
Attach an unspread source's bit pattern, framed or not.
The type=bits counterpart of dp_wfm_source_attach_dsss(), and called from the same two places for the same reason. When the source carries a frame, the pattern handed to dp_wfm_synth_set_bits() is dp_wfm_frame_assemble() of [preamble x reps | sync | payload | crc] rather than the payload alone — so the layout, the CRC's position and its bit order come from the one descriptor that the DSSS path and the receiver already read.
The frame CYCLES, exactly as an unframed pattern does: one descriptor fills whatever length is asked for, which is what turns a one-frame description into a multi-frame record.
Parameters:
synA synth from dp_wfm_synth_create() withwtype == WFM_SYNTH_BITS.srcThe source (pattern, modulation, and any frame fields).
Returns:
0 on success (or a non-bits/no-pattern no-op); -1 on failure.
function dp_wfm_source_bits_refuse_text¶
Refuse text for a source's bit field: an object takes bits.
size_t dp_wfm_source_bits_refuse_text (
const char * text,
uint8_t * out,
size_t max_out,
const char ** why
)
A composer source's bit fields (payload, sync, acq_code, data_code) take BITS on the Python face a uint8 array, bytes or a sequence of 0/1 and module helpers make them from other forms: field_bits(text) for the Field grammar, cvt.hex_to_bin, cvt.bytes_to_bin. A str is refused rather than read, so the object has one shape and the Field grammar one door. The text faces (the CLI, a scene) keep reading a Field through dp_wfm_field_parse().
It has the shape of dp_wfm_field_bits() because the binding calls it where that would be called (just-makeit's coerce_str_fn), and it always refuses: it returns 0 and sets why to its one static reason.
Parameters:
textignored.outnever written.max_outignored.whyreceives the static reason; may be NULL.
Returns:
0, always.
const char *why;
size_t n = dp_wfm_source_bits_refuse_text ("0101", NULL, 0, &why);
// n == 0; why names field_bits()
function dp_wfm_source_common_frame¶
The common frame as a description: [preamble x reps | sync | data | crc] , built by the ONE function that builds it.
int dp_wfm_source_common_frame (
const wfm_source_t * src,
const wfm_seq_t * sync,
int crc,
wfm_frame_desc_t * d
)
Used by the bridge (a scene's or Python's acq_code + data, with no sync word and no CRC) and by wfmgen for --sync and --crc, which are sugar for these fields. The preamble is a field for an unspread source and is NOT one for a spread burst: a DSSS preamble is sent unspread outside the description. The description borrows src's and sync's sequences.
Parameters:
srcthe source (itsacq_code,acq_reps,data,data_from_fileanddata_lenare read).syncthe sync word, or NULL for none.crcnon-zero appends a CRC-16 over the payload.dreceives the description.
Returns:
0, or -1 when it does not lay out.
function dp_wfm_source_crc_refuse_text¶
Refuse text for the retired crc= : a CRC is a stage of the frame description.
size_t dp_wfm_source_crc_refuse_text (
const char * text,
uint8_t * out,
size_t max_out,
const char ** why
)
As dp_wfm_source_sync_refuse_text, for crc=. crc="none" is refused too: it would otherwise be a spelling that works forever with no way to retire it.
Parameters:
textthe str (unused: every str is refused).outunused.max_outunused.whyreceives the sentence.
Returns:
function dp_wfm_source_create_snr¶
Resolve a source's (snr, snr_mode) into the pair to hand to dp_wfm_synth_create() .
dp_wfm_synth_create() runs before a dsss source's codes are attached, so it cannot know the spreading factor its own esno would need. This helper — the one create-time entry point shared by the composer (dp_wfm_compose_build_synth) and the standalone-Synth bridge (dp_wfm_source_to_synth), so every face agrees to the bit — converts a dsss source's SNR to the over-fs reference (via dp_wfm_snr_over_fs; the burst span is sf = n_data_code, a continuous stream uses fs/symbol_rate) and returns snr_mode=fs. A framed bpsk/qpsk/pn is referred the same way, because its synth is created as BITS (dp_wfm_source_synth_type()). Every other source passes through unchanged.
Parameters:
srcThe source (supplies type/sps/snr_mode/n_data_code/ symbol_rate).fsSegment sample rate (Hz) — needed for a continuous dsss source'sfs/symbol_ratespan; ignored otherwise.snrThe declared SNR in dB, already ranged-resolved.snr_modeReceives the snr_mode for create.
Returns:
The SNR in dB for create.
function dp_wfm_source_data_frames¶
Frames a FINITE data source makes, ceil(bits / LEN) ; 0 for a stream or none. On continuous dsss, which has no frame, its bits.
Known before the first sample (a file's length from fstat), which is what lets a finite run's length be derived rather than given (§4.6).
static const uint8_t bits[40] = { 1 };
wfm_source_t src = { .type = WFM_SYNTH_BPSK, .sps = 4 };
src.data = (wfm_seq_t){ .kind = WFM_SEQ_LITERAL, .bits = bits,
.len = 40 };
src.data_len = 16;
src.fill = (wfm_seq_t){ .kind = WFM_SEQ_DOTTED, .len = 2 };
if (dp_wfm_source_data_frames (&src) != 3) // 40 bits in 16-bit frames
return 1;
if (dp_wfm_source_data_samples (&src, 1e6, 3) != 3 * 16 * 4) // bpsk
return 1;
function dp_wfm_source_data_is_stream¶
Whether a source's data is a stream: data_from_file is- .
A stream has no length up front and no end to repeat, so a scene refuses it with repeat, continuous or repeats > 1, Plan refuses it, and a segment carrying one runs until it ends (dp_wfm_scene_error()).
function dp_wfm_source_data_samples¶
Samples the first frames frames of a source's data occupy; 0 with no data.
The one length a run with a data source is measured in: a finite run is this over dp_wfm_source_data_frames(), and a stream ends at this over the frames it sent.
- A frame (bits, bpsk/qpsk/pn): its output bits at the mapping's bits per symbol (rounded up), times
sps, per frame. - A dsss burst: its chips times
sps(samples per chip), per burst. - Continuous dsss has no frame: a "frame" is one data bit, one per data symbol, and the run is every chip up to the symbol after the
frames-thdata symbol, atfs / sps / symbol_ratechips per symbol not a whole number, so not a product timessps. Every symbol carries data unless a code-only window is set; then the data pauses through each frame's firstcode_only_symbols, and the run counts every window before the last data symbol (not one after it).
Parameters:
srcthe source.fsthe segment's sample rate (only continuous dsss reads it).framesframes (data bits, for continuous dsss) sent.
function dp_wfm_source_dsss_cps¶
Chips per data symbol of a CONTINUOUS dsss source at fs .
sps is samples per CHIP for dsss, so the chip rate is fs / sps, and the data clock is symbol_rate: their ratio, non-integer in general, which is the asynchronicity. It is the number the builder hands dp_wfm_synth_set_dsss_cont(), and the one dp_wfm_scene_error() holds to >= 1 so the rule and the synth read the same value.
Parameters:
srcthe source.fsits segment's sample rate, in Hz.
Returns:
chips per data symbol, or 0 for a source that is not continuous dsss (or has no sps).
wfm_source_t s = { .type = WFM_SYNTH_DSSS, .sps = 2,
.symbol_rate = 1000.0 };
if (dp_wfm_source_dsss_cps (&s, 1e6) != 500.0) // 500 chips per symbol
return 1;
function dp_wfm_source_dsss_nchips¶
Chips one DSSS BURST from this source occupies, description and all.
What sizes a lone dsss segment's intrinsic on-time. It reads the same description the burst is assembled from, so a stage that lengthens the frame a rate-1/2 inner code doubles it lengthens the segment by the same arithmetic instead of by a second copy of it.
Parameters:
srcthe source.
Returns:
burst chips, or 0 for a non-dsss source, a CONTINUOUS dsss source (which has no intrinsic length), or an empty/refused geometry.
function dp_wfm_source_error¶
NULL when this source can be built; else why not, as a sentence.
The question every face asks before it builds the wfmgen CLI, a scene read by dp_wfm_compose_from_json_why(), the standalone Synth through dp_wfm_source_to_synth() and the composer through dp_wfm_compose_create() so all four refuse the same sources for the same reason. It checks the source's own parameters, then asks dp_wfm_source_frame_error() about its frame:
pn_polymust fit thepn_length-bit register (pn_fits_register()). The generator masks a wider one, silently, sopn_poly = 0x40on a 5-bit register is a register with no feedback: the seed, then zeros, a constant waveform that still looks like a PN source (doppler#1636). 0 selects the maximal-length polynomial and always fits.- No two members the surface table declares exclusive may both be set (
WFM_SURFACE_EXCLUSIVE, wfm_surface.h): a carriedframeis the whole frame, so apayloadbeside it would be dropped (doppler#1683). The CLI and a scene refuse the same pair first, naming their own spelling. - A dsss source has the codes it needs (doppler#1696): a burst whose frame has bits to spread needs
data_code(dp_wfm_why_dsss_frame_no_data_code), a burst needs a preamble or a frame (dp_wfm_why_dsss_empty), and a continuous stream needsdata_code(dp_wfm_why_dsss_cont_no_data_code). A preamble alone is a valid burst: an acquisition stimulus.
A rule that needs the segment's sample rate is not here a source does not carry it but in dp_wfm_scene_error(), which asks this first.
Parameters:
srcThe source.
Returns:
NULL if there is nothing wrong, else a static message.
wfm_source_t s = { .type = WFM_SYNTH_PN, .sps = 1, .pn_length = 5,
.pn_poly = 0x40 };
dp_wfm_source_error (&s); // "pn_poly has a bit above ..."
s.pn_poly = 0x12;
dp_wfm_source_error (&s); // NULL: x^5 + x^2 + 1 fits
function dp_wfm_source_frame_error¶
NULL when this source's frame fields can be honoured; else why not.
ONE rule, asked by all three faces — the wfmgen CLI before it generates, the standalone Synth through dp_wfm_source_to_synth, and the composer through dp_wfm_compose_create — because the alternative is what shipped: the flags were accepted, stored and readable back on every face, and applied on none of them, so a caller who asked for a framed waveform silently got an unframed one.
A frame needs a payload. type=bits and the PN-sourced bpsk/qpsk/pn carry one when a payload is given (literal or generated, gh-762); the latter are then built as a BITS synth (dp_wfm_source_synth_type()). Types with no bit stream (tone, noise, chirp, symbols) are refused with a reason.
Parameters:
srcThe source.
Returns:
NULL if there is nothing wrong, else a static message.
function dp_wfm_source_has_frame¶
Non-zero when this source describes a FRAME.
A carried description, a preamble or a data source is what says "framed".
Parameters:
srcThe source; NULL reads as unframed.
function dp_wfm_source_sync_refuse_text¶
Refuse text for the retired sync= : a sync word is a field of the frame description.
size_t dp_wfm_source_sync_refuse_text (
const char * text,
uint8_t * out,
size_t max_out,
const char ** why
)
The coercion hook of the sync tombstone: ANY str reaching it is refused, and a non-empty array is refused by dp_wfm_source_error, so sync= fails whatever it is given and the sentence names frame=.
Parameters:
textthe str (unused: every str is refused).outunused.max_outunused.whyreceives the sentence.
Returns:
function dp_wfm_source_synth_type¶
The synth type to create this source with.
The source's own type, except for a FRAMED bpsk/qpsk/pn: that one transmits its frame, and the only synth that plays a bit pattern is a WFM_SYNTH_BITS one (dp_wfm_synth_set_bits() is a no-op on any other), so it is created as BITS and dp_wfm_source_attach_frame() hands it the frame with the mapping the type names (bpsk for bpsk/pn, Gray QPSK for qpsk). Created with its own type it played the LFSR stream and dropped the frame (doppler#1616).
Asked by both construction faces (dp_wfm_compose_build_synth and the standalone dp_wfm_source_to_synth) and by dp_wfm_source_create_snr(), which refers such a source's SNR to fs so its noise stays in the reference its type names.
wfm_source_t s = { .type = WFM_SYNTH_BPSK };
int t = dp_wfm_source_synth_type (&s); // unframed: WFM_SYNTH_BPSK
Parameters:
srcThe source.
Returns:
A WFM_SYNTH_* type.
function dp_wfm_source_to_synth_error¶
Why dp_wfm_source_to_synth() refused this source, or NULL.
The standalone Synth's reason channel (just-makeit's bridge_error_fn, which takes the bridge's own arguments): dp_wfm_scene_error() of a scene of one segment at fs, so a refused Synth raises the same sentence as every other face, including a rule that needs the rate. NULL leaves the binding's generic error, for a refusal that is not the source's.
Parameters:
srcThe source.fsThe sample rate the bridge was given.
Returns:
A static sentence, or NULL.
wfm_source_t s = { .type = WFM_SYNTH_PN, .sps = 1, .pn_length = 5,
.pn_poly = 0x40 };
dp_wfm_source_to_synth_error (&s, 1e6); // dp_wfm_why_pn_poly
function dp_wfm_spec_headroom¶
The top-level headroom (dB) from a spec JSON, or 0 if absent.
Lets --from-file reproduce a recorded --headroom; the value is a writer gain, so it lives outside the composer state.
function dp_wfm_spec_template_json¶
A ready-to-edit example spec in the canonical from-file schema.
Returns a representative multi-segment template — an inline tone, an RRC-shaped QPSK-from-bits burst with a trailing gap, and a two-source additive sum mix — serialised with dp_wfm_spec_to_json(), so it is valid by construction and round-trips through dp_wfm_compose_from_json() unchanged. It therefore doubles as a working starting point for wfmgen --from-file, not just documentation: dump it, edit the fields, feed it back.
Returns:
malloc'd JSON (caller frees), or NULL on allocation failure.
function dp_wfm_spec_to_json¶
Serialise a spec to a JSON string (for record).
char * dp_wfm_spec_to_json (
const wfm_segment_t * segs,
size_t n_segs,
int repeat,
int continuous,
int seed_advance,
double headroom
)
seed_advance (a wfm_seed_advance_t) and headroom (dB of output backoff applied at the writer, not the composer) are each emitted as a top-level field only when non-default, so an unrecorded run and any older spec stay byte-identical. Read headroom back with dp_wfm_spec_headroom(); the parser reads seed_advance straight onto the composer.
seed_advance is a parameter rather than something read from segs because it is a property of the whole stream, like repeat/continuous. Omitting it is what made a recorded run replay a DIFFERENT waveform (doppler#978): the key was parsed and never written, so the round-trip silently fell back to NONE and every loop after the first came out identical.
Returns:
malloc'd JSON (caller frees), or NULL on allocation failure.
function dp_wfm_synth_attach_data¶
Drive a type=bits synth from a frame whose payload is a data source.
int dp_wfm_synth_attach_data (
dp_wfm_synth_state_t * syn,
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
wfm_data_src_t * src,
wfm_data_pacing_t pacing,
int modulation
)
The pull that replaces the cycle (docs/design/payload-data-source.md §7). Each frame is d assembled over the next chunk of src (dp_wfm_frame_assemble_data): the first now, and each one after at the previous frame's last bit, so every stage over the payload covers its own frame's chunk. Under pacing, a source with nothing yet sends an idle frame (dp_wfm_data_frame, the one rule). When src ends, the synth goes silent and dp_wfm_synth_data_ended() says so.
Parameters:
syna synth created with type=bits.dthe frame; exactly one field isdata:LEN, and LEN is the source's.opsstage kernels, as dp_wfm_frame_assemble(); may be NULL.srcthe data source; the synth OWNS it from here, success or not, and frees it with the synth.pacingWFM_DATA_PACED for--realtime, else WFM_DATA_UNPACED.modulationas dp_wfm_synth_set_bits().
Returns:
0, or -1: not a bits synth, no data field in d, a frame that does not assemble, or a source that failed its first read.
wfm_frame_desc_t d;
const wfm_seq_t data = { .kind = WFM_SEQ_DATA, .len = 16 };
dp_wfm_frame_fixed (&d, NULL, 0, NULL, &data, 1); // [data:16 | crc]
wfm_data_src_t *src = dp_wfm_data_create ("0x0123456789AB", NULL, 16, NULL,
NULL);
dp_wfm_synth_state_t *s
= dp_wfm_synth_create (WFM_SYNTH_BITS, 1e6, 0.0, 100.0, 0, 1, 8, 7, 0,
0, 0.0);
dp_wfm_synth_attach_data (s, &d, NULL, src, WFM_DATA_UNPACED, 1); // bpsk
// three 32-bit frames, each over its own 16-bit chunk, then silence
dp_wfm_synth_destroy (s); // frees src too
function dp_wfm_synth_data_source¶
The data source a synth pulls from (dp_wfm_synth_attach_data), or NULL: its stats are the run's truth for scoring.
function dp_wfm_synth_set_data_pacing¶
Pace a synth's data source: WFM_DATA_PACED for --realtime .
A synth attached by dp_wfm_source_attach_frame() pulls UNPACED it waits for its data, as cat does. A paced caller (the composer under --realtime) sets this after the build, so a source with nothing yet sends an idle frame instead (dp_wfm_data_frame, the one rule). A synth with no data source ignores it.
dp_wfm_synth_state_t *s = dp_wfm_synth_create (
WFM_SYNTH_BITS, 1e6, 0.0, 100.0, 0, 1, 8, 7, 0, 0, 0.0);
dp_wfm_synth_set_data_pacing (s, WFM_DATA_PACED); // no source: no-op
dp_wfm_synth_destroy (s);
Public Static Functions Documentation¶
function dp_wfm_scene_fs¶
The ONE answer to "what is this stream's sample rate": the fs every segment shares, or 0.0 when they differ.
fs is per segment, and a scene whose segments differ is legal: no single rate is true of it. 0.0 is the library's "not stated" (a Writer opened at fs=0.0, a SigMF document without core:sample_rate), so an output asks this and either states the rate it returns or, given 0.0, says nothing or refuses, if its format cannot say nothing (a BLUE header has one xdelta). Every output asks here rather than reading segs[0].fs, which is a rate only when they agree (doppler#1733).
wfm_segment_t s[2] = { { .fs = 6e6 }, { .fs = 6e6 } };
int ok = dp_wfm_scene_fs (s, 2) == 6e6;
s[1].fs = 2e6;
ok = ok && dp_wfm_scene_fs (s, 2) == 0.0 && dp_wfm_scene_fs (s, 0) == 0.0;
return ok ? 0 : 1;
Parameters:
segsthe segments; may be NULL whenn_segsis 0.n_segstheir count.
Returns:
the shared fs, or 0.0 when the segments differ or there are none.
Macro Definition Documentation¶
define WFM_NUM_SAMPLES_PLAIN¶
A plain segment's on-time when its num_samples is 0 and no source sets one: what--count , a scene andSegment default to.
The documentation for this class was generated from the following file native/inc/doppler/wfm/wfm_compose.h