File resamp_core.h¶
FileList > inc > resamp > resamp_core.h
Go to the source code of this file
Continuously-variable polyphase resampler for CF32 IQ. More...
#include "clib_common.h"#include "dp_state.h"
Classes¶
| Type | Name |
|---|---|
| struct | resamp_state_t |
Public Functions¶
| Type | Name |
|---|---|
| resamp_state_t * | resamp_create (double rate) |
| resamp_state_t * | resamp_create_custom (size_t num_phases, size_t num_taps, const float * bank, double rate) |
| double | resamp_dc_gain (const resamp_state_t * state) The resampler's response to a constant input, from its own bank. |
| void | resamp_destroy (resamp_state_t * state) |
| size_t | resamp_execute (resamp_state_t * state, const float _Complex * in, size_t num_in, float _Complex * out, size_t max_out) Resample a block of CF32 samples (fixed rate). |
| size_t | resamp_execute_ctrl (resamp_state_t * state, const float _Complex * in, const double * ctrl, size_t num_in, float _Complex * out, size_t max_out) Resample with per-sample additive rate deviation. |
| size_t | resamp_execute_ctrl_push (resamp_state_t * state, float _Complex x, double ctrl, float _Complex * out, size_t max_out) Push one input at an instantaneous rate deviation; emit any outputs. |
| double | resamp_get_ctrl_acc (const resamp_state_t * state) The control accumulator's fractional phase, in [0, 1). |
| double | resamp_get_delay (const resamp_state_t * state) Group delay of the interpolator, in INPUT samples. |
| size_t | resamp_get_num_phases (const resamp_state_t * state) |
| size_t | resamp_get_num_taps (const resamp_state_t * state) |
| double | resamp_get_rate (const resamp_state_t * state) |
| void | resamp_get_state (const resamp_state_t * state, void * blob) Serialize state's mutable state intoblob . |
| size_t | resamp_interp_fill (resamp_state_t * state, const float _Complex * in, float _Complex * out, size_t max_out) Emit exactly max_out interpolated outputs, pulling inputs on overflow. |
| size_t | resamp_interp_inputs_needed (const resamp_state_t * state, size_t max_out) Input samples an interpolating fill of max_out outputs consumes. |
| void | resamp_reset (resamp_state_t * state) |
| void | resamp_set_rate (resamp_state_t * state, double rate) |
| int | resamp_set_state (resamp_state_t * state, const void * blob) Restore mutable state from blob (same rate). |
| size_t | resamp_state_bytes (const resamp_state_t * state) Bytes resamp_get_state() writes for state (envelope + payload). |
Macros¶
| Type | Name |
|---|---|
| define | RESAMP_CTRL_RATE_MIN 1e-6 |
| define | RESAMP_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('R', 'S', 'M', 'P') |
| define | RESAMP_STATE_VERSION 2u |
Detailed Description¶
Two execute paths:
resamp_execute — dual-mode: * Interpolation (rate >= 1): output-driven, one NCO tick per output sample, overflow pushes the next input into the delay line. * Decimation (rate < 1): input-driven transposed-form polyphase. Each input is multiplied by the current polyphase arm and accumulated into N integrate-and-dump registers; on NCO overflow the I&D dump through a transposed tapped delay line to produce one output. Bank coefficients are pre-scaled by rate so the passband gain is unity.
resamp_execute_ctrl — unified, and it rides the INTERPOLATOR at every rate: emit at every tick, and load an input when the accumulator fails to advance (u(k) <= u(k-1)). The steered rate rate + ctrl(i) sets the step; whole input intervals per output are owed as ctrl_debt and the fractional remainder selects the arm.
This paragraph used to describe the other structure — "each input
advances the accumulator by (rate + ctrl(i)); every time the
accumulator crosses 1.0 an output is emitted" — which is the DECIMATOR's recurrence, exact only at rate 1 where the two coincide. Running it here cost 55-60 dB of tone purity at every other rate. The description is kept accurate rather than deleted because it is the mental model under which someone writes a private (uint32_t) (frac * 2^32 + 0.5) and believes it correct; one did, and it stalled the interpolator for composite rates just above unity.
Phase accumulator (execute): upper log2(num_phases) bits of the 32-bit NCO word index the polyphase bank — nearest-neighbor, no interpolation between branches.
Default constructor builds a 4096-phase × 19-tap Kaiser bank (60 dB rejection, 0.4/0.6 pass/stop) at first call. Use resamp_create_custom() to supply your own bank.
Lifecycle:
resamp_state_t *r = resamp_create(0.5);
float _Complex out[64];
size_t n = resamp_execute(r, in, 128, out, 64);
resamp_destroy(r);
Public Functions Documentation¶
function resamp_create¶
Built-in 4096×19 Kaiser bank (60 dB, 0.4/0.6 pass/stop).
function resamp_create_custom¶
resamp_state_t * resamp_create_custom (
size_t num_phases,
size_t num_taps,
const float * bank,
double rate
)
User-supplied bank, shape num_phases × num_taps, row-major. num_phases must be a power of two.
function resamp_dc_gain¶
The resampler's response to a constant input, from its own bank.
Every arm of a polyphase bank is the same filter at a different fractional delay, so they share one DC gain and arm 0 answers for all of them: the sum of its taps. Computed, not measured — a caller (or a gate) can ask what gain this stage contributes without running a signal through it.
The decimating path pre-scales by rate and integrates over the whole bank between outputs, which cancels: rate inputs' worth of taps per output, so the tap sum is the answer on both paths.
ARM 0's gain, which is the realised gain only where arm 0 is the arm being used — at rate 1, where the fraction is zero and one arm is selected forever. A non-unity rate visits every arm, so what a caller measures is the arm AVERAGE: 1.000586 computed against 1.000293 at rate 0.5 and 2, and 1.000249 at 0.25 and 4. The 3.4e-4 spread is the bank's arm-to-arm ripple and of no practical consequence, but this returns the computed number, not the one a measurement will find. Pinned by test_resamp_core.c §13, which checks both.
Parameters:
stateState. Must be non-NULL.
Returns:
The DC gain. 1.0 for the default Kaiser bank; a matched pulse bank is a matched filter, not a flat one, so its DC gain is the pulse's own sum(h)/sum(h^2) and is not expected to be 1.
resamp_state_t *r = resamp_create (0.5);
printf ("%.3f\n", resamp_dc_gain (r)); // 1.000
resamp_destroy (r);
function resamp_destroy¶
Free all resources. NULL is a no-op.
function resamp_execute¶
Resample a block of CF32 samples (fixed rate).
size_t resamp_execute (
resamp_state_t * state,
const float _Complex * in,
size_t num_in,
float _Complex * out,
size_t max_out
)
Parameters:
stateMust be non-NULL.inInput samples.num_inNumber of input samples.outOutput buffer.max_outCapacity of out in samples.
Returns:
Number of output samples written.
function resamp_execute_ctrl¶
Resample with per-sample additive rate deviation.
size_t resamp_execute_ctrl (
resamp_state_t * state,
const float _Complex * in,
const double * ctrl,
size_t num_in,
float _Complex * out,
size_t max_out
)
rate_i = base_rate + ctrl[i]. The control is real-valued and double-precision, matching resamp_execute_ctrl_push()'s scalar ctrl and the double the base rate itself is configured in.
Output buffer: allocate ceil(num_in × (rate + max_ctrl)) samples.
Parameters:
stateMust be non-NULL.inInput CF32 samples (length num_in).ctrlRate deviations, parallel to in (length num_in).num_inNumber of input samples (= length of ctrl).outOutput buffer.max_outCapacity of out in samples.
Returns:
Number of output samples written.
function resamp_execute_ctrl_push¶
Push one input at an instantaneous rate deviation; emit any outputs.
size_t resamp_execute_ctrl_push (
resamp_state_t * state,
float _Complex x,
double ctrl,
float _Complex * out,
size_t max_out
)
The single-input streaming form of resamp_execute_ctrl(): OFFERS x to the delay line, advances the accumulator by rate + ctrl, and emits every output whose period completes (0 for a decimator between strobes, 1 typically, or several for an interpolator) at the polyphase arm the fractional remainder selects.
The scalar and block forms are INDISTINGUISHABLE. Feeding a stream one sample at a time through here yields the same outputs, in the same number, bit-for-bit, as one resamp_execute_ctrl() over the same (in, ctrl[]). Not "close" and not "one sample of delay apart": the same. A caller chooses between them for control flow — the block form when ctrl[] is known in advance, this one when each correction depends on the outputs already emitted — never for a difference in what comes out.
That is what "offers" buys, and why x is NOT pushed on entry: nothing enters an interpolator's delay line without a load REQUEST — a tick emits, the accumulator fails to advance, and only then is an input consumed. x is held until a tick asks. Pushing on entry is an unrequested load, and it is precisely what broke the invariant, costing exactly one sample of group delay against the block form. Fixed, and gated: eq_ctrl_push in test_resamp_core.c asserts bit-exact equality across decimating, unity-neighbourhood and interpolating rates, at zero and at both signs of steer.
ctrl_ahead covers the one case the API cannot decline — max_out ending the call before any tick could ask for the offered sample. Feeding a stream of (x, ctrl) through this one input at a time reproduces resamp_execute_ctrl() on the same (in, ctrl[]) bit-for-bit — but, unlike the block form's precomputed ctrl[], ctrl here can depend on the outputs already emitted. That closes the loop: a timing-recovery or rate-tracking loop reads each emitted output, computes its correction, and feeds it back as the next call's ctrl to steer the strobe. This is the per-output feedback a matched-filter timing loop (track.RateSync) needs and the block execute_ctrl cannot provide.
Parameters:
stateMust be non-NULL.xOne input sample.ctrlRate deviation added to the base rate for this input (real-valued; the effective rate israte + ctrl).outOutput buffer for any emitted samples.max_outCapacity ofout(emission stops at this bound).
Returns:
Number of outputs emitted into out (0, 1, or more).
function resamp_get_ctrl_acc¶
The control accumulator's fractional phase, in [0, 1).
This is the timing NCO's state, and observing it is the only way to see what a closed timing loop is actually doing to the sampling instant. mu IS the fractional delay applied to the stream, and floor(mu * num_phases) is the polyphase arm the next output will read — the accumulator advances after the emit, so on return it already describes the output still to come. That holds at EVERY rate, because the control port rides the interpolating structure at every rate (see resamp_execute_ctrl); it is not a peculiarity of a decimating stage.
A steady mu means the loop has settled on a sampling phase. A mu that slews and wraps means a residual RATE error the loop has not absorbed, and one cycle of wrap is one INPUT interval of slip — an output period only at rate 1, where the two coincide.
Reports the CONTROL accumulator, so it stays 0.0 for a caller driving this object through resamp_execute(): the free-running phase is a separate accumulator with no accessor.
Pinned by test_resamp_core.c §10 (the arm, read off the output at four rates), §11 (the wrap's unit, against the counting law) and §12.
function resamp_get_delay¶
Group delay of the interpolator, in INPUT samples.
The prototype is a symmetric (linear-phase) Kaiser lowpass, so its delay is its centre: floor((num_phases * num_taps) | 1) / 2 prototype taps, i.e. that over num_phases input samples 9.5 for the built-in bank plus ONE input sample of pipeline: an output is formed from the delay line as it stands, and only then is the next input loaded, so the newest sample under the taps is the one before the sample being offered. 10.5 for the built-in bank, on every entry point (the free, ctrl and push forms agree; test_resamp_core.c §3 measures it by phase differencing on each and holds it to this value).
What it is for: output k carries the input at index k * (1/rate) + (the accumulated ctrl) - delay. A caller comparing the output timeline with the input's a code loop fed through doppler_channel, a test asserting where a chip lands subtracts this, and a loop started at the input's phase without it is this far from the peak (doppler-dsp/doppler#1189: 5 chips at 2 samples per chip, onto a Gold sidelobe). Closed-form from the bank's geometry, so it is exact for the built-in bank; a resamp_create_custom() bank is assumed symmetric about the same centre.
Parameters:
stateMust be non-NULL.
Returns:
The delay in input samples (>= 1).
function resamp_get_num_phases¶
function resamp_get_num_taps¶
function resamp_get_rate¶
function resamp_get_state¶
Serialize state's mutable state intoblob .
function resamp_interp_fill¶
Emit exactly max_out interpolated outputs, pulling inputs on overflow.
size_t resamp_interp_fill (
resamp_state_t * state,
const float _Complex * in,
float _Complex * out,
size_t max_out
)
The output-count-driven twin of the interpolation branch of resamp_execute(): it emits one output per phase tick and pushes the next input on each NCO overflow, but — unlike resamp_execute(), whose loop halts as soon as the input is exhausted even with output capacity left — it always writes max_out outputs. The caller must therefore supply at least resamp_interp_inputs_needed(state, max_out) inputs in in; supplying exactly that many (the common case) consumes them all. This is what lets a streaming producer (e.g. a pulse-shaping synth) feed symbols on demand and get a bit-exact match between a single call for max_out outputs and max_out single-output calls (the resampler is block-boundary invariant).
Parameters:
stateMust be non-NULL, upsampling.inInputs to push on overflow (>= inputs_needed available).outOutput buffer, capacity >=max_out.max_outNumber of outputs to emit.
Returns:
Inputs consumed (== resamp_interp_inputs_needed(state, max_out)).
function resamp_interp_inputs_needed¶
Input samples an interpolating fill of max_out outputs consumes.
The exact number of delay-line pushes producing max_out outputs from the current phase: ((uint64_t)phase + max_out * phase_inc) >> 32.
Exact at EVERY rate, not only at an integer interpolation factor, and a caller may rely on it: generate precisely this many inputs, hand them to resamp_interp_fill(), and there is no over- or under-production. The guarantee is structural rather than numeric — this closed form and the fill loop advance the same accumulator by the same phase_inc, so they cannot disagree whatever the rate, and mid-stream phase is carried in the formula. Measured across 1800 mid-stream calls at nine rates with randomised max_out, worst deviation zero (test_resamp_core.c §20).
(This paragraph used to scope the promise to "an integer interpolation factor". That is where phase_inc divides evenly and therefore represents the rate exactly — a real property, but a different one: the prediction matches the fill because both USE phase_inc, not because it is exact.)
Meaningful only for an upsampling resampler (rate >= 1) — the prediction still matches what resamp_interp_fill() consumes below unity, but that entry point interpolates, so a decimating caller wants resamp_execute().
Parameters:
stateMust be non-NULL, upsampling.max_outNumber of outputs the following resamp_interp_fill() call will request.
Returns:
Inputs that call will consume.
function resamp_reset¶
Zero phase accumulator, ctrl accumulator, and delay line. Rate and bank are preserved.
function resamp_set_rate¶
Update rate and recompute phase_inc. Accumulator phase and delay line are preserved. Switching between interp and decim modes requires a new create() + destroy() pair.
function resamp_set_state¶
Restore mutable state from blob (same rate).
Returns:
DP_OK, or DP_ERR_INVALID if the blob's envelope rejects.
function resamp_state_bytes¶
Bytes resamp_get_state() writes forstate (envelope + payload).
Macro Definition Documentation¶
define RESAMP_CTRL_RATE_MIN¶
define RESAMP_STATE_MAGIC¶
define RESAMP_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/resamp/resamp_core.h