Skip to content

File wfm_compose.h

File List > doppler > wfm > wfm_compose.h

Go to the documentation of this file

#ifndef WFM_COMPOSE_H
#define WFM_COMPOSE_H

#include "doppler/clib_common.h"
#include "doppler/wfm_synth/wfm_synth_core.h"
#include "doppler/wfm/wfm_frame.h" /* wfm_frame_desc_t — a source's frame, described */
#include "doppler/wfm/wfm_data.h" /* a data:LEN payload's source */
#include "doppler/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 (dp_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;          /* Waveform type. tone: complex sinusoid. noise:
                          AWGN. pn: PN sequence (LFSR). bpsk/qpsk: PN-driven
                          modulation. chirp: linear-FM sweep. bits: a
                          caller's bit pattern, with selectable modulation.
                          symbols: a caller's complex constellation stream.
                          dsss: spread spectrum -- a two-code burst
                          (repeated preamble + data-code-spread frame) by
                          default, or a continuous asynchronous stream when
                          symbol_rate is set. */
    double freq;       /* Carrier or offset frequency in Hz; for chirp, the
                          sweep start. With fs = 1 it is in normalised
                          cycles per sample. */
    double snr;        /* Signal-to-noise ratio in dB, interpreted per
                          snr_mode. 100 or more is clean: no AWGN is added. */
    int snr_mode;      /* How snr is interpreted. auto: Es/N0 for
                          bpsk/qpsk/dsss, and relative to full scale for
                          tone/noise/pn/chirp/bits/symbols -- bits included,
                          because a bits frame has no symbol rate the engine
                          can infer. fs: dB relative to full scale. ebno:
                          Eb/N0, per bit. esno: Es/N0, per symbol; for a
                          dsss burst the outer data symbol of len(data_code)
                          chips x sps samples, for a continuous dsss stream
                          the fs/symbol_rate samples the async symbol spans. */
    uint32_t seed;     /* PRNG and LFSR seed for the noise and PN streams.
                          Deterministic: vary it for run-to-run change. For
                          a PN-sourced type it starts the pn_length-bit
                          register, masked: a seed whose low pn_length bits
                          are zero starts the register at 1, as seed 0
                          does. */
    int sps;           /* Samples per symbol (PSK) or per chip (PN): the
                          oversampling factor. Unused by noise, which
                          records it as 0. */
    int pn_length;     /* PN LFSR register length in bits, 2 to 64 for the
                          PN-bearing types; the sequence period is
                          2^pn_length - 1. Unused by noise, which records it
                          as 0. */
    uint64_t pn_poly;  /* PN generator polynomial, in the Galois bit-vector
                          convention; 0 selects a maximal-length (MLS)
                          polynomial for pn_length. It must fit the
                          pn_length-bit register: a bit at or above it is
                          refused, not masked. A polynomial above 2^53
                          does not survive a JSON number, so a scene file
                          needs 0 (auto) for such a register. */
    int lfsr;          /* PN LFSR realisation. Both give the same period;
                          fibonacci's chips are galois's in reverse order. */
    double level;      /* Source power in dBFS (<= 0; 0 is unit power).
                          Applies when summed in a Segment or Composer, as a
                          gain of 10^(level/20); a standalone Synth.steps()
                          ignores it. */
    int background;    /* Mark this source as part of the static background
                          field (0/1). Plan.prepare() folds a contiguous
                          leading run of background sources into ONE
                          pre-summed cache entry instead of caching each
                          separately, so a scene of many fixed emitters
                          costs one buffer rather than hundreds. The
                          composite is overridable as a unit: it takes a
                          single slot in gains/phases/enable and counts as
                          one in n_sources(), so scaling it trims the whole
                          field while its members keep their relative
                          levels. Background sources must come first in the
                          segment (a non-prefix ordering is rejected by
                          prepare, since the fold would no longer reproduce
                          compose bit-for-bit). Ignored by compose() and by
                          standalone Synth.steps(). */
    double f_end;      /* Chirp end frequency in Hz; ignored by other types. */
    size_t span;       /* Chirp sweep length in samples: the frequency ramps
                          from freq to f_end over this many samples, then
                          holds at f_end. 0 means the enclosing Segment's
                          num_samples. A standalone chirp (f_end != freq)
                          must declare it, so step(), steps(N) and any
                          chunking of reads produce the same waveform;
                          generating one without it raises. Ignored by
                          non-chirp types. */
    int modulation;    /* Symbol mapping of a bits pattern. none: the
                          pattern shaped and output as-is (NRZ). bpsk: +/-1
                          symbols. qpsk: Gray-coded symbols from pairs of
                          bits. */
    float _Complex *symbols; /* For type=symbols: a complex constellation
                                stream. Each element is the output point
                                itself, oversampled by sps, cycled, and
                                RRC-shaped with pulse=rrc, which generalises
                                any modulation (pi/4-QPSK, QAM, ...). */
    size_t n_symbols;        /* type=symbols: stream length */
    int pulse;         /* Pulse shape per symbol or chip, for
                          pn/bpsk/qpsk/bits/symbols/dsss. rect: rectangular,
                          no ISI filtering. rrc: root-raised cosine; see
                          rrc_beta and rrc_span. */
    double rrc_beta;   /* RRC roll-off factor, in (0, 1], when pulse=rrc. */
    int rrc_span;      /* RRC filter support in symbols when pulse=rrc,
                          ONE-SIDED: the filter has 2*rrc_span*sps + 1 taps,
                          unit energy (sum of h^2 = 1). */
    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;      /* Clock Doppler in ppm: the received time base is
                            rescaled by 1 + doppler*1e-6, so the symbol and
                            chip rates move with the carrier and a timing
                            loop sees the error a carrier-only `freq` offset
                            hides. Accepts a (lo, hi) tuple drawn uniformly
                            per repeat, like freq/snr. Zero doppler AND zero
                            doppler_rate means no channel is built at all,
                            so a source that does not ask for Doppler
                            renders exactly as it always did. */
    double doppler_rate; /* Linear ramp on `doppler`, in ppm per second of
                            elapsed stream time. The channel runs through a
                            segment's gaps as well as its on-time -- an
                            emitter does not stop moving because its burst
                            ended -- so this is per second, not per second
                            of on-time. Accepts a (lo, hi) tuple drawn
                            uniformly per repeat. */
    double carrier_hz;   /* RF carrier in Hz that the ppm figures are
                            referred to. It gives the coherent carrier
                            rotation that accompanies the time-base warp; 0
                            warps the clock alone, with no carrier rotation
                            -- a legitimate scene, not an unset field.
                            Independent of doppler/doppler_rate. */
    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;   /* How long this source's Doppler channel lives.
                               per_instance: the channel dies with each
                               `repeats` instance, so the geometry restarts
                               -- the repeated-trial shape, which composes
                               with a ranged doppler re-drawn per instance.
                               persist: one continuous pass carries across
                               the segment's gaps and repeat instances,
                               keyed by (segment, source) position -- the
                               only lifetime under which doppler_rate
                               accumulates across a multi-burst scene.
                               compose(), stream() and Plan.prepare() all
                               honour both. */
    /* A frame DESCRIPTION, the whole frame: fields in wire order, and
       stages that each name the span they cover (crc16, rs, randomise,
       interleave, conv, or a kind of your own). It is the only way a source
       says anything but the common frame: a coding stage, a field of the
       caller's own bits at a position of their choosing, a stage covering a
       span they name. `wfmgen --frame FILE`, a scene's
       `frame` key and Python's `frame=` (a FrameDesc or a Frame) all land
       here. When it is set it IS the frame, and an unspread acq_code
       beside it is refused. NULL means the common frame, `[preamble x
       reps | data]`, which `dp_wfm_frame_fixed()` builds from acq_code and
       the data source; a sync word or a CRC is a field or a stage of a
       description, never a flat field.

       A C caller's description is borrowed, exactly as `wfm_seq_t` is
       borrowed elsewhere here, so it must outlive the source. The composer
       and a Python source hold their own copy (`dp_wfm_frame_copy()`), so
       a later change to the FrameDesc does not reach them. On the Python
       face `frame=` is an input: read it back from the composer's JSON
       (its getter is jm's, pending removal: doppler#1694).

       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 `dp_wfm_frame_assemble` directly. */
    const wfm_frame_desc_t *frame;

    /* type=dsss: the two-code burst geometry (dp_wfm_dsss_desc_chips). The
       payload is a data source (`data` / `data_from_file`, below). */
    /* 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;  /* The preamble code, sent acq_reps times at the
                            head of the frame (a Field's *REPS on the
                            command line and in a scene) -- the coherent
                            pull-in target BurstDespreader.set_acq and
                            BurstDemod.set_preamble lock to. For type=dsss
                            it is unmodulated chips ahead of the spread
                            frame; for type=bits it is the head of the bit
                            pattern. Setting it is what makes a source
                            FRAMED. */
    size_t acq_reps;     /* Preamble repetitions: periods of acq_code before
                            the sync word. On the command line and in a
                            scene it is acq_code's *REPS. */
    wfm_seq_t data_code; /* For type=dsss: the payload spreading code, a
                            second code distinct from acq_code. Every frame
                            bit (sync, payload, crc) is XOR-spread across
                            its full length, so len(data_code) is the
                            spreading factor. */
    /* type=dsss, CONTINUOUS mode: a data-symbol rate independent of the code
       epoch rate selects the continuous form (dp_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/frame/data) are meaningless when this is
       set and are rejected by the caller rather than silently ignored. */
    double symbol_rate;  /* For type=dsss: > 0 selects CONTINUOUS
                            asynchronous mode. The spreading code repeats
                            endlessly and data rides on it at this symbol
                            rate (Hz), independent of the code-epoch rate
                            (chips/symbol = fs/sps/symbol_rate,
                            non-integer). No preamble/sync/CRC frame; data
                            comes from the payload when supplied, else a
                            seeded PN a receiver regenerates. Absent/0 =
                            burst. */
    int dsss_code_only;  /* Continuous dsss data source: 1 = code-only (the
                            pure spreading code, no data modulation); 0 =
                            data-modulated (the payload when supplied, else
                            the seeded PN). Ignored for burst dsss and
                            non-dsss types. */
    /* The data source a frame's payload is drawn from, `data_len` bits per
       frame (docs/design/payload-data-source.md; wfm/wfm_data.h is the one
       table of what a source is). It is EITHER `data` (a Field, or a bit
       array in Python) OR `data_from_file` (a file, or `-` for stdin):
       one declaration makes the pair exclusive on every face. */
    wfm_seq_t data;    /* A frame's payload drawn from a data source: a
                          Field on the command line and in a scene, a bit
                          array in Python. The source is split into
                          data_len-bit frames, one chunk per frame, and its
                          last chunk is padded from fill. For type=bits,
                          bpsk/qpsk/pn framed, a dsss burst (one burst per
                          frame), and continuous dsss (one bit per data
                          symbol, no frame); not with data_from_file. */
    size_t data_len;   /* Bits of the data source per frame: the data:LEN
                          of the common frame [preamble x reps | sync |
                          data:LEN | crc]. 0 takes a finite source whole, as
                          one frame. A carried frame names its own data
                          field, and this is then 0 or that field's LEN.
                          Continuous dsss has no frame, and refuses it. */
    wfm_seq_t fill;    /* The bits that pad a data source's last frame when
                          it does not divide into data_len-bit frames, tiled
                          from their first bit; stdin on a framed source
                          always needs them. Without them such a source is
                          refused before the first sample. A Field on the
                          command line and in a scene, a bit array in
                          Python. Continuous dsss has no frame to pad, and
                          refuses it. */
    const char *data_from_file; /* A data source read from a file of packed
                          octets, MSB first, or `-` for stdin, instead of
                          data. Not on the Python face: there a file is
                          cvt.bytes_to_bin of its bytes, passed as data
                          (payload-data-source.md section 4.9). Borrowed,
                          so it must outlive the source. */
    wfm_seq_t retired_bits; /* RETIRED (doppler#1718): nothing reads it but
                          the refusal. A payload is drawn from a data
                          source, so bits=, and its aliases payload= and
                          pattern=, are refused naming data=; the CLI and a
                          scene refuse --bits and "payload" the same way. */
    wfm_seq_t retired_sync; /* RETIRED (doppler#1617): nothing reads it but
                          the refusal. The frame-sync word is a field of the
                          frame DESCRIPTION, so sync= is refused naming
                          frame=; the CLI's --sync builds that description
                          for you, and a scene refuses the "sync" key. */
    wfm_seq_t retired_crc; /* RETIRED (doppler#1617): nothing reads it but
                          the refusal. A CRC is a stage of the frame
                          DESCRIPTION, so crc= is refused naming frame=,
                          whatever its value (crc="none" included); the
                          CLI's --crc builds that description for you, and
                          a scene refuses the "crc" key. */
    wfm_data_stats_t data_sent; /* What this source's data sent in the
                          composer's latest instance: its frames, the fill
                          bits padding the last, its idle frames, the source
                          bits read and, for data_from_file, the dp_hash64
                          of the octets read. The truth --record and SigMF
                          carry (payload-data-source.md section 4.8), kept
                          by the composer, which clears it at create; zero
                          until an instance has sent a frame. Not an input:
                          no face sets it. */
    size_t code_only_symbols; /* Continuous dsss: pure-code symbols opening
                          each frame of frame_symbols symbols. The data
                          pauses through them and runs on across frames,
                          so a searcher can integrate coherently over the
                          window; once a payload ends, no window is sent.
                          At most frame_symbols, and fewer when a data
                          source is set; 0 with frame_symbols 0 = no
                          window. */
    size_t frame_symbols; /* Continuous dsss: the frame, in symbols, that
                          code_only_symbols opens; give it with
                          code_only_symbols, or both 0 for no window. The
                          frame runs on the stream's own symbol clock, so
                          its edges land at any chip phase. */
} 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 in Hz, one per segment and shared
                              by all its sources. At the default 1.0 every
                              frequency is normalised (cycles per sample);
                              state it whenever a scene is in real Hz. */
    size_t num_samples;    /* Segment on-time in samples, before the
                              trailing gap: 0 derives it from the sources,
                              or 1024 when they set none. A finite data
                              source sets its frames, a lone dsss burst one
                              burst, and a stream runs to its end. A count beside a finite
                              data source or a lone dsss burst is refused,
                              since they set the length; give repeats for
                              more. */
    size_t off_samples;    /* Trailing gap after the on-time, in samples. It
                              carries the noise floor or hard zeros, per
                              gap_noise. */
    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 */
    /* Play the segment this many times back-to-back (each instance = delay
       + on-time + trailing gap) before advancing. Ranged fields re-draw and
       the AWGN is fresh per instance; the signal (codes, payload, PN phase)
       stays fixed. 0 and 1 both mean one instance. */
    size_t repeats;
    /* Leading gap before the on-time, in samples: the burst arrives after
       this delay, and the gap carries the noise floor like off_samples.
       Ranged like off_samples and re-drawn per repeats instance, so a (lo,
       hi) delay is per-burst arrival jitter. Use off_samples for
       inter-burst spacing, delay_samples for arrival jitter. */
    size_t delay_samples;
    size_t delay_samples_hi; /* upper bound when WFM_RANGE_DELAY_SAMPLES */
    /* Gap policy for this segment's delay and trailing gap. auto: gaps
       carry the segment's noise floor -- the sources' AWGN keeps running
       while the signal stops (clean scenes still get exact-zero gaps). 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 dp_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 dp_wfm_compose_draws(const wfm_segment_t *segs, size_t n_segs,
                         wfm_draw_t *out, size_t cap);

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

int dp_wfm_resolve_noise(wfm_segment_t *segs, size_t n);

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

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

int dp_wfm_source_attach_dsss(dp_wfm_synth_state_t *syn, const wfm_source_t *src,
                           double fs);

int dp_wfm_source_has_frame(const wfm_source_t *src);

size_t dp_wfm_source_dsss_nchips(const wfm_source_t *src);

double dp_wfm_source_dsss_cps(const wfm_source_t *src, double fs);

const char *dp_wfm_source_frame_error(const wfm_source_t *src);

const char *dp_wfm_source_error(const wfm_source_t *src);

extern const char dp_wfm_why_pn_poly[];

extern const char dp_wfm_why_dsss_frame_no_data_code[];
extern const char dp_wfm_why_dsss_empty[];
extern const char dp_wfm_why_dsss_cont_no_data_code[];

extern const char dp_wfm_why_retired_bits[];

extern const char dp_wfm_why_retired_sync[];
extern const char dp_wfm_why_retired_crc[];

const char *dp_wfm_source_to_synth_error(const wfm_source_t *src, double fs);

int dp_wfm_source_attach_frame(dp_wfm_synth_state_t *syn, const wfm_source_t *src);

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

void dp_wfm_synth_set_data_pacing(dp_wfm_synth_state_t *syn,
                                  wfm_data_pacing_t pacing);

const char *dp_wfm_scene_error(const wfm_segment_t *segs, size_t n_segs,
                               int repeat, int continuous);

extern const char dp_wfm_why_dsss_cont_rate[];

#define WFM_NUM_SAMPLES_PLAIN ((size_t)1024)

int dp_wfm_segment_sets_length(const wfm_segment_t *seg);

extern const char dp_wfm_why_count_derived[];


int dp_wfm_source_data_is_stream(const wfm_source_t *src);

uint64_t dp_wfm_source_data_samples(const wfm_source_t *src, double fs,
                                    uint64_t frames);

uint64_t dp_wfm_source_data_frames(const wfm_source_t *src);

const wfm_data_src_t *dp_wfm_synth_data_source(const dp_wfm_synth_state_t *syn);

int dp_wfm_source_synth_type(const wfm_source_t *src);

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

typedef struct wfm_render wfm_render_t;

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

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

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

typedef void (*wfm_feed_fn)(void *ctx, float _Complex *dst, size_t n,
                            int signal);

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

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

void dp_wfm_render_set_input_timeline(wfm_render_t *r, size_t delay,
                                      size_t on);

void dp_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 dp_wfm_compose_state_t;

dp_wfm_compose_state_t *dp_wfm_compose_create(
    const wfm_segment_t *segs, size_t n_segs, int repeat, int continuous);

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

void dp_wfm_compose_set_seed_advance(dp_wfm_compose_state_t *state, int mode);

void dp_wfm_compose_set_data_pacing(dp_wfm_compose_state_t *state,
                                    wfm_data_pacing_t pacing);

int dp_wfm_compose_seed_advance(const dp_wfm_compose_state_t *state);

size_t dp_wfm_compose_execute(
    dp_wfm_compose_state_t *state, float _Complex *out, size_t max);

size_t dp_wfm_compose_execute_rate(dp_wfm_compose_state_t *state,
                                   float _Complex *out, size_t max,
                                   double *fs);

static inline double dp_wfm_scene_fs(const wfm_segment_t *segs,
                                     size_t n_segs)
{
    if (!segs || n_segs == 0)
        return 0.0;
    for (size_t i = 1; i < n_segs; i++)
        if (segs[i].fs != segs[0].fs)
            return 0.0;
    return segs[0].fs;
}

void dp_wfm_compose_destroy(dp_wfm_compose_state_t *state);

const wfm_segment_t *dp_wfm_compose_segments(const dp_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 *dp_wfm_spec_to_json(const wfm_segment_t *segs, size_t n_segs, int repeat,
                       int continuous, int seed_advance, double headroom);

double dp_wfm_spec_headroom(const char *json);

char *dp_wfm_spec_template_json(void);

wfm_frame_desc_t *dp_wfm_frame_from_json(const char *json, const char **why);

void dp_wfm_frame_free(wfm_frame_desc_t *d);

size_t dp_wfm_source_sync_refuse_text(const char *text, uint8_t *out,
                                      size_t max_out, const char **why);

size_t dp_wfm_source_crc_refuse_text(const char *text, uint8_t *out,
                                     size_t max_out, const char **why);

const char *dp_wfm_framing_flags_error(const wfm_source_t *src);

int dp_wfm_source_common_frame(const wfm_source_t *src, const wfm_seq_t *sync,
                               int crc, wfm_frame_desc_t *d);

size_t dp_wfm_source_bits_refuse_text(const char *text, uint8_t *out,
                                      size_t max_out, const char **why);

wfm_frame_desc_t *dp_wfm_frame_refuse_text(const char *text, const char **why);

char *dp_wfm_frame_to_json(const wfm_frame_desc_t *d);

wfm_frame_desc_t *dp_wfm_frame_copy(const wfm_frame_desc_t *d);

dp_wfm_compose_state_t *dp_wfm_compose_from_json(const char *json);

dp_wfm_compose_state_t *dp_wfm_compose_from_json_why(const char *json,
                                               const char **why);

dp_wfm_compose_state_t *dp_wfm_compose_from_json_at(const char *json,
                                                    const char *base,
                                                    const char **why);

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_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);

#ifdef __cplusplus
}
#endif

#endif /* WFM_COMPOSE_H */