Skip to content

Python Capture I/O API — Reader / Writer

Reader and Writer are duals: one turns complex64 samples into a capture file, the other turns a capture file back into complex64 samples. Both are CPython extension types over the C cores, so all parsing, deinterleaving and rescaling happens in C — there is no Python fast path to fall off.

from doppler.wfm import Reader, Writer, write_blue_header
Symbol Direction Use when
Writer out Serialise samples to raw / CSV / BLUE type-1000 / SigMF
Reader in Open a capture — yours or someone else's — and stream unit-scale I/Q
write_blue_header out Write a detached BLUE header beside a payload written separately

Source: src/doppler/wfm/__init__.py

The narrative versions of both sides are Writing captures and Reading captures; for generating the samples in the first place see Python: Waveform Generator.


A round trip

import pathlib
import tempfile

from doppler.wfm import Composer, Reader, Segment, Writer

tmpdir = tempfile.TemporaryDirectory()
tmp = pathlib.Path(tmpdir.name)

x = Composer([Segment("qpsk", sps=8, snr=15, fs=2.4e6,
                      num_samples=8192)]).compose()

with Writer(tmp / "capture.blue", file_type="blue", sample_type="ci16",
            fs=2.4e6, fc=1.2e9) as w:
    w.write(x)

with Reader(tmp / "capture.blue") as r:
    assert (r.file_type, r.sample_type) == ("blue", "ci16")
    assert (r.fs, r.fc) == (2.4e6, 1.2e9)
    back = r.read(r.num_samples)

assert len(back) == len(x)

Both types are context managers, and both also expose close() (aliased destroy()) for callers that manage the lifetime themselves. Closing a Writer is what finalises the file — a BLUE header's sample count and a SigMF sidecar are both written at close, so a Writer that is never closed leaves an incomplete capture.


Writer

Writer(path, file_type="raw", sample_type="cf32", endian="le", fs=1e6, fc=0.0, total=0, headroom=0.0)

The file type decides how much metadata the result can carry. raw and csv are headerless — fs, fc and the sample type are written nowhere, so a reader has to be told them. blue and sigmf are self-describing.

# headerless: fs/fc are accepted but cannot be stored
with Writer(tmp / "capture.raw", fs=1e6, file_type="raw",
            sample_type="ci16") as w:
    w.write(x)

# self-describing: fs and fc survive the round trip
with Writer(tmp / "capture.blue", file_type="blue", sample_type="ci16",
            fs=2.4e6, fc=1.2e9) as w:
    w.write(x)

A sigmf writer needs a path ending in .sigmf-data — the two halves of a SigMF capture are found by name, so the name is part of the format. Anything else is refused with a message saying so, rather than emitting a pair no SigMF reader will locate:

with Writer(tmp / "cap.sigmf-data", file_type="sigmf", sample_type="ci16",
            fs=2e6, fc=1.2e9) as w:
    w.write(x)
assert (tmp / "cap.sigmf-meta").exists()      # sidecar written at close

Clipping

Integer sample types map ±1.0 to ±max-code, so content with PAPR above 0 dBFS clips. track_clipping() turns on the counters, and clip_fraction / peak_dbfs / clipped report what happened — measured on the way out, at no cost when tracking is off:

with Writer(tmp / "clip.raw", fs=1e6, file_type="raw", sample_type="ci8") as w:
    w.track_clipping()
    w.write(x * 4.0)                  # deliberately over full scale
    assert w.clipped and w.clip_fraction > 0.0

headroom (dB) attenuates on the way out instead, so a scene with known peaks lands under full scale without rescaling it yourself. See Levels & SNR.

BLUE keywords

add_keyword(tag, type, value) appends a keyword to the extended header. The type is the BLUE type code (A for ASCII, B/I/L integer, F/D floating point, and so on), and the value may be a scalar or a sequence:

