Skip to content

File wfm_reader_core.h

FileList > doppler > 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 "doppler/dp_complex.h"
  • #include <stddef.h>
  • #include <stdint.h>
  • #include "doppler/wfm/wfm_keywords.h"
  • #include "doppler/wfm_writer/wfm_writer_core.h"
  • #include "doppler/dp_interrupt_guard/dp_interrupt_guard_core.h"

Classes

Type Name
struct wfm_reader_info_t

Public Types

Type Name
typedef struct wfm_reader_state dp_wfm_reader_state_t
enum wfm_fc_source_t
enum wfm_follow_end_t
enum wfm_fs_source_t
enum wfm_mode_t
enum wfm_reader_stype_t
sample_type value meaning "the caller said nothing".
enum wfm_t0_source_t

Public Functions

Type Name
dp_wfm_reader_state_t * dp_wfm_reader_create (const char * path, int sample_type, int endian)
Open a capture exactly as dp_wfm_reader_create_interruptible does, but install no stop predicate.
dp_wfm_reader_state_t * dp_wfm_reader_create_interruptible (const char * path, int sample_type, int endian)
Open a capture, auto-detecting its file type from its content.
void dp_wfm_reader_destroy (dp_wfm_reader_state_t * state)
Close the file, free the reader and its decoded keywords.
const wfm_keyword_t * dp_wfm_reader_find_header_field (const dp_wfm_reader_state_t * state, const char * name)
Look up one HCB field by name, or NULL if absent.
const wfm_keyword_t * dp_wfm_reader_find_keyword (const dp_wfm_reader_state_t * r, const char * tag)
int dp_wfm_reader_get_endian (const dp_wfm_reader_state_t * state)
int dp_wfm_reader_get_ending (const dp_wfm_reader_state_t * state)
double dp_wfm_reader_get_fc (const dp_wfm_reader_state_t * state)
int dp_wfm_reader_get_fc_source (const dp_wfm_reader_state_t * state)
Which keyword dp_wfm_reader_get_fc read the centre frequency from.
int dp_wfm_reader_get_file_type (const dp_wfm_reader_state_t * state)
uint32_t dp_wfm_reader_get_follow_grace_ms (const dp_wfm_reader_state_t * state)
uint32_t dp_wfm_reader_get_follow_timeout_ms (const dp_wfm_reader_state_t * state)
double dp_wfm_reader_get_fs (const dp_wfm_reader_state_t * state)
int dp_wfm_reader_get_fs_source (const dp_wfm_reader_state_t * state)
Which metadata dp_wfm_reader_get_fs read the sample rate from.
int dp_wfm_reader_get_mode (const dp_wfm_reader_state_t * state)
size_t dp_wfm_reader_get_num_samples (const dp_wfm_reader_state_t * state)
size_t dp_wfm_reader_get_position (const dp_wfm_reader_state_t * state)
The read position, in samples from the first sample.
int dp_wfm_reader_get_sample_type (const dp_wfm_reader_state_t * state)
double dp_wfm_reader_get_t0 (const dp_wfm_reader_state_t * state)
Capture start time in seconds since the UNIX epoch, or 0.0.
int dp_wfm_reader_get_t0_source (const dp_wfm_reader_state_t * state)
Where dp_wfm_reader_get_t0 read the capture start time from.
size_t dp_wfm_reader_get_trailing_bytes (const dp_wfm_reader_state_t * state)
Payload bytes left over after the last whole sample.
const wfm_keyword_t * dp_wfm_reader_header_field (const dp_wfm_reader_state_t * state, size_t i)
The i-th decoded HCB field, or NULL if i is out of range.
const char * dp_wfm_reader_header_tag (const dp_wfm_reader_state_t * state, size_t i)
The i-th HCB field's name, for the .header dict binding.
void dp_wfm_reader_info (const dp_wfm_reader_state_t * r, wfm_reader_info_t * info)
Copy the resolved capture metadata into info .
const wfm_keyword_t * dp_wfm_reader_keyword (const dp_wfm_reader_state_t * r, size_t i)
The i'th keyword in file order, or NULL ifi is out of range.
const char * dp_wfm_reader_keyword_tag (const dp_wfm_reader_state_t * state, size_t i)
The tag of the i'th keyword (key_fn for the.keywords dict).
size_t dp_wfm_reader_num_header_fields (const dp_wfm_reader_state_t * state)
The first keyword whose tag equals tag , or NULL if absent.
size_t dp_wfm_reader_num_keywords (const dp_wfm_reader_state_t * state)
Number of extended-header keywords recovered from the capture.
size_t dp_wfm_reader_read (dp_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 dp_wfm_reader_read_follow (dp_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 dp_wfm_reader_read_follow_max_out (dp_wfm_reader_state_t * state, size_t n)
size_t dp_wfm_reader_read_max_out (dp_wfm_reader_state_t * state, size_t n)
Maximum samples one read(n) yields: n (fewer at EOF).
void dp_wfm_reader_reset (dp_wfm_reader_state_t * state)
Rewind to the first sample of the capture.
int dp_wfm_reader_seek (dp_wfm_reader_state_t * state, int64_t index)
Move the read position to sample index .
int dp_wfm_reader_seek_time (dp_wfm_reader_state_t * state, double seconds)
Move the read position to seconds into the capture.
void dp_wfm_reader_set_follow_grace_ms (dp_wfm_reader_state_t * state, uint32_t val)
void dp_wfm_reader_set_follow_timeout_ms (dp_wfm_reader_state_t * state, uint32_t val)
void dp_wfm_reader_set_stop_fn (dp_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 dp_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).

// header-code: no-run=opens a capture file that is not on disk
dp_wfm_reader_state_t *r = dp_wfm_reader_create("cap.sigmf-data", 0, 0);
wfm_reader_info_t info;
dp_wfm_reader_info(r, &info);                 // info.fs, info.sample_type, ...
float _Complex buf[4096];
size_t n, total = 0;
while ((n = dp_wfm_reader_read(r, 4096, buf, 4096)) > 0)   // (state, count, out)
  total += n;
dp_wfm_reader_destroy(r);

Public Types Documentation

typedef dp_wfm_reader_state_t

typedef struct wfm_reader_state dp_wfm_reader_state_t;

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


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.


enum wfm_reader_stype_t

sample_type value meaning "the caller said nothing".

enum wfm_reader_stype_t {
    WFM_READER_STYPE_AUTO = -1,
    WFM_READER_STYPE_CF32 = 0,
    WFM_READER_STYPE_CF64,
    WFM_READER_STYPE_CI32,
    WFM_READER_STYPE_CI16,
    WFM_READER_STYPE_CI8,
    WFM_READER_STYPE_F32,
    WFM_READER_STYPE_F64,
    WFM_READER_STYPE_I32,
    WFM_READER_STYPE_I16,
    WFM_READER_STYPE_I8
};

Distinct from cf32 deliberately. Reader(p) and Reader(p, sample_type="cf32") have to mean different things for a stale sidecar to be overridable, and they cannot if the default IS cf32. -1 was free dp_wfm_reader_create returned NULL for it so giving it a meaning cannot change what any existing caller gets. The sample_type hint dp_wfm_reader_create takes, by name.

Ten wire types in wavegen order (the same indices as the stype manifest enum and STYPE_NAMES[]) plus WFM_READER_STYPE_AUTO, which is NOT one of them. It is the reason this is an enum rather than the stype table: "auto" is -1, so a binding that numbered the names by position would make it 0 and slide every other name up by one.


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 dp_wfm_reader_create

Open a capture exactly as dp_wfm_reader_create_interruptible does, but install no stop predicate.

dp_wfm_reader_state_t * dp_wfm_reader_create (
    const char * path,
    int sample_type,
    int endian
) 

This is the constructor the Python Reader calls: it opens exactly as dp_wfm_reader_create does, then installs dp_interrupted as the stop predicate (dp_wfm_reader_set_stop_fn), so Ctrl-C ends a following read. Without a predicate a follow read has no escape at all, because both budgets default to "forever" on purpose. A C consumer that wants a different predicate, or none, keeps dp_wfm_reader_create. The link cost is nil: this object already links dp_interrupt_guard_core, and only a caller of this function pulls dp_interrupted out of an archive.

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 dp_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. "auto" (WFM_READER_STYPE_AUTO from C) is the DEFAULT and says the caller has no opinion: a headerless capture takes its type, byte order and rate from the <path>.sigmf-meta sidecar this library's own writer leaves beside it, falling back to cf32/le when there is none. A NAMED type still wins over the sidecar, so a stale one can be overridden. A wrong hint does not fail, and dp_wfm_reader_get_trailing_bytes is NOT the way to notice see what it says about itself.
  • 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()

The one difference: the stop predicate is left NULL, so a read_follow() ends only with the capture or a bounded budget until the caller installs one with dp_wfm_reader_set_stop_fn.


function dp_wfm_reader_create_interruptible

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

dp_wfm_reader_state_t * dp_wfm_reader_create_interruptible (
    const char * path,
    int sample_type,
    int endian
) 

This is the constructor the Python Reader calls: it opens exactly as dp_wfm_reader_create does, then installs dp_interrupted as the stop predicate (dp_wfm_reader_set_stop_fn), so Ctrl-C ends a following read. Without a predicate a follow read has no escape at all, because both budgets default to "forever" on purpose. A C consumer that wants a different predicate, or none, keeps dp_wfm_reader_create. The link cost is nil: this object already links dp_interrupt_guard_core, and only a caller of this function pulls dp_interrupted out of an archive.

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 dp_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. "auto" (WFM_READER_STYPE_AUTO from C) is the DEFAULT and says the caller has no opinion: a headerless capture takes its type, byte order and rate from the <path>.sigmf-meta sidecar this library's own writer leaves beside it, falling back to cf32/le when there is none. A NAMED type still wins over the sidecar, so a stale one can be overridden. A wrong hint does not fail, and dp_wfm_reader_get_trailing_bytes is NOT the way to notice see what it says about itself.
  • 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 dp_wfm_reader_destroy

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

void dp_wfm_reader_destroy (
    dp_wfm_reader_state_t * state
) 


function dp_wfm_reader_find_header_field

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

const wfm_keyword_t * dp_wfm_reader_find_header_field (
    const dp_wfm_reader_state_t * state,
    const char * name
) 


function dp_wfm_reader_find_keyword

const wfm_keyword_t * dp_wfm_reader_find_keyword (
    const dp_wfm_reader_state_t * r,
    const char * tag
) 

function dp_wfm_reader_get_endian

int dp_wfm_reader_get_endian (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_ending

int dp_wfm_reader_get_ending (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_fc

double dp_wfm_reader_get_fc (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_fc_source

Which keyword dp_wfm_reader_get_fc read the centre frequency from.

int dp_wfm_reader_get_fc_source (
    const dp_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 dp_wfm_reader_get_file_type

int dp_wfm_reader_get_file_type (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_follow_grace_ms

uint32_t dp_wfm_reader_get_follow_grace_ms (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_follow_timeout_ms

uint32_t dp_wfm_reader_get_follow_timeout_ms (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_fs

double dp_wfm_reader_get_fs (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_fs_source

Which metadata dp_wfm_reader_get_fs read the sample rate from.

int dp_wfm_reader_get_fs_source (
    const dp_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 dp_wfm_reader_get_mode

int dp_wfm_reader_get_mode (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_num_samples

size_t dp_wfm_reader_get_num_samples (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_position

The read position, in samples from the first sample.

size_t dp_wfm_reader_get_position (
    const dp_wfm_reader_state_t * state
) 

The dual of seek(), and what every read advances: 0 at open and after reset(), num_samples once the capture is exhausted. Counted in samples rather than bytes so that it means the same thing on a CSV, which has no fixed stride to divide by.


function dp_wfm_reader_get_sample_type

int dp_wfm_reader_get_sample_type (
    const dp_wfm_reader_state_t * state
) 

function dp_wfm_reader_get_t0

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

double dp_wfm_reader_get_t0 (
    const dp_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 dp_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 dp_wfm_reader_get_t0_source

Where dp_wfm_reader_get_t0 read the capture start time from.

int dp_wfm_reader_get_t0_source (
    const dp_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 dp_wfm_reader_get_trailing_bytes

Payload bytes left over after the last whole sample.

size_t dp_wfm_reader_get_trailing_bytes (
    const dp_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: dp_wfm_reader_read stops at the last complete sample.

Warning:

This is not a check for a wrong hint, and cannot be made into one. It only fires when the byte count fails to divide, so it is 0 for every EVEN sample count which is most captures, and every power-of-two one. Worse, a ci32 file read as cf32 is 8 bytes per sample either way: the count divides at every length, num_samples is correct, this reads 0, and every sample is wrong. It is a truncation signal that a wrong hint sometimes also trips. Measured in doppler#1120; the answer to a wrong hint is not to detect it afterwards but to stop guessing pass "auto" (the default) and let the capture's own sidecar name its type.

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


function dp_wfm_reader_header_field

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

const wfm_keyword_t * dp_wfm_reader_header_field (
    const dp_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 dp_wfm_reader_header_tag

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

const char * dp_wfm_reader_header_tag (
    const dp_wfm_reader_state_t * state,
    size_t i
) 


function dp_wfm_reader_info

Copy the resolved capture metadata into info .

void dp_wfm_reader_info (
    const dp_wfm_reader_state_t * r,
    wfm_reader_info_t * info
) 


function dp_wfm_reader_keyword

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

const wfm_keyword_t * dp_wfm_reader_keyword (
    const dp_wfm_reader_state_t * r,
    size_t i
) 

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


function dp_wfm_reader_keyword_tag

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

const char * dp_wfm_reader_keyword_tag (
    const dp_wfm_reader_state_t * state,
    size_t i
) 

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


function dp_wfm_reader_num_header_fields

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

size_t dp_wfm_reader_num_header_fields (
    const dp_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 dp_wfm_reader_num_keywords

Number of extended-header keywords recovered from the capture.

size_t dp_wfm_reader_num_keywords (
    const dp_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 dp_wfm_reader_read

Read up to count samples, returning them ascomplex64 .

size_t dp_wfm_reader_read (
    dp_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 dp_wfm_reader_read_follow

Read from a capture that is still being written.

size_t dp_wfm_reader_read_follow (
    dp_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 dp_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"; dp_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 dp_wfm_reader_read_follow_max_out

size_t dp_wfm_reader_read_follow_max_out (
    dp_wfm_reader_state_t * state,
    size_t n
) 

function dp_wfm_reader_read_max_out

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

size_t dp_wfm_reader_read_max_out (
    dp_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 dp_wfm_reader_reset

Rewind to the first sample of the capture.

void dp_wfm_reader_reset (
    dp_wfm_reader_state_t * state
) 

seek() at index 0, and nothing else — one implementation of "put the read position at sample k" rather than two that can drift. It lands where the payload starts (512 bytes into an attached BLUE file, byte 0 of a .det or a raw/SigMF payload), 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 dp_wfm_reader_seek

Move the read position to sample index .

int dp_wfm_reader_seek (
    dp_wfm_reader_state_t * state,
    int64_t index
) 

Random access, in the timebase the data owns. The index is absolute — 0 is the first sample, there is no whence — and it is in SAMPLES, which is the only unit every container can answer: fs is 0.0 on a headerless capture, so a time would mean nothing there — see seek_time(), which refuses rather than pretend.

Cost follows the container. Raw, BLUE and SigMF are strided, so this is one fseek to data_start + index * bytes_per_sample. CSV is delimited rather than strided and has no byte arithmetic at all, so it is a scan: forward from the current position when seeking forward, from the first sample when seeking back. A loop that seeks forward monotonically therefore walks the file once, not once per seek.

Seeking to num_samples exactly is legal and lands at the end, where read() returns 0. Past it is refused, as is a negative index: that is a caller's arithmetic gone wrong, and a silent empty read would hide it. A refused seek does not move the read position — the bound is checked before anything moves, and the CSV scan puts the position back if it runs out.

On a capture still being written, the bound is what is on disk right now, measured at the call. A BLUE capture whose writer has not closed yet still carries the placeholder data_size it opened with, so its declared length is not used until the writer patches it in; read_follow() is the read that works on such a capture, and seeking does not change that.

Parameters:

  • state the reader.
  • index sample to move to; 0 to num_samples inclusive.

Returns:

DP_OK, or DP_ERR_INVALID if index is negative or past the end of the capture.

>>> import pathlib, tempfile
>>> import numpy as np
>>> 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="cf32", fs=2.4e6)
>>> _ = w.write(x)
>>> w.close()
>>> r = Reader(p)
>>> r.seek(600)                  # straight there, no decode first
>>> np.array_equal(r.read(424), x[600:])
True
>>> r.position                      # the read left us at the end
1024
>>> try:                            # past the end is a refusal...
...     r.seek(1025)
... except ValueError:
...     print("refused")
refused
>>> r.position                      # ...that did not move us
1024
>>> r.close()
>>> tmp.cleanup()

function dp_wfm_reader_seek_time

Move the read position to seconds into the capture.

int dp_wfm_reader_seek_time (
    dp_wfm_reader_state_t * state,
    double seconds
) 

seek() over index = round(seconds * fs), and the rounding is to nearest. The conversion is the whole method; the REFUSAL is the point of having it.

A capture that declares no sample rate reports fs == 0.0, and that is raw and CSV always — neither container has anywhere to record one. A caller computing round(t * r.fs) for itself gets sample 0 for every time, silently. So this refuses when fs_source says nothing declared a rate, rather than convert through one — the same reason the provenance accessors exist at all.

Seconds are measured from the FIRST SAMPLE of the capture, never from the UNIX epoch. A capture's absolute start is t0, and t0_source is none on every capture doppler itself writes — so an absolute face would be unusable by default. Converting is the caller's, and it is r.seek_time(t_unix - r.t0) once t0_source says there is a t0 to subtract.

Parameters:

  • state the reader.
  • seconds offset from the first sample; 0 to num_samples / fs inclusive.

Returns:

DP_OK, or DP_ERR_INVALID if the capture declares no sample rate, or seconds is negative, not finite, or past the end.

>>> import pathlib, tempfile
>>> import numpy as np
>>> 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="cf32", fs=1e6)
>>> _ = w.write(x)
>>> w.close()
>>> r = Reader(p)
>>> r.fs, r.fs_source            # the rate it converts through
(1000000.0, 'xdelta')
>>> r.seek_time(250e-6)             # 250 us at 1 MHz is sample 250
>>> r.position
250
>>> np.array_equal(r.read(774), x[250:])
True
>>> r.close()
>>> raw = pathlib.Path(tmp.name) / "capture.raw"
>>> w = Writer(raw, 0.0, file_type="raw", sample_type="cf32",
...            sidecar=False)   # no sidecar: nothing records a rate
>>> _ = w.write(x)
>>> w.close()
>>> h = Reader(raw, sample_type="cf32")
>>> h.fs, h.fs_source            # headerless: no rate declared
(0.0, 'none')
>>> try:                            # so a time cannot mean anything
...     h.seek_time(250e-6)
... except ValueError:
...     print("refused")
refused
>>> h.seek(250)                  # ...the sample index still does
>>> h.position
250
>>> h.close()
>>> tmp.cleanup()

function dp_wfm_reader_set_follow_grace_ms

void dp_wfm_reader_set_follow_grace_ms (
    dp_wfm_reader_state_t * state,
    uint32_t val
) 

function dp_wfm_reader_set_follow_timeout_ms

void dp_wfm_reader_set_follow_timeout_ms (
    dp_wfm_reader_state_t * state,
    uint32_t val
) 

function dp_wfm_reader_set_stop_fn

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

void dp_wfm_reader_set_stop_fn (
    dp_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.

dp_wfm_reader_set_stop_fn (r, dp_interrupted);


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