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¶
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¶
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¶
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¶
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.
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:
pathfile to read astror anyos.PathLikefrom Python. For a DETACHED BLUE capture this is normally the HEADER file<base>.tmpor<base>.prmper BLUE 3.1.1.4 (this library's own writer emits<base>.hdr) whose HCBdetachedfield points at the collocated<base>.detpayload; the extension does not decide,detacheddoes. Passing the<base>.detdirectly also works (its header sibling is resolved). A SigMF.sigmf-datafile resolves its.sigmf-metasidecar the same way.sample_typethe 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-metasidecar 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.endianbyte 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:
pathfile to read astror anyos.PathLikefrom Python. For a DETACHED BLUE capture this is normally the HEADER file<base>.tmpor<base>.prmper BLUE 3.1.1.4 (this library's own writer emits<base>.hdr) whose HCBdetachedfield points at the collocated<base>.detpayload; the extension does not decide,detacheddoes. Passing the<base>.detdirectly also works (its header sibling is resolved). A SigMF.sigmf-datafile resolves its.sigmf-metasidecar the same way.sample_typethe 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-metasidecar 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.endianbyte 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.
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¶
function dp_wfm_reader_get_ending¶
function dp_wfm_reader_get_fc¶
function dp_wfm_reader_get_fc_source¶
Which keyword dp_wfm_reader_get_fc read the centre frequency from.
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¶
function dp_wfm_reader_get_follow_grace_ms¶
function dp_wfm_reader_get_follow_timeout_ms¶
function dp_wfm_reader_get_fs¶
function dp_wfm_reader_get_fs_source¶
Which metadata dp_wfm_reader_get_fs read the sample rate from.
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¶
function dp_wfm_reader_get_num_samples¶
function dp_wfm_reader_get_position¶
The read position, in samples from the first sample.
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¶
function dp_wfm_reader_get_t0¶
Capture start time in seconds since the UNIX epoch, or 0.0.
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.
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.
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/endianhint is wrong for a headerless file type (reading aci16file ascf32leaves 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.
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.
function dp_wfm_reader_info¶
Copy the resolved capture metadata into info .
function dp_wfm_reader_keyword¶
The i'th keyword in file order, or NULL ifi is out of range.
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).
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.
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.
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:
statethe reader.nhow many samples to read (countin the Python binding, which also accepts an optional pre-allocatedout=array to avoid an allocation per block in a streaming loop).outdestination, at leastmax_outsamples.max_outcapacity ofout; 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¶
function dp_wfm_reader_read_max_out¶
Maximum samples one read(n) yields: n (fewer at EOF).
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.
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 .
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:
statethe reader.indexsample to move to; 0 tonum_samplesinclusive.
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.
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:
statethe reader.secondsoffset from the first sample; 0 tonum_samples / fsinclusive.
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¶
function dp_wfm_reader_set_follow_timeout_ms¶
function dp_wfm_reader_set_stop_fn¶
Tell a following read how to learn that a stop was requested.
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.
The documentation for this class was generated from the following file native/inc/doppler/wfm_reader/wfm_reader_core.h