with Writer(tmp / "kw.blue", file_type="blue", fs=1e6) as w:
    w.add_keyword("MISSION", "A", "doppler-demo")
    w.add_keyword("GAIN_DB", "D", -3.5)
    w.write(x[:64])

with Reader(tmp / "kw.blue") as r:
    assert r.keywords["MISSION"] == "doppler-demo"
    assert r.keywords["GAIN_DB"] == -3.5

Writer

Open a capture for writing.

Parameters:

Name Type Description Default
path str | PathLike

where to write -- a str or any os.PathLike from Python. For file_type="sigmf" this MUST end in .sigmf-data: a SigMF capture is a <base>.sigmf-data + <base>.sigmf-meta pair found by name, and close() writes the sidecar beside it.

required
fs float

sample rate (Hz), and REQUIRED -- there is no default. BLUE stores it as xdelta = 1/fs, SigMF and the raw/CSV sidecar as core:sample_rate. Pass 0.0 to say the rate is not known: that writes xdelta = 0 and omits core:sample_rate, where a defaulted value would have written a rate nobody supplied into a file that outlives the process.

required
file_type Literal['raw', 'csv', 'blue', 'sigmf']

"raw" (headerless interleaved I/Q), "csv" (one I,Q line per sample), "blue" (self-describing X-Midas/REDHAWK type-1000) or "sigmf". BLUE and SigMF record fs/fc/t0 in the capture itself; raw and CSV have nowhere to put them and keep them in the sidecar instead.

"raw"
sample_type Literal['cf32', 'cf64', 'ci32', 'ci16', 'ci8']

wire type: "cf32", "cf64", "ci32", "ci16" or "ci8". The integer types quantise ±1.0 to full scale and can clip -- see track_clipping()/peak_dbfs.

"cf32"
endian Literal['le', 'be']

"le" or "be"; ignored for CSV, which is text.

"le"
fc float

centre frequency (Hz). BLUE records it as a FREQ keyword, SigMF as captures[0]["core:frequency"], raw and CSV in the sidecar. 0.0 writes nothing, in every one of them -- absent is how this library says "not stated", which is what Reader.fc_source reports back.

0.0
total int

expected sample count, for the BLUE header; close() patches the real count, so 0 is fine when unknown.

0
headroom float

dB 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.

0.0
t0 float

capture start, seconds since the UNIX epoch. Optional where fs is 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 as captures[0]["core:datetime"], raw and CSV in the sidecar. 0.0 means unset and stays unset -- it is never written as 1970. Reader.t0 / Reader.t0_source read it back.

0.0
sidecar bool

write a <path>.sigmf-meta JSON beside a "raw" or "csv" capture, recording the fs, fc and t0 those 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.

True

Examples:

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

clip_fraction property

clip_fraction: float

Fraction (0..1) of I/Q components that saturated. Always 0.0 unless track_clipping() was enabled before writing -- the counter is the one extra per-sample compare, so it is opt-in. peak_dbfs is always tracked and is enough to tell you clipping happened; this tells you how much.

peak_dbfs property

peak_dbfs: float

Largest per-axis magnitude written so far, in dBFS (full scale = 0 dBFS, so a value above 0 means an integer capture clipped). Always tracked. It is also the remedy: back off by ceil(peak_dbfs) dB of headroom and the capture fits. Float wire types never clip but still report a peak. -inf before anything is written.

clipped property

clipped: bool

True if an integer capture saturated -- peak_dbfs > 0 and the wire type is one of ci32/ci16/ci8. Always False for cf32/cf64, which cannot clip: a float sample above full scale is merely loud.

write

write(x: NDArray[complex64]) -> int

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.

Parameters:

Name Type Description Default
x NDArray[complex64]

Input.

required

Returns:

Type Description
int

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.

Examples:

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

track_clipping

track_clipping(on: int = 1) -> None

Enable the per-component clip counter (off by default; peak is always on).

Parameters:

