Skip to content

File wfm_compose.h

File List > inc > wfm > wfm_compose.h

Go to the documentation of this file

#ifndef WFM_COMPOSE_H
#define WFM_COMPOSE_H

#include "clib_common.h"
#include "wfm_synth/wfm_synth_core.h"
#include "wfm/wfm_frame.h" /* wfm_frame_desc_t — a source's frame, described */
#include "doppler_channel/doppler_channel_core.h" /* a source's clock Doppler */

#ifdef __cplusplus
extern "C" {
#endif

enum
{
  WFM_RANGE_FREQ          = 1u << 0, /* source.freq  → [freq, freq_hi]   */
  WFM_RANGE_SNR           = 1u << 1, /* source.snr   → [snr, snr_hi]     */
  WFM_RANGE_LEVEL         = 1u << 2, /* source.level → [level, level_hi] */
  WFM_RANGE_FEND          = 1u << 3, /* source.f_end → [f_end, f_end_hi] */
  WFM_RANGE_NUM_SAMPLES   = 1u << 4, /* segment.num_samples span         */
  WFM_RANGE_OFF_SAMPLES   = 1u << 5, /* segment.off_samples span         */
  WFM_RANGE_DELAY_SAMPLES = 1u << 6, /* segment.delay_samples span       */
  /* Source again, continuing after the segment bits rather than renumbering
     them: the bit index is the draw's stream selector (wfm_draw_range), so
     moving one would change every drawn value in every existing scene. */
  WFM_RANGE_DOPPLER      = 1u << 7, /* source.doppler → [lo, doppler_hi] */
  WFM_RANGE_DOPPLER_RATE = 1u << 8, /* source.doppler_rate → [lo, hi]    */
};

typedef enum
{
  WFM_DOPPLER_PER_INSTANCE = 0,
  WFM_DOPPLER_PERSIST      = 1,
} wfm_doppler_lifetime_t;

typedef enum
{
  WFM_SNR_AUTO = 0, /* the type's own convention (esno for modulated) */
  WFM_SNR_FS   = 1, /* against the noise in the WHOLE sampled band    */
  WFM_SNR_EBNO = 2, /* per information bit                           */
  WFM_SNR_ESNO = 3, /* per transmitted symbol                        */
} wfm_snr_mode_t;

typedef enum
{
  WFM_BITMOD_NONE = 0, /* the payload is not modulated */
  WFM_BITMOD_BPSK = 1,
  WFM_BITMOD_QPSK = 2,
} wfm_bitmod_t;

typedef struct {
    int type;          /* WFM_SYNTH_TONE … WFM_SYNTH_BITS */
    double freq;       /* freq offset (Hz); chirp: start frequency f_start */
    double snr;        /* dB, per snr_mode */
    int snr_mode;      /* a wfm_snr_mode_t */
    uint32_t seed;     /* PRNG / LFSR seed */
    int sps;           /* samples per symbol / chip */
    int pn_length;     /* LFSR register length */
    uint64_t pn_poly;  /* 0 → MLS poly for the length */
    int lfsr;          /* 0 galois, 1 fibonacci */
    double level;      /* source level in dBFS (≤0); 0 = unit power, no gain */
    int background;    /* 1 = static background: prepare() folds a contiguous
                          prefix of background sources into ONE pre-summed Plan
                          cache slot (scaled/rotated/dropped as a unit), instead
                          of caching each individually. Ignored by compose().  */
    double f_end;      /* chirp end frequency (Hz); ignored by other types */
    /* The payload, as a SEQUENCE like its three siblings below rather than
       a bare array. A literal keeps its bits at `payload.bits`/`payload.len`
       exactly as `bits`/`n_bits` did; a GENERATED payload (PN/Gold/Dotted)
       carries its parameters instead, which is what lets a 100k-bit frame be
       six numbers in a --record. The bridge used to flatten this to
       WFM_SEQ_LITERAL on the way to the descriptor -- the same copy that
       made the preamble's generated kinds unreachable (gh-762). */
    wfm_seq_t payload; /* type=bits: pattern; type=dsss: frame payload */
    int modulation;    /* type=bits: a wfm_bitmod_t */
    float _Complex *symbols; /* type=symbols: stream, owned; NULL otherwise */
    size_t n_symbols;        /* type=symbols: stream length */
    int pulse;         /* pn/bpsk/qpsk pulse shape: 0 rect, 1 rrc */
    double rrc_beta;   /* RRC roll-off (pulse=rrc) */
    int rrc_span;      /* RRC support in symbols (pulse=rrc) */
    unsigned ranged;   /* WFM_RANGE_{FREQ,SNR,LEVEL,FEND,DOPPLER*} bitmask */
    double freq_hi;    /* upper bound when WFM_RANGE_FREQ is set */
    double snr_hi;     /* upper bound when WFM_RANGE_SNR is set */
    double level_hi;   /* upper bound when WFM_RANGE_LEVEL is set */
    double f_end_hi;   /* upper bound when WFM_RANGE_FEND is set */
    /* CLOCK DOPPLER, per source rather than per segment: it is a property of
       one emitter's motion, and two transmitters in a `sum` segment are on
       different geometries. `freq` cannot express it -- an offset moves the
       carrier alone, while Doppler rescales the whole received time base, so
       the symbol and chip rates move with it and a timing loop sees the error
       a carrier-only offset hides.

       Zero `doppler` AND zero `doppler_rate` means no channel is built at
       all, so a scene that does not ask for Doppler renders through exactly
       the code it always did. */
    double doppler;      /* ppm; time-base scale is 1 + doppler*1e-6 */
    double doppler_rate; /* ppm/s; linear ramp on `doppler` */
    double carrier_hz;   /* RF carrier the ppm is referred to, for the
                            coherent carrier term (0 = no carrier rotation) */
    double doppler_hi;      /* upper bound when WFM_RANGE_DOPPLER is set */
    double doppler_rate_hi; /* upper bound when WFM_RANGE_DOPPLER_RATE */
    int doppler_lifetime;   /* a wfm_doppler_lifetime_t */
    /* A frame the CALLER built, and the answer to "what frame is this?"
       when it is set. The flat framing and coding fields below stay, as
       SUGAR that builds one of these -- so every existing scene, flag and
       JSON key keeps working -- but a description says things they cannot:
       a field of the caller's own bits at a position of their choosing, a
       stage covering a span they name, an arrangement no flag spells.

       Borrowed, never owned: the description points at the caller's
       sequences exactly as `wfm_seq_t` is borrowed elsewhere here, so it
       must outlive the source. NULL means "derive one from the fields
       below", which is what every source did before this existed.

       KERNELS stay in C by design. A description names a stage's KIND; the
       code that runs it is a `wfm_frame_ops_t` entry, and a caller adding a
       genuinely new transform (convolutional interleaving, say) writes that
       kernel in C and hands it to `wfm_frame_assemble` directly. */
    const wfm_frame_desc_t *frame;

    /* type=dsss: the two-code burst geometry (wfm_frame_dsss_chips). The
       payload bits ride the shared `bits` field above (alias "payload"). */
    /* The three sequences a framed source carries. `wfm_seq_t` already names
       "a run of bits, however produced" -- LITERAL plus the generated PN /
       GOLD / DOTTED kinds and their parameters -- so carrying it here is
       what lets a face spell a generated one (gh-762). The literal case is
       `kind = WFM_SEQ_LITERAL`, which is what every one of these was before,
       so today's callers describe exactly what they described.

       OWNERSHIP: a source OWNS its `.bits`. `wfm_seq_t` declares them
       `const uint8_t *` because a frame DESCRIPTOR borrows them, and the
       borrowing consumer is the common one; the owner casts to free. */
    wfm_seq_t acq_code;  /* preamble code (0/1); len 0 = no preamble */
    size_t acq_reps;     /* preamble repetitions */
    wfm_seq_t data_code; /* payload spreading code; len = spreading factor */
    wfm_seq_t sync;      /* frame-sync word bits; len 0 = none */
    int crc;             /* frame trailer: 0 none, 1 crc16 (dp_crc16.h) */
    /* type=dsss, CONTINUOUS mode: a data-symbol rate independent of the code
       epoch rate selects the continuous form (wfm_synth_set_dsss_cont) over
       the burst form above -- one waveform type, one discriminator, rather
       than a tenth entry in five hand-maintained name tables. 0 = burst.
       The frame fields (acq_code/sync/crc/bits) are meaningless when this is
       set and are rejected by the caller rather than silently ignored. */
    double symbol_rate;  /* Hz; > 0 selects continuous async DSSS */
    int dsss_code_only;  /* continuous dsss: 1 = code-only (--data none), no
                            data modulation; 0 = data-modulated (payload if
                            supplied, else the seeded PN). Ignored for burst. */
    /* Channel coding over the frame, as STAGES with the spans they cover
       (wfm/wfm_frame.h). Each is optional and they do not all cover the same
       bits, which is the whole reason the frame is a description rather than
       a pipeline: the outer code and the randomiser reach over the payload
       group, and the inner code reaches over everything including a marker
       neither of the other two touches.

       Set all four with a Transfer Frame payload and no preamble or sync word
       and the result is a CCSDS CADU. That is the point -- CCSDS is the
       configuration these flags reach, not a mode they switch into. */
    unsigned rs_depth;   /* outer code interleaving depth; 0 = no outer code.
                            4.3.5.1 allows 1,2,3,4,5,8 and the payload must be
                            exactly 223*depth octets -- virtual fill is not
                            implemented (gh-813), so any other length is
                            refused rather than padded. */
    int randomise;       /* XOR a section-10 pseudo-random sequence over the
                            payload group -- not over a marker, which has to
                            look the same in every frame to be found.
                            0 = off, 1 = 131.0-B-6 10.4.1's 131071-bit
                            sequence (the `shall`), 2 = 10.4.2's 255-bit one,
                            which B-6 keeps only for legacy systems. It is a
                            CHOICE rather than a flag because B-6 makes it
                            one, and because the two produce waveforms only
                            the matching receiver derandomises. */
    int attach_asm;      /* prepend the 0x1ACFFC1D marker as a FIELD */
    int convolutional;   /* inner code over the whole frame, marker included;
                            doubles the bit count (rate 1/2, K=7) */
    unsigned interleave_depth;      /* block interleaver over the data group;
                            0 = none. LAST of the data-group stages, so it is
                            what the channel sees: an interleaver exists to
                            make a burst arrive spread across the outer
                            code's codewords, so anything between it and the
                            wire would undo the point. */
    unsigned interleave_unit_bits;  /* bits per permuted unit; 0 reads as 1.
                            Match it to the outer code's symbol -- 8 for RS
                            over GF(256). Permuting BITS inside a symbol that
                            is already wrong buys nothing. */
} wfm_source_t;

typedef struct {
    wfm_source_t *sources; /* n_sources sources summed at the same time */
    size_t n_sources;
    double fs;             /* sample rate (Hz) — one per segment */
    size_t num_samples;    /* on-time (samples) */
    size_t off_samples;    /* off-time gap after the segment (samples) */
    unsigned ranged;       /* WFM_RANGE_{NUM,OFF}_SAMPLES bitmask */
    size_t num_samples_hi; /* upper bound when WFM_RANGE_NUM_SAMPLES is set */
    size_t off_samples_hi; /* upper bound when WFM_RANGE_OFF_SAMPLES is set */
    /* Bounded instancing: play this segment `repeats` times back-to-back
       (each instance = delay + on-time + trailing gap) before advancing.
       Every ranged field re-draws per instance and the AWGN is always fresh
       per instance, while the signal (codes/payload/PN phase) stays fixed —
       so `repeats=5` with a ranged off_samples is a 5-burst train with
       jittered gaps from one declaration. 0 and 1 both mean one instance;
       instance 0 renders byte-identically to a repeats-less segment. */
    size_t repeats;
    /* Leading gap before the on-time (samples) — "the burst arrives after a
       delay". Ranged like off_samples (WFM_RANGE_DELAY_SAMPLES), re-drawn
       per repeats instance, so a ranged delay is per-burst arrival jitter.
       Inter-burst spacing composes as off(k) + delay(k+1). */
    size_t delay_samples;
    size_t delay_samples_hi; /* upper bound when WFM_RANGE_DELAY_SAMPLES */
    /* Gap-noise policy for this segment's delay + trailing gap. 0 (auto,
       the default): the gaps carry the segment's noise floor — every
       source's additive-AWGN term keeps running (same stream, same power)
       while the signal stops, so a noisy scene's inter-burst region is the
       channel, not digital silence. Clean sources have no AWGN, so a clean
       scene's gaps remain exact zeros. 1 (off): gaps are hard zeros. */
    int gap_noise;
} wfm_segment_t;

typedef struct {
    size_t seg;      /* segment index in the spec */
    size_t instance; /* repeats instance, 0-based */
    size_t start;    /* absolute sample index where the instance begins */
    size_t delay;    /* leading gap length (samples) */
    size_t on;       /* on-time length (samples) */
    size_t off;      /* trailing gap length (samples) */
} wfm_span_t;

size_t wfm_compose_spans(const wfm_segment_t *segs, size_t n_segs,
                         wfm_span_t *out, size_t cap);

typedef struct {
    size_t seg;      /* segment index in the spec                        */
    size_t instance; /* repeats instance, 0-based                        */
    size_t src;      /* source index within the segment                  */
    size_t start;    /* absolute sample index where the instance begins  */
    size_t delay;    /* leading gap length (samples)                     */
    size_t on;       /* on-time length (samples)                         */
    size_t off;      /* trailing gap length (samples)                    */
    double freq;     /* DRAWN carrier / sweep-start offset, Hz           */
    double f_end;    /* DRAWN sweep-end offset, Hz (chirp)               */
    double snr;      /* DRAWN SNR, dB, in the source's own snr_mode      */
    double level;    /* DRAWN level, dB                                  */
    /* Clock Doppler is DRAWN like the four above, so it is reported like
       them. A ranged `doppler` recorded in the spec is a span; what this
       instance actually flew is only ever knowable here. */
    double doppler;      /* DRAWN clock Doppler, ppm                     */
    double doppler_rate; /* DRAWN Doppler rate, ppm/s                    */
} wfm_draw_t;

size_t wfm_compose_draws(const wfm_segment_t *segs, size_t n_segs,
                         wfm_draw_t *out, size_t cap);

char *wfm_draws_json(const wfm_segment_t *segs, size_t n_segs);

int wfm_resolve_noise(wfm_segment_t *segs, size_t n);

double wfm_snr_over_fs(int snr_mode, int type, int sps, size_t sf,
                       double sym_span, double snr);

double wfm_source_create_snr(const wfm_source_t *src, double fs, double snr,
                             int *snr_mode);

int wfm_source_attach_dsss(wfm_synth_state_t *syn, const wfm_source_t *src,
                           double fs);

int wfm_source_has_frame(const wfm_source_t *src);

int wfm_source_describe_frame(const wfm_source_t *src, wfm_frame_desc_t *d);

size_t wfm_source_dsss_nchips(const wfm_source_t *src);

const char *wfm_source_frame_error(const wfm_source_t *src);

int wfm_source_attach_frame(wfm_synth_state_t *syn, const wfm_source_t *src);

wfm_synth_state_t *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);

