File wfm_writer_core.h¶
FileList > inc > wfm_writer > wfm_writer_core.h
Go to the source code of this file
Output file types for generated IQ: raw / csv / BLUE-1000 + SigMF meta. More...
#include <stdbool.h>#include <stdio.h>#include "clib_common.h"#include "wfm/wfm_compose.h"
Public Types¶
| Type | Name |
|---|---|
| enum | wfm_filetype_t |
| typedef struct wfm_writer_state | wfm_writer_state_t |
Public Functions¶
| Type | Name |
|---|---|
| int | wfm_blue_write_hcb (FILE * fp, int sample_type, int endian, double fs, double fc, double data_start, size_t total_samples, int detached, double t0_unix_sec) Write a complete 512-byte BLUE/Platinum type-1000 Header Control Block. |
| char * | wfm_sigmf_meta_json (int sample_type, int endian, double fs, double fc, double t0_unix_sec, const wfm_segment_t * segs, size_t n_segs) Build a SigMF .sigmf-meta JSON document for a generated capture. |
| char * | wfm_sigmf_meta_json_ex (int sample_type, int endian, double fs, double fc, double t0_unix_sec, const wfm_segment_t * segs, size_t n_segs, const char * extra_global_json, const char *const * annotations, size_t n_ann) wfm_sigmf_meta_json() plus the two things a caller can add to it. |
| int | wfm_writer_add_keyword (wfm_writer_state_t * w, const char * tag, char type, const void * value, size_t count) Attach a BLUE extended-header keyword (a tag/value pair). |
| double | wfm_writer_clip_fraction (const wfm_writer_state_t * w) |
| int | wfm_writer_close (wfm_writer_state_t * w) Flush, patch the BLUE data_size from the actual count (if seekable), write any attached extended-header keywords, and free the writer (does not close the FILE*). |
| wfm_writer_state_t * | wfm_writer_create (const char * path, double fs, int file_type, int sample_type, int endian, double fc, size_t total, double headroom, double t0, bool sidecar) Open a capture for writing. |
| int | wfm_writer_destroy (wfm_writer_state_t * state) Finalise and free — the object binding's fallible destructor. |
| int | wfm_writer_flush (wfm_writer_state_t * state) Make written samples durable and observable, without finishing. |
| double | wfm_writer_get_clip_fraction (const wfm_writer_state_t * state) |
| bool | wfm_writer_get_clipped (const wfm_writer_state_t * state) |
| double | wfm_writer_get_peak_dbfs (const wfm_writer_state_t * state) |
| wfm_writer_state_t * | wfm_writer_open (FILE * fp, wfm_filetype_t ft, int sample_type, int endian, double fs, double fc, size_t total_samples, double t0_unix_sec) Open a writer on an already-open stream. |
| double | wfm_writer_peak (const wfm_writer_state_t * w) |
| void | wfm_writer_set_gain (wfm_writer_state_t * w, double gain) |
| void | wfm_writer_track_clipping (wfm_writer_state_t * state, int on) |
| size_t | wfm_writer_write (wfm_writer_state_t * state, const float _Complex * x, size_t x_len) Convert and write a block of samples. |
| int | write_blue_header (const char * path, double fs, int sample_type, int endian, double fc, double data_start, size_t total, int detached, double t0) |
Detailed Description¶
A streaming writer over a FILE* that serialises cf32 blocks into one of three on-disk file types, in the chosen wire sample type and byte order. The fourth file-type, SigMF, writes its samples as raw (into <base>.sigmf-data) and pairs with a sidecar <base>.sigmf-meta JSON from wfm_sigmf_meta_json().
A writer opened by PATH (wfm_writer_create) emits that sidecar itself, at close, so sigmf produces a readable pair with no further work — and for that reason it REQUIRES a path ending in .sigmf-data, since both halves of a SigMF capture are found by name. A writer opened on a FILE* (wfm_writer_open) has no name to derive the sidecar's from, so the caller owns it — that is the path wfmgen and Composer take, and it is also how they attach their per-segment annotations.
The same mechanism keeps raw and csv interpretable. Both containers take fs, fc and t0 at construction and have nowhere to store them, and until now simply discarded them — handing back a file that not even its author could interpret afterwards. A path-opened raw/CSV writer therefore gets a <path>.sigmf-meta sidecar too (sidecar=false opts out). It is SigMF-SHAPED, not a SigMF capture: the name is appended rather than swapped so it cannot collide with a real pair's metadata (see wfm_meta_path), and for CSV core:datatype names the value domain the samples were quantised to rather than a byte layout. BLUE gets none — its header already carries all three, and a second copy is only somewhere for them to drift.
Axes (orthogonal to the file type): * sample_type (wavegen order): 0 cf32, 1 cf64, 2 ci32, 3 ci16, 4 ci8. Integer types quantise full-scale ±1.0 (ci32 2^31-1, ci16 32767, ci8 127). * endian: 0 little, 1 big (csv is text, so endian is ignored there).
// ..., fs, fc, total, t0 — 0.0 for t0 means "no capture time known",
// which leaves the BLUE timecode field unset rather than dating the
// capture to 1970.
wfm_writer_state_t *w =
wfm_writer_open(fp, WFM_FT_BLUE, 3, 0, 1e6, 2.4e9, 4096, 0.0);
wfm_writer_write(w, iq, 4096);
wfm_writer_close(w); // patches the BLUE data_size from the actual count
Public Types Documentation¶
enum wfm_filetype_t¶
Output file type.
typedef wfm_writer_state_t¶
Opaque writer.
Public Functions Documentation¶
function wfm_blue_write_hcb¶
Write a complete 512-byte BLUE/Platinum type-1000 Header Control Block.
int wfm_blue_write_hcb (
FILE * fp,
int sample_type,
int endian,
double fs,
double fc,
double data_start,
size_t total_samples,
int detached,
double t0_unix_sec
)
Used for the blue file type — both attached (the writer calls this with data_start = 512, detached = 0, then streams the data after it) and detached (the caller writes the data to a separate .det file and this HCB to a .hdr file with data_start = 0, detached = 1). Every standard field is written; the header byte order follows endian.
Parameters:
fpdestination (binary).sample_typewire type (wavegen order) → BLUE format char C{B,I,L,F,D}.endian0 little (EEEI) / 1 big (IEEE).fssample rate (Hz) →xdelta = 1/fs. A zero writesxdelta = 0, the header's own way of saying the rate is not known.fccentre frequency (Hz). Type 1000 has no HCB field for it, so a non-zero value is written as an ASCIIFREQ=<value>pair in the HCB keyword area.data_startdata_startfield: 512 attached, 0 detached.total_samplescomplex-sample count →data_size.detachednon-zero sets the HCBdetachedflag.t0_unix_seccapture start in UNIX seconds → thetimecodefield at byte 56, converted to the J1950 epoch BLUE counts from. WFM_TIMECODE_UNSET (0.0) leaves the field zero, which is what a reader tests for; it does not write 1970 (nor 1950).
Returns:
0 on success, non-zero on a write error.
function wfm_sigmf_meta_json¶
Build a SigMF .sigmf-meta JSON document for a generated capture.
char * wfm_sigmf_meta_json (
int sample_type,
int endian,
double fs,
double fc,
double t0_unix_sec,
const wfm_segment_t * segs,
size_t n_segs
)
global carries core:datatype (from sample_type+endian, e.g. "ci16_le"), core:version "1.0.0", a wfmgen description/author, and core:sample_rate if fs is non-zero. captures is a single capture at sample 0 / frequency fc, carrying core:datetime if t0_unix_sec is set. annotations has one entry per composer segment — sample span, frequency edges (fc + freq ± bandwidth/2, bandwidth ≈ fs/sps for symbol/chip types), a core:label of the waveform type, and custom wfmgen:* parameters.
Both optional keys are OMITTED rather than defaulted when their input is unset. SigMF 1.0.0 requires only core:datatype and core:version in global, so an absent sample rate is legal — and it is the honest answer, where a fabricated one is a number a downstream tool will act on.
Parameters:
sample_typewire type (wavegen order) — withendianthis becomes core:datatype, e.g."ci16_le".endian0 little, 1 big.fssample rate (Hz). 0.0 means "not stated by the caller", and is then DERIVED fromsegswhen they carry one and agree on it — the annotations below are already computed from each segment's ownfs, so omittingcore:sample_ratewould withhold a rate this document demonstrably knows. Segments that disagree leave it unstated (no single rate is true of the stream), as does a NULLsegs. A non-zerofsalways wins: a caller rendering at a resampled rate is describing the FILE.fccentre frequency (Hz) →captures[0]["core:frequency"].t0_unix_seccapture start in UNIX seconds, or WFM_TIMECODE_UNSET. Rendered as extended ISO 8601 (core:datetimerequires the separators; doppler's filename stamps do not).segscomposer segments to annotate, or NULL for none.n_segsnumber of entries insegs.
Returns:
malloc'd JSON string (caller frees), or NULL on allocation failure.
function wfm_sigmf_meta_json_ex¶
wfm_sigmf_meta_json() plus the two things a caller can add to it.
char * wfm_sigmf_meta_json_ex (
int sample_type,
int endian,
double fs,
double fc,
double t0_unix_sec,
const wfm_segment_t * segs,
size_t n_segs,
const char * extra_global_json,
const char *const * annotations,
size_t n_ann
)
The same document, from the same code — this IS the implementation and wfm_sigmf_meta_json() is the call with both extras absent. It exists so an events sidecar (dp_event_log/dp_event_log_core.h) is this emitter with annotations of its own, rather than a second builder free to spell global and captures, and their omit-when-unknown rules, differently.
Parameters:
sample_typewire type (wavegen order) ->core:datatype.endian0 little, 1 big.fssample rate (Hz); 0.0 derives fromsegsor is omitted see wfm_sigmf_meta_json().fccentre frequency (Hz) ->captures[0].t0_unix_seccapture start, or WFM_TIMECODE_UNSET.segscomposer segments to annotate, or NULL for none.n_segsnumber of entries insegs.extra_global_jsona JSON OBJECT whose members are merged intoglobal, or NULL. A member replaces a key already there rather than duplicating it. Text that is not a JSON object is ignored.annotationsJSON object strings appended toannotationsafter the segments', or NULL. A string that does not parse as an object is skipped, not fatal.n_annnumber of entries inannotations.
Returns:
malloc'd JSON string (caller frees), or NULL on allocation failure.
function wfm_writer_add_keyword¶
Attach a BLUE extended-header keyword (a tag/value pair).
int wfm_writer_add_keyword (
wfm_writer_state_t * w,
const char * tag,
char type,
const void * value,
size_t count
)
Keywords are buffered and written as one block by wfm_writer_close(), after the data — the layout BLUE §3.3 recommends for streaming, since the total data size is not known until the stream ends. ext_start/ext_size are patched into the HCB at the same time. Call as many times as you like, before or between writes; order is preserved, and duplicate tags are allowed (the format permits them).
Parameters:
wan open BLUE writer (any other file type returns an error — only BLUE has an extended header).tagNUL-terminated tag, 1..255 characters. Upper-case is strongly preferred: lower-case has limited support across the Midas baselines.typeelement type code —B/I/L/X(8/16/32/64-bit integer),F/D(32/64-bit float), orA(ASCII string, variable length in keyword context).O/P/Nare not permitted in keywords and are rejected.valuecountelements in host byte order; forA,countcharacters (no NUL is written or required).countelement count; must be non-zero.
Returns:
0 on success, non-zero if the file type is not BLUE, the arguments are invalid, or the buffer could not grow.
double fc = 1.2345e9;
wfm_writer_add_keyword(w, "F_C", 'D', &fc, 1);
wfm_writer_add_keyword(w, "COMMENT", 'A', "10 dB pad", 9);
wfm_writer_close(w); // keywords land after the data, HCB patched
function wfm_writer_clip_fraction¶
Fraction (0..1) of I/Q components that saturated (|v| > 1). Always 0 unless wfm_writer_track_clipping() was enabled.
function wfm_writer_close¶
Flush, patch the BLUE data_size from the actual count (if seekable), write any attached extended-header keywords, and free the writer (does not close the FILE*).
Returns:
0 on success, non-zero on a write/seek error.
function wfm_writer_create¶
Open a capture for writing.
wfm_writer_state_t * wfm_writer_create (
const char * path,
double fs,
int file_type,
int sample_type,
int endian,
double fc,
size_t total,
double headroom,
double t0,
bool sidecar
)
Streams complex64 blocks to disk in the chosen file type, wire sample type and byte order, quantising to full scale (±1.0) for the integer types. Finish with close(): a BLUE capture's data_size and extended header are only written there, so a capture that is never closed is incomplete.
Parameters:
pathwhere to write astror anyos.PathLikefrom Python. Forfile_type="sigmf"this MUST end in.sigmf-data: a SigMF capture is a<base>.sigmf-data+<base>.sigmf-metapair found by name, and close() writes the sidecar beside it.file_type"raw"(headerless interleaved I/Q),"csv"(oneI,Qline per sample),"blue"(self-describing X-Midas/REDHAWK type-1000) or"sigmf". BLUE and SigMF recordfs/fc/t0in the capture itself; raw and CSV have nowhere to put them and keep them in thesidecarinstead.sample_typewire type:"cf32","cf64","ci32","ci16"or"ci8". The integer types quantise ±1.0 to full scale and can clip see track_clipping()/peak_dbfs.endian"le"or"be"; ignored for CSV, which is text.fssample rate (Hz), and REQUIRED there is no default. BLUE stores it asxdelta = 1/fs, SigMF and the raw/CSVsidecarascore:sample_rate. Pass 0.0 to say the rate is not known: that writesxdelta = 0and omitscore:sample_rate, where a defaulted value would have written a rate nobody supplied into a file that outlives the process.fccentre frequency (Hz). BLUE records it as aFREQkeyword, SigMF ascaptures[0]["core:frequency"], raw and CSV in thesidecar. 0.0 writes nothing, in every one of them absent is how this library says "not stated", which is whatReader.fc_sourcereports back.totalexpected sample count, for the BLUE header; close() patches the real count, so 0 is fine when unknown.headroomdB of output backoff (gain = 10^(-H/20)) applied before quantisation. A single scale, so it does not change any power ratio only the absolute level. 0 is a bit-exact no-op.t0capture start, seconds since the UNIX epoch. Optional wherefsis required, because a capture with no wall-clock anchor is still readable and one with no rate is not. BLUE stores it as a J1950 timecode, SigMF ascaptures[0]["core:datetime"], raw and CSV in thesidecar. 0.0 means unset and stays unset it is never written as 1970.Reader.t0/Reader.t0_sourceread it back.sidecarwrite a<path>.sigmf-metaJSON beside a"raw"or"csv"capture, recording thefs,fcandt0those containers have nowhere to keep. On by default: the caller already supplied the values at construction, and dropping them on the floor left a file nobody its own author included could interpret. Only what was actually stated is written; nothing is invented. It is SigMF-SHAPED, not a SigMF capture: the spec pairs.sigmf-data, so the name is APPENDED rather than swapped (cap.raw->cap.raw.sigmf-meta), which keeps it 1:1 with its data file and unable to collide with a real capture's metadata. Ignored for"blue"(its header already carries all three) and for"sigmf", where the sidecar is half the capture and cannot be turned off. Pass false when an extra file beside the capture would break a downstream glob.
Returns:
a writer, or NULL if the path cannot be opened for writing (or is a SigMF path not ending in .sigmf-data).
>>> import pathlib, tempfile
>>> import numpy as np
>>> from doppler.wfm import Reader, Writer
>>> tmp = tempfile.TemporaryDirectory()
>>> p = pathlib.Path(tmp.name) / "capture.blue"
>>> x = np.arange(1024, dtype=np.complex64) / 1024.0
>>> with Writer(p, file_type="blue", sample_type="cf32",
... fs=2.4e6, fc=1.2e9) as w:
... w.write(x) # samples in
... w.add_keyword("COMMENT", "A", "demo") # tag the header
1024
>>> p.exists()
True
>>> with Reader(p) as r: # everything round-trips
... back = r.read(len(x))
... r.fs, r.fc, r.num_samples, r.keywords["COMMENT"]
(2400000.0, 1200000000.0, 1024, 'demo')
>>> bool(np.array_equal(back, x))
True
A raw capture has nowhere to put `fs`/`fc`, so they go beside it:
>>> q = pathlib.Path(tmp.name) / "capture.raw"
>>> with Writer(q, fs=2.4e6, fc=1.2e9) as w:
... w.write(x)
1024
>>> (q.parent / "capture.raw.sigmf-meta").exists()
True
>>> tmp.cleanup()
function wfm_writer_destroy¶
Finalise and free — the object binding's fallible destructor.
Identical to wfm_writer_close(); the object shape (gh-541) generates a Python close() from this that raises when it returns non-zero, so the finaliser's status reaches the caller and out of a with block. C callers may use either name.
Returns:
0 on success, non-zero on a write/seek error during finalisation.
function wfm_writer_flush¶
Make written samples durable and observable, without finishing.
Leaves the file on a sample boundary write() emits whole samples, so a flush BETWEEN write calls is what lets a follower read the capture without meeting a partial one. Raises OSError if this or any earlier write failed; a capture is not complete until close().
>>> import pathlib, tempfile
>>> import numpy as np
>>> from doppler.wfm import Reader, Writer
>>> tmp = tempfile.TemporaryDirectory()
>>> p = pathlib.Path(tmp.name) / "live.blue"
>>> w = Writer(p, file_type="blue", sample_type="ci16", fs=2.4e6)
>>> _ = w.write(np.zeros(16, dtype=np.complex64))
>>> w.flush() # the samples are on disk now
>>> Reader(p).read_follow(16).size
16
>>> w.close()
>>> tmp.cleanup()
function wfm_writer_get_clip_fraction¶
function wfm_writer_get_clipped¶
function wfm_writer_get_peak_dbfs¶
function wfm_writer_open¶
Open a writer on an already-open stream.
wfm_writer_state_t * wfm_writer_open (
FILE * fp,
wfm_filetype_t ft,
int sample_type,
int endian,
double fs,
double fc,
size_t total_samples,
double t0_unix_sec
)
Parameters:
fpdestination (binary mode for raw/blue; text-safe for csv).ftfile type; SIGMF is treated as RAW here.sample_typewire type (wavegen order); see file header.endian0 little, 1 big (ignored for csv).fssample rate (Hz) — BLUE xdelta = 1/fs. Pass 0.0 for "not known", which writes xdelta 0 and omits SigMF's core:sample_rate rather than claiming a rate.fccentre frequency (Hz). BLUE records it as aFREQkeyword — see wfm_writer_create; raw and CSV have nowhere to put it and drop it.total_samplesexpected complex-sample count for the BLUE header (0 if unknown; close() patches the actual count when fp is seekable).t0_unix_seccapture start, seconds since the UNIX epoch, or WFM_TIMECODE_UNSET (0.0) if unknown. BLUE stores it as a J1950 timecode, SigMF ascore:datetime; raw and CSV have nowhere to put it and drop it. A zero stays an unset field — it is never written as 1970.
Returns:
Writer handle, or NULL on bad args / allocation. BLUE writes its 512-byte header here.
function wfm_writer_peak¶
Largest per-axis magnitude max(|I|,|Q|) written so far (pre-clip, full-scale 1.0). > 1.0 ⇒ integer output clipped; peak_dBFS = 20*log10(peak).
function wfm_writer_set_gain¶
Set the output gain (linear; default 1.0). For headroom H dB pass 10^(−H/20).
function wfm_writer_track_clipping¶
Enable the per-component clip counter (off by default; peak is always on).
function wfm_writer_write¶
Convert and write a block of samples.
Takes complex64 at unit scale and emits it in the writer's wire type. Call as many times as you like; the capture is the concatenation.
Returns:
the number of samples that actually landed — equal to what you passed on success, fewer if the write was short (a full disk, a quota). A short return is the per-block signal; close() reports the same failure for the capture as a whole.
>>> import pathlib, tempfile
>>> from doppler.wfm import Composer, Reader, Segment, Writer
>>> tmp = tempfile.TemporaryDirectory()
>>> p = pathlib.Path(tmp.name) / "capture.blue"
>>> x = Composer([Segment("qpsk", sps=8, num_samples=1024)]).compose()
>>> with Writer(p, file_type="blue", sample_type="ci16",
... fs=2.4e6, fc=1.2e9) as w:
... w.write(x)
1024
>>> r = Reader(p)
>>> r.fs, r.fc, r.num_samples
(2400000.0, 1200000000.0, 1024)
>>> r.close()
>>> tmp.cleanup() # directory and contents removed
function write_blue_header¶
int write_blue_header (
const char * path,
double fs,
int sample_type,
int endian,
double fc,
double data_start,
size_t total,
int detached,
double t0
)
The documentation for this class was generated from the following file native/inc/wfm_writer/wfm_writer_core.h