Skip to content

File wfm_reader_core.h

FileList > inc > wfm_reader > wfm_reader_core.h

Go to the source code of this file

Input file types for generated IQ — the dual of wfm_writer. More...

  • #include <complex.h>
  • #include <stddef.h>
  • #include "wfm/wfm_keywords.h"
  • #include "wfm_writer/wfm_writer_core.h"
  • #include "dp_interrupt_guard/dp_interrupt_guard_core.h"

Classes

Type Name
struct wfm_reader_info_t

Public Types

Type Name
enum wfm_fc_source_t
enum wfm_follow_end_t
enum wfm_fs_source_t
enum wfm_mode_t
typedef struct wfm_reader_state wfm_reader_state_t
enum wfm_t0_source_t

Public Functions

Type Name
wfm_reader_state_t * wfm_reader_create (const char * path, int sample_type, int endian)
Open a capture, auto-detecting its file type from its content.
void wfm_reader_destroy (wfm_reader_state_t * state)
Close the file, free the reader and its decoded keywords.
const wfm_keyword_t * wfm_reader_find_header_field (const wfm_reader_state_t * state, const char * name)
Look up one HCB field by name, or NULL if absent.
const wfm_keyword_t * wfm_reader_find_keyword (const wfm_reader_state_t * r, const char * tag)
int wfm_reader_get_endian (const wfm_reader_state_t * state)
int wfm_reader_get_ending (const wfm_reader_state_t * state)
double wfm_reader_get_fc (const wfm_reader_state_t * state)
int wfm_reader_get_fc_source (const wfm_reader_state_t * state)
Which keyword wfm_reader_get_fc read the centre frequency from.
int wfm_reader_get_file_type (const wfm_reader_state_t * state)
uint32_t wfm_reader_get_follow_grace_ms (const wfm_reader_state_t * state)
uint32_t wfm_reader_get_follow_timeout_ms (const wfm_reader_state_t * state)
double wfm_reader_get_fs (const wfm_reader_state_t * state)
int wfm_reader_get_fs_source (const wfm_reader_state_t * state)
Which metadata wfm_reader_get_fs read the sample rate from.
int wfm_reader_get_mode (const wfm_reader_state_t * state)
size_t wfm_reader_get_num_samples (const wfm_reader_state_t * state)
int wfm_reader_get_sample_type (const wfm_reader_state_t * state)
double wfm_reader_get_t0 (const wfm_reader_state_t * state)
Capture start time in seconds since the UNIX epoch, or 0.0.
int wfm_reader_get_t0_source (const wfm_reader_state_t * state)
Where wfm_reader_get_t0 read the capture start time from.
size_t wfm_reader_get_trailing_bytes (const wfm_reader_state_t * state)
Payload bytes left over after the last whole sample.
const wfm_keyword_t * wfm_reader_header_field (const wfm_reader_state_t * state, size_t i)
The i-th decoded HCB field, or NULL if i is out of range.
const char * wfm_reader_header_tag (const wfm_reader_state_t * state, size_t i)
The i-th HCB field's name, for the .header dict binding.
void wfm_reader_info (const wfm_reader_state_t * r, wfm_reader_info_t * info)
Copy the resolved capture metadata into info .
const wfm_keyword_t * wfm_reader_keyword (const wfm_reader_state_t * r, size_t i)
The i'th keyword in file order, or NULL ifi is out of range.
const char * wfm_reader_keyword_tag (const wfm_reader_state_t * state, size_t i)
The tag of the i'th keyword (key_fn for the.keywords dict).
size_t wfm_reader_num_header_fields (const wfm_reader_state_t * state)
The first keyword whose tag equals tag , or NULL if absent.
size_t wfm_reader_num_keywords (const wfm_reader_state_t * state)
Number of extended-header keywords recovered from the capture.
size_t wfm_reader_read (wfm_reader_state_t * state, size_t n, float _Complex * out, size_t max_out)
Read up to count samples, returning them ascomplex64 .
size_t wfm_reader_read_follow (wfm_reader_state_t * state, size_t n, float _Complex * out, size_t max_out)
Read from a capture that is still being written.
size_t wfm_reader_read_follow_max_out (wfm_reader_state_t * state, size_t n)
size_t wfm_reader_read_max_out (wfm_reader_state_t * state, size_t n)
Maximum samples one read(n) yields: n (fewer at EOF).
void wfm_reader_reset (wfm_reader_state_t * state)
Rewind to the first sample of the capture.
void wfm_reader_set_follow_grace_ms (wfm_reader_state_t * state, uint32_t val)
void wfm_reader_set_follow_timeout_ms (wfm_reader_state_t * state, uint32_t val)
void wfm_reader_set_stop_fn (wfm_reader_state_t * state, int(*)(void) fn)
Tell a following read how to learn that a stop was requested.