Name Type Description Default
on int

Input.

1

add_keyword

add_keyword(
    tag: str,
    type: str,
    value: str
    | int
    | float
    | Sequence[int]
    | Sequence[float],
) -> None

Attach a BLUE extended-header keyword (BLUE captures only). type is a single character (B/I/L/X int, F/D float, A string, T deprecated int); value is a str for A, a single int/float, or a sequence of them. Keywords are buffered and written at close(). The read side is Reader.keywords.

close

close() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

Raises:

Type Description
OSError

If the C destructor reports failure. Raised from an explicit call and from __exit__ alike, so a failing teardown propagates out of a with block (gh-541).

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

Raises:

Type Description
OSError

If the C destructor reports failure. Raised from an explicit call and from __exit__ alike, so a failing teardown propagates out of a with block (gh-541).

__enter__

__enter__() -> Writer

Enter a context manager, returning this object.

Lets a Writer be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
Writer

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the Writer.

Equivalent to calling close(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

Reader

Reader(path, sample_type="cf32", endian="le")

The constructor arguments are hints, used only for a headerless file type. The actual file type comes from the file's content — BLUE magic at byte 0, a .sigmf-data name with its required sidecar, a first line that scans as I,Q, otherwise raw. So a CSV named capture.dat reads as CSV, and a BLUE file named capture.csv reads as BLUE.

read(count) returns up to count samples as complex64 at unit scale whatever the wire type was, and an empty array at end of file — which makes the block loop a while:

with Reader(tmp / "capture.blue") as r:
    total = 0
    while len(block := r.read(4096)):
        total += len(block)
assert total == len(x)

Pass out= to read into a buffer you own instead of allocating per call.

Two properties that exist because the obvious answer is ambiguous

fc == 0.0 does not mean baseband — it also means "nothing in this file declared a centre frequency". fc_source is what separates them, reporting the tag that answered ("FREQ", "RF_FREQ", "CENTER_FREQ", "F_C", "core:frequency") or "none":

with Reader(tmp / "capture.blue") as r:
    assert r.fc_source == "FREQ"      # not a default — the file says 1.2 GHz
with Reader(tmp / "capture.raw", sample_type="ci16") as r:
    assert r.fc_source == "none"      # headerless: fc is a default, not a reading

trailing_bytes is the payload left over after the last whole sample. A wrong sample_type on a headerless capture cannot fail — nothing in the file can contradict it — so a non-zero remainder is the only signal that the hint is wrong or the capture is truncated:

with Writer(tmp / "short.raw", fs=1e6, file_type="raw",
            sample_type="ci8") as w:
    w.write(x[:5])                             # 5 samples x 2 bytes = 10 bytes

with Reader(tmp / "short.raw", sample_type="cf32") as r:
    assert r.trailing_bytes == 2               # 10 bytes = one cf32 + 2 over

It cannot catch a wrong hint that happens to divide evenly (a ci16 file read as ci8), and it never false-alarms.

BLUE introspection

header is the whole 512-byte header control block as a dict under the format's own field names; keywords merges both keyword blocks into one {tag: value} dict. Both are empty for a non-BLUE file type.

with Reader(tmp / "capture.blue") as r:
    assert r.header["type"] == 1000            # type-1000, the I/Q workhorse
    assert r.header["xdelta"] == 1.0 / 2.4e6   # xdelta is 1/fs

tmpdir.cleanup()

Reader

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

Parameters:

Name Type Description Default
path str | PathLike

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.

required
sample_type Literal['cf32', 'cf64', 'ci32', 'ci16', 'ci8']

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. "cf32", "cf64", "ci32", "ci16" or "ci8" from Python; the matching 0..4 from C. A wrong hint does not fail; see ::wfm_reader_get_trailing_bytes.

"cf32"
endian Literal['le', 'be']

byte order, likewise a hint that only headerless raw uses; "le" or "be" from Python, 0 or 1 from C.

"le"

Examples:

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

file_type property

file_type: Literal['raw', 'csv', 'blue', 'sigmf']

Which file type the capture turned out to be -- "raw", "csv", "blue" or "sigmf". Detected from the file's CONTENT, not its name, so a CSV called capture.dat reports "csv" and a BLUE file called capture.csv reports "blue". "raw" is also the fallback for a file nothing else recognised, so it means "headerless interleaved I/Q at the sample_type you passed" rather than a positive identification.

sample_type property

sample_type: Literal['cf32', 'cf64', 'ci32', 'ci16', 'ci8']

The wire sample type the samples are being decoded FROM -- "cf32", "cf64", "ci32", "ci16" or "ci8". For BLUE and SigMF this was read from the file's metadata and is authoritative; for raw and CSV it is simply the hint passed to the constructor, echoed back. read() returns complex64 at unit scale regardless.

mode property

mode: Literal['complex', 'scalar']

Components per wire sample: "complex" for interleaved I/Q, "scalar" for a real capture. Only BLUE carries this (its format field's mode designator, C or S); every other file type is complex. A scalar capture still reads back as complex64 -- the imaginary part is exactly 0, so a real signal lands on the real axis.

endian property

endian: Literal['le', 'be']

Byte order of the samples on the wire, "le" or "be". Read from the metadata for BLUE (the HCB's head_rep) and SigMF (the _be/_le datatype suffix); for raw it is the constructor hint echoed back. CSV is text and ignores it.

fs property

fs: float

Sample rate in Hz, or 0.0 when the file type does not carry one. BLUE derives it from the header's xdelta (fs = 1/xdelta); SigMF reads core:sample_rate. Raw and CSV have nowhere to record a rate, so they always report 0.0 -- whatever rate the capture was taken at has to travel with it by other means.

fc property

fc: float

Centre frequency in Hz, or 0.0 when nothing in the capture declares one. 0.0 is ambiguous on its own -- a genuine baseband capture and a capture whose frequency could not be found report the same number -- so read fc_source alongside it: "none" there is what distinguishes them. SigMF takes it from captures[0]["core:frequency"]; BLUE from a FREQ keyword (see fc_source for the tags tried), in either the ASCII HCB keyword area or the typed extended header. Raw and CSV carry no metadata at all.

fs_source property

fs_source: Literal['none', 'xdelta', 'core:sample_rate']

Which metadata fs was read from -- "xdelta" for BLUE (the type-1000 adjunct, as 1/xdelta), "core:sample_rate" for SigMF, or "none" when nothing carried a rate. Raw and CSV always report "none": they have nowhere to record one, so whatever rate the capture was taken at has to travel with it by other means.

t0 property

t0: float

Capture start time in seconds since the UNIX epoch, or 0.0 when the capture does not declare one. 0.0 does not mean 1970 -- read t0_source alongside it, exactly as with fc/fc_source. This is the t0 of t = t0 + n/fs: hand it to a SampleClock via track() and a replayed capture's timeline lands where the samples were taken, not where they are being replayed. BLUE carries it as a J1950 timecode in the header, converted here to the UNIX epoch.

t0_source property

t0_source: Literal['none', 'timecode']

Where t0 was read from -- "timecode" for a BLUE header that declares one, or "none". "none" is the common answer and the one that matters: a zero BLUE timecode means the field was never set, not 1950-01-01, and doppler's own writer leaves it zero -- so a caller that skips this check dates every doppler-written capture to 1950. SigMF's core:datetime is an ISO 8601 string this reader does not parse yet, so a SigMF capture also reports "none" rather than a guess.

num_samples property

num_samples: int

Total samples in the capture, or 0 when the file type cannot say. BLUE takes it from the header's data_size; raw and SigMF divide the file length by the sample stride. A CSV has to be counted, so the first read of this property scans the file once (the scan is exact -- it parses rows the same way read does -- and leaves the read position alone); every later read is free.

fc_source property

fc_source: Literal[
    "none",
    "FREQ",
    "RF_FREQ",
    "CENTER_FREQ",
    "F_C",
    "core:frequency",
]

Which piece of metadata fc was read from -- the keyword's own tag ("FREQ", "RF_FREQ", "CENTER_FREQ", "F_C"), "core:frequency" for SigMF, or "none" when nothing carried it. Check this before trusting fc == 0.0: "none" means not found, anything else means the capture really does say 0 Hz. BLUE type-1000 has no header field for centre frequency, so an RF capture conveys it as a keyword; FREQ in the HCB keyword area is the X-Midas convention and is tried first.

trailing_bytes property

trailing_bytes: int

Payload bytes left over after the last whole sample; 0 for a capture whose declared sample type and mode match its content, and always 0 for CSV. Non-zero means either the sample_type/endian hint is wrong for a headerless file type or the capture is truncated -- the reader cannot tell which, and stops at the last complete sample either way. This is the only signal available for a raw file: a wrong hint does not fail, it returns plausible garbage at the wrong stride.

keywords property

keywords: dict[
    str, str | int | float | list[int] | list[float]
]

The BLUE extended header as a {tag: value} dict, in file order; empty when the capture carries no extended header. Values follow the keyword type: a str for A, an int/float for a single-element numeric keyword, a list for a multi-element one. For a detached capture these come from the HEADER file.

header property

header: dict[
    str, str | int | float | list[int] | list[float]
]

The BLUE header control block as a {field: value} dict, under the names the format itself uses -- version, head_rep, data_rep, detached, protected, pipe, ext_start, ext_size, data_start, data_size, type, format, flagmask, timecode, inlet, outlets, outmask, pipeloc, pipesize, in_byte, out_byte, outbytes, keylength, and the type-1000 adjunct xstart, xdelta, xunits. Empty for a non-BLUE file type. Nothing is renamed or omitted, so what you see is what the file holds; the decoded keywords are in keywords.

reset

reset() -> None

Rewind to the first sample of the capture.

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.

read

read(
    count: int = 1, out: NDArray[complex64] | None = None
) -> NDArray[np.complex64]

Read up to count samples, returning them as complex64.

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.

Returns:

Type Description
NDArray[complex64]

Output.

Examples:

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

read_max_out

read_max_out(n: int) -> int

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.

Parameters:

Name Type Description Default
n int

Input.

required

Returns:

Type Description
int

Output.

close

close() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> Reader

Enter a context manager, returning this object.

Lets a Reader be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
Reader

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the Reader.

Equivalent to calling close(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

Module-level helpers

write_blue_header writes a standalone BLUE header, for a detached capture whose samples were written separately (a .det payload beside a .hdr header). Reader opens either half and resolves the other.

write_blue_header

write_blue_header(
    path: str | PathLike,
    fs: float,
    sample_type: str = "cf32",
    endian: str = "le",
    fc: float = 0.0,
    data_start: float = 0.0,
    total: int = 0,
    detached: int = 1,
    t0: float = 0.0,
) -> None

Write a standalone BLUE type-1000 HCB header (the detached .hdr): 512 bytes carrying the BLUE magic, byte order, data_size (total x bytes-per-sample), the type-1000 tag and xdelta = 1/fs. Pair it with a detached .det body of raw interleaved I/Q. Raises on a failed write.


Gallerytype="symbols" — Bring Your Own Constellation, Composing a Scene — .sum(), .add(), and Headroom, Waveform I/O — One Capture, Four File Types, Waveform Write — Compose, Write, Read Back, wfmgen — One Engine, Every Waveform GuidesCapture I/O, Reading captures, Writing captures — output & file types, Levels & SNR, Python API DesignTelemetry — zero-cost scalar taps for running pipelines ContributingRelease Checklist