typedef struct wfm_render wfm_render_t;

wfm_render_t *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,
                                       doppler_channel_state_t *borrow);

void wfm_render_steps(wfm_render_t *r, float _Complex *dst, size_t n);

void wfm_render_noise_steps(wfm_render_t *r, float _Complex *dst, size_t n);

void wfm_render_destroy(wfm_render_t *r);

typedef enum
{
  WFM_SEED_ADVANCE_NONE  = 0, /* byte-identical repeats (default) */
  WFM_SEED_ADVANCE_NOISE = 1, /* signal fixed, AWGN fresh per repeat */
  WFM_SEED_ADVANCE_ALL   = 2, /* whole seed advances (code+data+noise) */
} wfm_seed_advance_t;

typedef struct wfm_compose_state wfm_compose_state_t;

wfm_compose_state_t *wfm_compose_create(
    const wfm_segment_t *segs, size_t n_segs, int repeat, int continuous);

void wfm_compose_set_seed_advance(wfm_compose_state_t *state, int mode);

int wfm_compose_seed_advance(const wfm_compose_state_t *state);

size_t wfm_compose_execute(
    wfm_compose_state_t *state, float _Complex *out, size_t max);

void wfm_compose_destroy(wfm_compose_state_t *state);

const wfm_segment_t *wfm_compose_segments(const wfm_compose_state_t *state,
                                          size_t *n_out, int *repeat,
                                          int *continuous);

/* ── JSON spec (the shared --from-file / --record format) ─────────────────── */
/*
 * Canonical schema: docs/schema/wfmgen.schema.json (JSON Schema 2020-12).
 * A recorded run reproduces byte-for-byte when fed back via --from-file.
 * Use `wfmgen json-template` for a ready-to-edit example covering all fields.
 */

char *wfm_spec_to_json(const wfm_segment_t *segs, size_t n_segs, int repeat,
                       int continuous, int seed_advance, double headroom);

double wfm_spec_headroom(const char *json);

char *wfm_spec_template_json(void);

wfm_compose_state_t *wfm_compose_from_json(const char *json);

wfm_compose_state_t *wfm_compose_from_json_why(const char *json,
                                               const char **why);

wfm_compose_state_t *wfm_compose_from_file(const char *path);

#ifdef __cplusplus
}
#endif

#endif /* WFM_COMPOSE_H */