Detailed Description

Reads back what wfm_writer wrote: raw interleaved I/Q, CSV, BLUE type-1000 (attached or detached, format mode S or C), and SigMF. A BLUE file in any other mode is rejected at open — see wfm_mode_t.

The file type is auto-detected from the file's CONTENT, not its name: the BLUE magic at byte 0, a first line that parses as I,Q for CSV, a .sigmf-meta sidecar alongside. The extension only breaks a tie the content cannot (a .det payload, which is headerless by construction). So a CSV called capture.dat reads as CSV, and a BLUE file called capture.csv reads as BLUE — misnaming a capture costs nothing.

Self-describing file types (BLUE, SigMF) recover the sample type, byte order, sample rate and centre frequency from their metadata. Headerless file types (raw, CSV) take the sample type / byte order as hints, and there is no way to check a hint against the file — see wfm_reader_get_trailing_bytes for the one tell that is available.

Samples come out as float _Complex at unit scale: float wire types are reinterpreted, integer wire types are rescaled by their full-scale (the exact inverse of the writer's quantiser).

wfm_reader_state_t *r = wfm_reader_create("cap.sigmf-data", 0, 0);
wfm_reader_info_t info;
wfm_reader_info(r, &info);                 // info.fs, info.sample_type, ...
float _Complex buf[4096];
size_t n;
while ((n = wfm_reader_read(r, 4096, buf, 4096)) > 0)   // (state, count, out)
  consume(buf, n);
wfm_reader_destroy(r);

Public Types Documentation

enum wfm_fc_source_t

enum wfm_fc_source_t {
    WFM_FC_NONE = 0,
    WFM_FC_FREQ,
    WFM_FC_RF_FREQ,
    WFM_FC_CENTER_FREQ,
    WFM_FC_F_C,
    WFM_FC_SIGMF
};

Where a capture's centre frequency came from.

BLUE type-1000 has no HCB field for centre frequency — the adjunct carries xstart/xdelta/xunits, which describe the abscissa (time), not the RF the capture was taken at. So an RF capture conveys it as a keyword, and which tag it uses is X-Midas convention rather than anything BLUE 1.1 mandates: 3.1.2.6.4.4 defines FREQ, but only as a type-6000 column name, under a heading stating those names "are not keyword names". FREQ in the HCB keyword area is nonetheless what real captures carry, so it is what this reader looks for first.

Reporting the tag matters because 0.0 is a legitimate answer: a genuine baseband capture and a capture whose frequency this library failed to find are otherwise indistinguishable. WFM_FC_NONE says "not found", and only then is fc == 0.0 a guess rather than a reading.


enum wfm_follow_end_t

enum wfm_follow_end_t {
    WFM_FOLLOW_NONE = 0,
    WFM_FOLLOW_EOF,
    WFM_FOLLOW_TIMEOUT,
    WFM_FOLLOW_INTERRUPTED
};

Why a following read came back empty.

Indices, because that is what the ending property decodes to a string. Each corresponds 1:1 to the doppler return code a C caller would expect from any other transport WFM_FOLLOW_EOF is DP_ERR_EOF, and so on so the two faces name the same four states even though the property carries the index.


enum wfm_fs_source_t

enum wfm_fs_source_t {
    WFM_FS_NONE = 0,
    WFM_FS_BLUE_XDELTA,
    WFM_FS_SIGMF
};

Where fs came from, for the same reason wfm_fc_source_t exists.

fs == 0.0 is not a rate anyone captured at, so it is less ambiguous than fc == 0.0 — but "which metadata said so" is still the difference between a capture that declares its rate and one this library had to give up on, and a caller about to build a timeline on it wants to know which it has.


enum wfm_mode_t

enum wfm_mode_t {
    WFM_MODE_COMPLEX = 0,
    WFM_MODE_SCALAR = 1
};

Components per sample — the BLUE format field's mode designator (HCB byte 52). Only these two are supported; every other Midas mode (V/Q/M/T/…, 3..10 components) is rejected at open rather than misinterpreted as interleaved I/Q. Non-BLUE file types are complex.


typedef wfm_reader_state_t

typedef struct wfm_reader_state wfm_reader_state_t;

Opaque reader handle. Opaque reader state; the layout is private to wfm_reader_core.c.


enum wfm_t0_source_t

enum wfm_t0_source_t {
    WFM_T0_NONE = 0,
    WFM_T0_BLUE_TIMECODE,
    WFM_T0_SIGMF
};

Where the capture's absolute start time came from.

This is the t0 of t = t0 + n/fs — the epoch belonging to the DATA, which is what makes a replayed capture's telemetry line up with the recording rather than with the machine replaying it. Hand it to dp_sample_clock_track(); the sample clock owns the arithmetic.

WFM_T0_NONE is the common case and has to stay visible: doppler's own BLUE writer leaves the timecode field zero, so a zero there means "unset", never 1950-01-01 (see wfm/wfm_time.h). A caller that cannot tell those apart will confidently date every doppler-written capture to *


Public Functions Documentation

function wfm_reader_create

Open a capture, auto-detecting its file type from its content.

wfm_reader_state_t * wfm_reader_create (
    const char * path,
    int sample_type,
    int endian
) 

Detection order, first match wins: the BLUE magic at byte 0; a first line that scans as I,Q; otherwise headerless raw. Two suffixes are decided by name instead, because neither has content that identifies it — .det, a detached payload described by its header sibling, and .sigmf-data, half of a pair whose other half carries the datatype.

Nothing is refused for looking unfamiliar: an unrecognised file opens as raw at the caller's sample_type, because a truncated or partial recording is a real thing and a reader that rejects it is useless. What you get instead of a refusal is wfm_reader_get_trailing_bytes.

Parameters:

  • path file to read a str or any os.PathLike from Python. For a DETACHED BLUE capture this is normally the HEADER file <base>.tmp or <base>.prm per BLUE 3.1.1.4 (this library's own writer emits <base>.hdr) whose HCB detached field points at the collocated <base>.det payload; the extension does not decide, detached does. Passing the <base>.det directly also works (its header sibling is resolved). A SigMF .sigmf-data file resolves its .sigmf-meta sidecar the same way.
  • sample_type the wire sample type, used only as a HINT for the headerless file types (raw, CSV) BLUE and SigMF carry their own and ignore it. The five complex names "cf32", "cf64", "ci32", "ci16", "ci8" or the five real ones "f32", "f64", "i32", "i16", "i8" from Python; the matching 0..9 from C. A real hint is the only way to say that a headerless file carries one component per sample rather than interleaved I/Q. A wrong hint does not fail; see wfm_reader_get_trailing_bytes.
  • endian byte order, likewise a hint that only headerless raw uses; "le" or "be" from Python, 0 or 1 from C.

Returns:

a reader, or NULL on open/parse failure.

>>> 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()
>>> w = Writer(p, file_type="blue", sample_type="ci16", fs=2.4e6)
>>> w.add_keyword("NAME", "A", "demo")   # tag the header
>>> _ = w.write(x)
>>> w.close()
>>> r = Reader(p)                         # file type auto-detected
>>> r.file_type, r.sample_type, r.fs
('blue', 'ci16', 2400000.0)
>>> r.keywords["NAME"]                    # keyword round-trips
'demo'
>>> total = 0
>>> while len(block := r.read(256)):      # read returns 0 at EOF
...     total += len(block)
>>> total == r.num_samples == 1024
True
>>> r.close()
>>> tmp.cleanup()

function wfm_reader_destroy

Close the file, free the reader and its decoded keywords.

void wfm_reader_destroy (
    wfm_reader_state_t * state
) 


function wfm_reader_find_header_field

Look up one HCB field by name, or NULL if absent.

const wfm_keyword_t * wfm_reader_find_header_field (
    const wfm_reader_state_t * state,
    const char * name
) 


function wfm_reader_find_keyword

const wfm_keyword_t * wfm_reader_find_keyword (
    const wfm_reader_state_t * r,
    const char * tag
) 

function wfm_reader_get_endian

int wfm_reader_get_endian (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_ending

int wfm_reader_get_ending (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_fc

double wfm_reader_get_fc (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_fc_source

Which keyword wfm_reader_get_fc read the centre frequency from.

int wfm_reader_get_fc_source (
    const wfm_reader_state_t * state
) 

A wfm_fc_source_t. WFM_FC_NONE means nothing was found, which is the only way to tell a baseband capture (fc genuinely 0 Hz) from one whose frequency this library could not locate — both report fc == 0.0.


function wfm_reader_get_file_type

int wfm_reader_get_file_type (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_follow_grace_ms

uint32_t wfm_reader_get_follow_grace_ms (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_follow_timeout_ms

uint32_t wfm_reader_get_follow_timeout_ms (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_fs

double wfm_reader_get_fs (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_fs_source

Which metadata wfm_reader_get_fs read the sample rate from.

int wfm_reader_get_fs_source (
    const wfm_reader_state_t * state
) 

A wfm_fs_source_t. WFM_FS_NONE means nothing carried a rate — raw and CSV always, and any BLUE header whose xdelta is zero.


function wfm_reader_get_mode

int wfm_reader_get_mode (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_num_samples

size_t wfm_reader_get_num_samples (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_sample_type

int wfm_reader_get_sample_type (
    const wfm_reader_state_t * state
) 

function wfm_reader_get_t0

Capture start time in seconds since the UNIX epoch, or 0.0.

double wfm_reader_get_t0 (
    const wfm_reader_state_t * state
) 

The t0 of t = t0 + n/fs, belonging to the capture rather than to whatever is reading it — hand it to dp_sample_clock_track() and a replayed recording's timeline lands where the samples were taken, not where they were played back.

0.0 does not mean 1970. Check wfm_reader_get_t0_source first: WFM_T0_NONE is "not found", which is the usual answer, including for every capture doppler itself writes.


function wfm_reader_get_t0_source

Where wfm_reader_get_t0 read the capture start time from.

int wfm_reader_get_t0_source (
    const wfm_reader_state_t * state
) 

A wfm_t0_source_t. WFM_T0_NONE is the common case and the one that matters: a zero BLUE timecode means the field was never set, not 1950-01-01, so a caller that skips this check dates every such capture to 1950.


function wfm_reader_get_trailing_bytes

Payload bytes left over after the last whole sample.

size_t wfm_reader_get_trailing_bytes (
    const wfm_reader_state_t * state
) 

A capture is a whole number of samples, so this is 0 for every file whose declared sample type and mode match its content. Non-zero means one of two things, and the reader cannot tell them apart:

  • the sample_type/endian hint is wrong for a headerless file type (reading a ci16 file as cf32 leaves a remainder unless the length happens to divide), or
  • the capture is truncated — a recording that was cut mid-sample.

Either way the leftover bytes are dropped: wfm_reader_read stops at the last complete sample. This exists because there is otherwise no signal at all. A wrong hint on a headerless file does not fail, it returns plausible garbage at the wrong stride, and nothing in the samples themselves says so.

Always 0 for CSV, which is delimited rather than strided.


function wfm_reader_header_field

The i-th decoded HCB field, or NULL if i is out of range.

const wfm_keyword_t * wfm_reader_header_field (
    const wfm_reader_state_t * state,
    size_t i
) 

Every field of the 512-byte header control block is carried as a wfm_keyword_t, under the name the format itself uses data_start, ext_size, xdelta and so on (Midas BLUE 1.1 3.1.1). Reusing the keyword struct means the header and the keywords share one tag/value codec, so a double or an ASCII field can never be turned into a Python object two different ways.


function wfm_reader_header_tag

The i-th HCB field's name, for the .header dict binding.

const char * wfm_reader_header_tag (
    const wfm_reader_state_t * state,
    size_t i
) 


function wfm_reader_info

Copy the resolved capture metadata into info .

void wfm_reader_info (
    const wfm_reader_state_t * r,
    wfm_reader_info_t * info
) 


function wfm_reader_keyword

The i'th keyword in file order, or NULL ifi is out of range.

const wfm_keyword_t * wfm_reader_keyword (
    const wfm_reader_state_t * r,
    size_t i
) 

The returned pointer (and its value buffer) is owned by the reader and is freed by wfm_reader_destroy().


function wfm_reader_keyword_tag

The tag of the i'th keyword (key_fn for the.keywords dict).

const char * wfm_reader_keyword_tag (
    const wfm_reader_state_t * state,
    size_t i
) 

jm's generated dict loop (gh-543) calls this for every index in [0, wfm_reader_num_keywords()), so i is always in range. The returned pointer is owned by the reader.


function wfm_reader_num_header_fields

The first keyword whose tag equals tag , or NULL if absent.

size_t wfm_reader_num_header_fields (
    const wfm_reader_state_t * state
) 

Tags are not required to be unique; this returns the earliest match.

Number of decoded HCB fields (0 for a non-BLUE file type).


function wfm_reader_num_keywords

Number of extended-header keywords recovered from the capture.

size_t wfm_reader_num_keywords (
    const wfm_reader_state_t * state
) 

BLUE only, and 0 unless the file carries an extended header. Keywords of a type this library cannot decode are skipped during the walk (BLUE §3.3.1) and are not counted; a truncated or malformed keyword region yields whatever decoded cleanly before it, since metadata must never cost you the samples. For a detached capture the keywords come from the HEADER file, not the .det.


function wfm_reader_read

Read up to count samples, returning them ascomplex64 .

size_t wfm_reader_read (
    wfm_reader_state_t * state,
    size_t n,
    float _Complex * out,
    size_t max_out
) 

Samples come out at unit scale whatever the wire type was: a float type is reinterpreted, an integer type is divided by its full scale. Returns fewer than asked at the end of the capture, and 0 once it is exhausted, so a while over the result terminates. Never returns more than the file's declared payload — trailing bytes past data_size (an extended header, X-Midas slack) are not samples.

Parameters:

  • state the reader.
  • n how many samples to read (count in the Python binding, which also accepts an optional pre-allocated out= array to avoid an allocation per block in a streaming loop).
  • out destination, at least max_out samples.
  • max_out capacity of out; emission stops there.
>>> 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)
>>> r = Reader(p)
>>> r.file_type, r.sample_type, r.endian
('blue', 'ci16', 'le')
>>> r.fs, r.fc, r.fc_source
(2400000.0, 1200000000.0, 'FREQ')
>>> total = 0
>>> while len(block := r.read(256)):
...     total += len(block)
>>> total
1024
>>> r.close()
>>> tmp.cleanup()   # directory and contents removed

function wfm_reader_read_follow

Read from a capture that is still being written.

size_t wfm_reader_read_follow (
    wfm_reader_state_t * state,
    size_t n,
    float _Complex * out,
    size_t max_out
) 

Blocks until whole samples arrive. A short or empty result does not mean end-of-file the way wfm_reader_read's does the reader waits. Zero means the capture ENDED, because with the default unbounded budgets the call does not come back for "not yet"; wfm_reader_get_ending says which way it ended.

>>> 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.zeros(8, dtype=np.complex64)
>>> with Writer(p, file_type="blue", sample_type="ci16", fs=2.4e6) as w:
...     _ = w.write(x)
>>> r = Reader(p)
>>> total = 0
>>> while len(block := r.read_follow(4)):   # 0 only when the capture ends
...     total += len(block)
>>> total, r.ending
(8, 'eof')
>>> r.close()
>>> tmp.cleanup()

function wfm_reader_read_follow_max_out

size_t wfm_reader_read_follow_max_out (
    wfm_reader_state_t * state,
    size_t n
) 

function wfm_reader_read_max_out

Maximum samples one read(n) yields: n (fewer at EOF).

size_t wfm_reader_read_max_out (
    wfm_reader_state_t * state,
    size_t n
) 

A reader streams, so a read of n produces at most n samples; the binding sizes its buffer to this per-call bound (gh-607) and resizes down to the actual count, never pre-allocating the whole capture.


function wfm_reader_reset

Rewind to the first sample of the capture.

void wfm_reader_reset (
    wfm_reader_state_t * state
) 

Seeks back to where the payload starts — 512 bytes into an attached BLUE file, byte 0 of a .det or a raw/SigMF payload — and restores the remaining-sample count, so the capture reads again from the top. The file's metadata and decoded keywords are unaffected: they came from the header and do not change.


function wfm_reader_set_follow_grace_ms

void wfm_reader_set_follow_grace_ms (
    wfm_reader_state_t * state,
    uint32_t val
) 

function wfm_reader_set_follow_timeout_ms

void wfm_reader_set_follow_timeout_ms (
    wfm_reader_state_t * state,
    uint32_t val
) 

function wfm_reader_set_stop_fn

Tell a following read how to learn that a stop was requested.

void wfm_reader_set_stop_fn (
    wfm_reader_state_t * state,
    int(*)(void) fn
) 

read_follow() blocks until data arrives; fn is what lets it stop for a reason other than the capture ending. It is INJECTED rather than hard-wired because a capture reader has no business depending on the process interrupt primitive: doing so would put dp_interrupt.c on the link line of every consumer of wfm_reader_core, and the policy is the caller's anyway. doppler passes dp_interrupted; a test passes its own.

NULL (the default) means the follow read never stops early only the capture's end or a bounded budget finishes it.

wfm_reader_set_stop_fn (r, dp_interrupted);


The documentation for this class was generated from the following file native/inc/wfm_reader/wfm_reader_core.h