File i16_buffer_core.h¶
FileList > doppler > i16_buffer > i16_buffer_core.h
Go to the source code of this file
The int16 I/Q pair ring as the component just-makeit binds. More...
#include "doppler/clib_common.h"#include "doppler/buffer/buffer.h"#include "doppler/dp_interrupt_guard/dp_interrupt_guard_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_iq16_t One q15 complex sample: the element the i16 ring's view hands back. |
Public Types¶
| Type | Name |
|---|---|
| typedef dp_i16_t | dp_i16_buffer_state_t The component's state IS the ring. |
Public Static Functions¶
| Type | Name |
|---|---|
| size_t | dp_i16_buffer_get_available (const dp_i16_buffer_state_t * state) Samples written but not yet consumed. |
| size_t | dp_i16_buffer_get_capacity (const dp_i16_buffer_state_t * state) Buffer capacity in IQ sample pairs. |
| bool | dp_i16_buffer_get_closed (const dp_i16_buffer_state_t * state) True once the producer has called :meth:close . |
| size_t | dp_i16_buffer_get_dropped (const dp_i16_buffer_state_t * state) Cumulative IQ sample pairs in REFUSED writes not pairs lost. |
| size_t | dp_i16_buffer_get_space (const dp_i16_buffer_state_t * state) Free room in samples: the largest :meth: write sure to fit. |
| void | dp_i16_close (dp_i16_t * state) Say that no more data is coming. |
| int | dp_i16_consume (dp_i16_t * state, size_t n) Release n samples back to the producer. |
| dp_i16_t * | dp_i16_create (size_t capacity) Lock-free SPSC ring buffer for interleaved int16 IQ pairs. |
| void | dp_i16_destroy (dp_i16_t * state) Unmap the buffer and free the underlying struct. |
| dp_iq16_t * | dp_i16_peek_view (dp_i16_t * state, size_t n) :meth: wait that never blocks: a view, or None for not yet. |
| void | dp_i16_reset (dp_i16_t * state) Empty the ring and reopen it. |
| dp_iq16_t * | dp_i16_wait_view (dp_i16_t * state, size_t n) Block until n samples are available, then lend a zero-copy view. |
| size_t | dp_i16_write_some_view (dp_i16_t * state, const dp_iq16_t * x, size_t x_len) Write as much of x as fits and say how much that was. |
| bool | dp_i16_write_view (dp_i16_t * state, const dp_iq16_t * x, size_t x_len) Write IQ samples into the buffer without blocking. |
Detailed Description¶
The ring itself is dp_i16_* in buffer/buffer.h, header-only and macro-stamped. This header is what makes it a jm component without changing it:
dp_i16_buffer_state_tISdp_i16_t, so the binding holds the real ring and calls the real functions nothing is wrapped.- DECLARE_DP_BUFFER_VIEW stamps the element-typed face (one element per SAMPLE), which is the face a numpy array has.
- The Doxygen below sits on DECLARATIONS. The macros supply the definitions, but a doc extractor reads text, not the preprocessor's output, so the per-width documentation and the Python examples the stub and
help()both render has to be written where it can be seen. The<obj>_get_<prop>accessors are the one thing defined here: they are jm's naming, not the ring's.
The two siblings (f32 / f64 / i16) are the same file over a different element; a manifest template (just-makeit#1310) will say so once.
Public Types Documentation¶
typedef dp_i16_buffer_state_t¶
The component's state IS the ring.
Public Static Functions Documentation¶
function dp_i16_buffer_get_available¶
Samples written but not yet consumed.
The largest n for which :meth:wait is guaranteed to return without spinning. Read this rather than tracking the count yourself: :meth:wait has no timeout and no short return, so asking for more than has been written spins until the producer catches up forever, if there is no producer.
Read from the consumer side this is a lower bound. A producer on another thread can only increase it, so a block sized from it is always safe; it may simply be smaller than what has landed by the time :meth:wait runs.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.available
0
>>> _ = buf.write(np.zeros(100, dtype=IQ16))
>>> buf.available
100
>>> _ = buf.wait(64); buf.consume(64)
>>> buf.available
36
function dp_i16_buffer_get_capacity¶
Buffer capacity in IQ sample pairs.
Read-only. Exactly the number passed to the constructor, whatever the machine's page size; the mapping behind it is larger when that number is not a power of two or spans less than a page, and that slack is never room.
>>> from doppler.buffer import I16Buffer
>>> I16Buffer(1024).capacity, I16Buffer(1000).capacity
(1024, 1000)
function dp_i16_buffer_get_closed¶
True once the producer has called :meth:close .
The consumer's half of end of stream: it distinguishes "the producer is slow" from "the producer has finished", which an empty ring alone cannot.
>>> from doppler.buffer import I16Buffer
>>> buf = I16Buffer(1024)
>>> buf.closed
False
>>> buf.close()
>>> buf.closed
True
function dp_i16_buffer_get_dropped¶
Cumulative IQ sample pairs in REFUSED writes not pairs lost.
Not a count of lost data. :meth:write is all-or-nothing: with no room it copies nothing, leaves the caller's array untouched and refuses the call and this counter is then incremented by the length of that refused call, not by 1 and not by anything actually lost.
So a producer that spins on :meth:write until it succeeds, the obvious way to apply backpressure, inflates this while losing nothing: a 60,000-pair run written that way reported 5,960,438. Samples are lost only when the caller discards them, which is what ignoring the return value does. Wait for room if you want this to mean what it sounds like.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.dropped
0
>>> buf.write(np.zeros(1024, dtype=IQ16))
True
>>> buf.write(np.zeros(3, dtype=IQ16))
False
>>> buf.dropped
1
function dp_i16_buffer_get_space¶
Free room in samples: the largest :meth: write sure to fit.
capacity - available, read in one place so callers stop deriving it. Read from the producer side it is a lower bound: a consumer on another thread can only increase it, so a block sized from it is always accepted.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.space == buf.capacity
True
>>> buf.write_some(np.ones(8, dtype=IQ16))
8
>>> buf.capacity - buf.space
8
function dp_i16_close¶
Say that no more data is coming.
The producer's half of end of stream. Until this exists a consumer cannot tell a slow producer from a finished one both look like an empty ring so :meth:wait had nothing to do but spin. Call it once, after the last write.
Release ordering: every sample written before this is visible to a consumer that observes the flag. Closing does not discard what was already written; :meth:wait keeps returning batches until the ring is drained, and only then raises EOFError.
See docs/design/io-termination.md for the one termination contract shared with the network and disk transports.
>>> import numpy as np
>>> from doppler.buffer import I16Buffer
>>> buf = I16Buffer(1024)
>>> buf.close()
>>> buf.closed
True
>>> buf.wait(4)
Traceback (most recent call last):
...
EOFError: end of stream: the producer closed the ring
function dp_i16_consume¶
Release n samples back to the producer.
Advances the consumer tail pointer by n, making that space available for the producer to overwrite, and ends the loan: the view a :meth:wait or :meth:peek lent must not be used afterwards. If n is omitted it is the count of that outstanding view, so the number is written once. n smaller than the view is how overlapped frames are read: release a hop, keep the rest.
Parameters:
stateThe ring. Must be non-NULL.nNumber of samples to release. Defaults to the count of the outstanding :meth:wait/ :meth:peekview.
Returns:
DP_OK, or DP_ERR_INVALID when n exceeds what is readable.
Exception:
ValueErrornexceeds :attr:available. Nothing is released: past that point the ring's counts would stop describing it.RuntimeErrornwas omitted and nothing is outstanding no view was lent since the last release, so there is no count to default to.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.write(np.array([1, 2, 3, 4], dtype=np.int16).view(IQ16))
True
>>> _ = buf.wait(2)
>>> buf.consume()
function dp_i16_create¶
Lock-free SPSC ring buffer for interleaved int16 IQ pairs.
Stores raw 16-bit integer I/Q samples as they arrive from SDR hardware (e.g. RTL-SDR, HackRF) before conversion to floating point. Uses the same virtual-memory double-mapping as :class:F32Buffer to give zero-copy, branchless access across the wrap boundary.
numpy has no complex-integer dtype, so one sample is a RECORD: [("i", "<i2"), ("q", "<i2")]. Both faces speak it :meth:write takes a 1-D array of it and :meth:wait lends one so the ring is 1-D with one element per sample, exactly like its float siblings. The storage underneath is still interleaved int16 (I, Q, I, Q, ...), so flat.view(IQ16) and view.view(np.int16) convert either way with no copy.
A record rather than a packed int32 on purpose: both are one element per sample, but packed + 1 carries across the I/Q boundary and increments I only, silently. A record refuses arithmetic instead.
Parameters:
capacityHow many samples the ring holds: any size from 1 up, and :attr:capacityis exactly this number on every machine. What is rounded is the MAPPING behind it up to a power of two, because indexing is a mask, and up to a whole page so a capacity that is not a power of two costs some address space (under 2x) and nothing per call.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> buf = I16Buffer(1024)
>>> buf.capacity
1024
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> adc = np.array([10, 20, 30, 40], dtype=np.int16) # I, Q, I, Q
>>> buf.write(adc.view(IQ16))
True
>>> buf.wait(2)["q"].tolist()
[20, 40]
function dp_i16_destroy¶
Unmap the buffer and free the underlying struct.
Releases both virtual-address views and frees the C struct. Safe to call more than once; subsequent calls are no-ops.
function dp_i16_peek_view¶
:meth: wait that never blocks: a view, or None for not yet.
The single-threaded consumer's read. :meth:wait spins until a producer on another thread delivers, so a caller that is its own producer would deadlock in it; peek answers at once instead. When n samples are buffered it returns the same zero-copy, always-contiguous view :meth:wait would (1-D, one (i, q) record per sample); otherwise it returns None.
None means not yet and nothing else. The two conditions no amount of waiting can cure are raised, exactly as :meth:wait raises them, so a poll loop cannot mistake either for a slow producer.
Peeking does not consume. Follow it with :meth:consume; a consume(k) with k < n advances by a hop smaller than the frame, which is how overlapped frames are read.
Parameters:
stateThe ring. Must be non-NULL.nNumber of samples wanted. Must be positive and not larger than :attr:capacity.
Returns:
Zero-copy view of the next n samples, or None when fewer than n have been written so far.
Exception:
EOFErrorThe ring is closed and fewer thannsamples remain: the rest is never coming.ValueErrornexceeds :attr:capacity(or is not positive), so no producer could ever satisfy it.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.peek(4) is None
True
>>> buf.write_some(np.ones(8, dtype=IQ16))
8
>>> buf.peek(4).shape
(4,)
>>> buf.consume(2)
>>> buf.available
6
>>> buf.close()
>>> buf.peek(8)
Traceback (most recent call last):
...
EOFError: end of stream: the producer closed the ring
function dp_i16_reset¶
Empty the ring and reopen it.
Discards everything buffered, and clears :attr:closed so the same ring can carry a second stream without it, reuse after :meth:close means destroying and re-mapping. :attr:dropped is a lifetime count and is kept.
Not safe against a concurrent producer or consumer: it moves both ends of the ring. Call it only when both sides are idle.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> buf.write_some(np.ones(8, dtype=IQ16))
8
>>> buf.close()
>>> buf.reset()
>>> buf.available, buf.closed
(0, False)
function dp_i16_wait_view¶
Block until n samples are available, then lend a zero-copy view.
Spins with the GIL released until the producer has written at least n samples. Returns a 1-D record array directly into the double-mapped ring: view["i"] is the I channel, view["q"] the Q channel, each a strided int16 view with no copy. Caller must call :meth:consume before the next wait.
Parameters:
stateThe ring. Must be non-NULL.nNumber of IQ sample pairs to wait for.
Returns:
Zero-copy view of the next n samples, one record each.
Exception:
EOFErrorThe producer called :meth:closeand fewer thannsamples remain. The tail is drained and no more is coming, so the wait ends rather than blocking forever.KeyboardInterruptSomebody asked this process to stop, through a :class:doppler.interrupt.Interruptguard from any module: the flag is process-wide. Without a guard the spin checks for no signals at all.
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> buf = I16Buffer(1024)
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf.write(np.array([10, 20, 30, 40], dtype=np.int16).view(IQ16))
True
>>> view = buf.wait(2)
>>> view.dtype
dtype([('i', '<i2'), ('q', '<i2')])
>>> view.shape
(2,)
>>> view.tolist()
[(10, 20), (30, 40)]
>>> buf.consume()
function dp_i16_write_some_view¶
Write as much of x as fits and say how much that was.
The partial-write twin of :meth:write. Where :meth:write refuses a block that does not fit whole, this takes the leading samples that do and returns their count 0 when the ring is full. It never refuses, so it never touches :attr:dropped. It is the only way to feed a chunk larger than the ring: loop, advancing by the return value, draining in between.
Parameters:
stateThe ring. Must be non-NULL.xSamples to write: 1-D, C-contiguous, dtype[("i", "<i2"), ("q", "<i2")].x_lenLength ofx, in samples.
Returns:
Samples accepted, 0 <= k <= len(x). The caller still owns x[k:].
A chunk three times the size of the ring, fed by looping:
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf = I16Buffer(1024)
>>> cap = buf.capacity
>>> chunk = np.ones(3 * cap, dtype=IQ16)
>>> fed = 0
>>> while fed < len(chunk):
... fed += buf.write_some(chunk[fed:])
... _ = buf.peek(buf.available); buf.consume()
>>> fed == 3 * cap, buf.dropped
(True, 0)
function dp_i16_write_view¶
Write IQ samples into the buffer without blocking.
Copies the record array into the ring in a single memcpy. With fewer than len(x) free slots the call is refused entirely nothing copied, x untouched and :attr:dropped grows by len(x), which is not a loss count: you still hold every sample.
A bare int16 array is refused with TypeError, flat or (n, 2): it is not an array of samples. flat.view(IQ16) makes it one, with no copy.
Parameters:
stateThe ring. Must be non-NULL.xIQ samples to write: 1-D, C-contiguous, dtype[("i", "<i2"), ("q", "<i2")].x_lenLength ofx, in samples.
Returns:
True if all samples were written; False if the ring had no room and the call was refused (x untouched).
>>> from doppler.buffer import I16Buffer
>>> import numpy as np
>>> buf = I16Buffer(1024)
>>> IQ16 = np.dtype([("i", "<i2"), ("q", "<i2")])
>>> buf.write(np.array([10, 20, 30, 40], dtype=np.int16).view(IQ16))
True
>>> buf2 = I16Buffer(1024)
>>> buf2.write(np.zeros(1024, dtype=IQ16))
True
>>> buf2.write(np.zeros(1, dtype=IQ16))
False
The documentation for this class was generated from the following file native/inc/doppler/i16_buffer/i16_buffer_core.h