Skip to content

File agc_core.h

FileList > agc > agc_core.h

Go to the source code of this file

Log-domain automatic gain control (AGC). More...

  • #include "clib_common.h"
  • #include "jm_perf.h"
  • #include "dp_state.h"
  • #include "dp_tlm/dp_tlm_core.h"
  • #include "util/util_core.h"
  • #include <math.h>

Classes

Type Name
struct agc_state_t
AGC state.
struct agc_tlm_t
Telemetry attachment: a borrowed context + this object's probe ids. NULL ctx (the default) means detached — every probe site is then a single predicted-not-taken branch. Zeroed in state blobs and preserved across set_state (DP_DEFINE_POD_STATE_TLM); telemetry is observation, not DSP state that migrates.

Public Functions

Type Name
agc_state_t * agc_create (double ref_db, double loop_bw, double alpha)
Construct a log-domain feedback AGC and return its heap state. The loop integrator starts at 0 dB (unity gain) and the power detector p_avg is pre-seeded to10^ (ref_db/10) linear, so the first block of on-target samples produces no transient. Three parameters tune the closed-loop behaviour:ref_db sets the target,loop_bw sets the convergence speed, andalpha sets the detector smoothing.
void agc_destroy (agc_state_t * state)
Destroy an AGC instance and release all memory. Frees the heap-allocated agc_state_t . Safe to call withNULL . After this call the pointer is invalid; set it toNULL . The Python binding calls this automatically when the object is garbage- collected or when used as a context manager (with AGC() as agc:).
JM_FORCEINLINE double agc_exp10_ (double v)
Fast 10^v approximation (~1e-3 relative).
double agc_get_applied_gain_db (const agc_state_t * state)
Return the gain (in dB) actually applied to the most recent sample. Computes 20*log10 (g_last), whereg_last is the linear multiplier that was used on the most recently processed sample. This differs fromgain_db (the loop integrator's current command) because the loop filter advances the command one step ahead after each sample: immediately afteragc_step() __gain_db already reflects the updated command whileapplied_gain_db still reflects what the signal actually saw. At loop convergence the two values are numerically equal. At create/reset both are 0.0 dB (unity).
void agc_get_state (const agc_state_t * state, void * blob)
JM_FORCEINLINE double agc_log10_ (double p)
Fast log10(p) approximation for p > 0 (~1e-3 absolute).
JM_FORCEINLINE double agc_power_ (float _Complex y)
Power |y|^2 in the detector's working precision (double).
void agc_reset (agc_state_t * state)
Reset the AGC loop state to its post-create condition. Sets gain_db back to 0 dB (unity), clearsg_last , and re-seeds the power-detector EMAp_avg from the currentref_db so that the first post-reset block produces no transient. All configuration fields (ref_db ,loop_bw ,alpha ,decim ,clip_db ) are left untouched. Use this to process a new, independent signal segment without re-allocating.
int agc_set_state (agc_state_t * state, const void * blob)
int agc_set_telemetry (agc_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context and register the AGC's probes on it. Registers two probes, both recorded once per gain-update event and further thinned by decim:
size_t agc_settling_samples (double loop_bw, double alpha, double gain_err_db, double tol_db)
How many samples this loop needs to settle — the design query.
size_t agc_state_bytes (const agc_state_t * state)
JM_FORCEINLINE JM_HOT float _Complex agc_step (agc_state_t * state, float _Complex x)
Process one complex sample through the per-sample AGC loop. Applies the current gain, measures the output power via the EMA detector, advances the loop-filter integrator, then square-clips the returned sample to clip_db . The clip is applied after the detector update, so clipping never disturbs convergence. With the defaultgain_update_period == 1 this is the exact per-sample reference path; withgain_update_period P > 1 the detector and gain-apply still run every sample but the loop-filter command (and the exp10/log10 it needs) refreshes once per P samples — a zero-order hold on the gain that amortises the transcendentals on a sample-rate hot loop, the streaming analogue ofagc_steps() ' decimation.agc_steps() is the faster block equivalent; neither is bit-identical to the P == 1 loop once decimated, but both converge to the same steady state.
void agc_steps (agc_state_t * state, const float _Complex * input, float _Complex * output, size_t n)
Process a block of complex samples through the decimated AGC loop. Splits the input into chunks of decim samples. Within each chunk the gain is linearly interpolated from the previous chunk's end value to the new loop-filter output (a first-order hold) so there is no inter-chunk gain staircase. The detector and loop filter run once per chunk on the chunk's mean power — O(n/decim) control-loop work versus O(n) foragc_step() . The output array may alias the input (in-place).
size_t settling_samples (double loop_bw, double alpha, double gain_err_db, double tol_db)

Macros

Type Name
define AGC_CLIP_DB_DEFAULT 120.0
Default output clip level ( agc_state_t::clip_db ), in dB.
define AGC_DECIM_DEFAULT 8
Default envelope decimation factor ( agc_state_t::decim ).
define AGC_POWER_CEIL 2.3158417847463238e77
Power ceiling for the detector, in linear units.
define AGC_POWER_FLOOR 1e-30
Power floor for the detector, in linear units.
define AGC_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('A', 'G', 'C', ' ')
define AGC_STATE_VERSION 3u /\* v3: telemetry attachment (zeroed in blob) \*/

Detailed Description

A feedback AGC that drives the average power of its output toward a fixed reference level. Three stages run per sample:

  • Gain y = x * 10^(gain_db / 20)
  • Detector p_avg += alpha * (|y|^2 - p_avg)
  • Loop filter gain_db += (4*loop_bw) * (ref_db - 10*log10(p_avg))

** **

The loop filter is a single integrator whose step size is 4*loop_bw. loop_bw is the loop's normalised noise-equivalent bandwidth in cycles/sample: a 1st-order loop with integrator step mu has a noise bandwidth of mu/4, so the knob is expressed as a bandwidth rather than a bare loop gain. Smaller loop_bw is slower and smoother.

** **

The control variable gain_db and the detector output are both in decibels, so the closed loop is a linear 1st-order recursion in the dB domain. Because output power (dB) equals input power (dB) plus gain_db, the loop reduces to

gain_db[n+1] = (1 - 4*loop_bw) * gain_db[n]
             + (4*loop_bw) * (ref_db - px_db[n])

which converges to gain_db = ref_db - px_db with a time constant of roughly 1/(4*loop_bw) samples, independent of the absolute signal level.

** **

The reduction above treats px_db as given. It is not: the detector is inside this loop and it measures in POWER, so a quiet input's dB reading approaches from the wrong side of a concave log and crawls. The level-independence therefore belongs to the loop filter alone, and the OBJECT's settling is level-dependent. Measured, 1/e settling at loop_bw 0.005 against a predicted 50 samples:

+40 dB in: 41 +20 dB: 45 -20 dB: 84 -40 dB: 109

The asymmetry scales with the detector's own bandwidth — the spread between a +40 dB and a -40 dB input is 0.21 at alpha 0.2, 1.01 at 0.05, and 2.98 at 0.01. A composing receiver sizing a warm-up budget from 1/(4*loop_bw) alone will be optimistic by up to 3x on a weak signal. See docs/design/agc.md section 6.

** **

p_avg is an exponential moving average (1-pole leaky integrator) of the instantaneous output power |y|^2. alpha in (0, 1] sets the detector bandwidth: small alpha smooths hard but reacts slowly.

Its input is this object's ONE safety boundary — see AGC_POWER_CEIL. The EMA is where an input sample first becomes persistent state, so it is the only place a malformed input can do lasting damage, and the only place guarded.

** **

Feedback — power is measured AFTER the gain. The gain applied to sample n is computed from samples up to n-1, so the per-sample loop is inherently sequential.

** **

agc_step() advances the control loop every sample. agc_steps() decimates it: the detector + loop filter run once per chunk of decim samples (default AGC_DECIM_DEFAULT; typically 8, 16 or 32). The gain the loop commands is linearly interpolated across the chunk — a first-order hold, so the applied gain has no inter-chunk staircase — while the gain-apply and the power sum vectorise. This is sound because the detector average already band-limits the envelope, but it makes agc_steps() not bit-identical to a per-sample agc_step() loop, only equivalent at convergence. Both per-block coefficients are COMPOUNDED from alpha / loop_bw internally — 1-(1-a)^decim, not decim*a — so both keep their per-sample meaning exactly, including at decim==1.

** **

** **

Each output sample is square-clipped: the real and imaginary parts are independently limited to +/-10^(clip_db/20) — a square region in the IQ plane, not a circular magnitude limit. The clip is the last operation applied to the output and does NOT feed the power detector: the loop always measures the true, unclipped power, so clipping never disturbs convergence. clip_db defaults to AGC_CLIP_DB_DEFAULT, which is high enough to be effectively off.

Lifecycle: agc_create -> (step / steps / reset)* -> agc_destroy

// Hold output power at 0 dB; slow loop, moderate detector smoothing.
agc_state_t *agc = agc_create(0.0, 0.0025, 0.05);
float _Complex y = agc_step(agc, 4.0f + 0.0f * I);  // loud input
// ... feed more samples; gain_db converges so 10*log10(|y|^2) -> 0 dB
agc_destroy(agc);

Public Functions Documentation

function agc_create

Construct a log-domain feedback AGC and return its heap state. The loop integrator starts at 0 dB (unity gain) and the power detector p_avg is pre-seeded to10^ (ref_db/10) linear, so the first block of on-target samples produces no transient. Three parameters tune the closed-loop behaviour:ref_db sets the target,loop_bw sets the convergence speed, andalpha sets the detector smoothing.

agc_state_t * agc_create (
    double ref_db,
    double loop_bw,
    double alpha
) 

Parameters:

  • ref_db Target output power in dB (e.g. 0.0 for unity power).
  • loop_bw Loop noise bandwidth in cycles/sample. The FILTER's time constant is 1/(4*loop_bw) samples; the object settles more slowly than that on a quiet input, because the detector is inside the loop and measures in power (see the Linear-in-dB note above — measured 1.7x to 2.2x at -40 dB in, worse at small alpha). Treat 1/(4*loop_bw) as a floor on settling, not an estimate of it. Smaller values are slower and smoother. With agc_steps(), the pairing rule is 4*decim*loop_bw <= 0.05 — see "Choosing decim".
  • alpha Power-detector EMA coefficient in (0, 1]; smaller values smooth harder but react slower to envelope changes.

Returns:

Heap-allocated agc_state_t, or NULL on allocation failure. The caller must call agc_destroy() when done.

>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.ref_db, agc.loop_bw, agc.alpha
(0.0, 0.0025, 0.05)
>>> agc.gain_db, agc.applied_gain_db
(0.0, 0.0)
>>> agc.decim, agc.clip_db
(8, 120.0)


function agc_destroy

Destroy an AGC instance and release all memory. Frees the heap-allocated agc_state_t . Safe to call withNULL . After this call the pointer is invalid; set it toNULL . The Python binding calls this automatically when the object is garbage- collected or when used as a context manager (with AGC() as agc:).

void agc_destroy (
    agc_state_t * state
) 

Parameters:

  • state Pointer to the state to free; may be NULL (no-op).
    >>> from doppler.agc import AGC
    >>> agc = AGC()
    >>> agc.destroy()   # explicit release
    >>> with AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05) as agc2:
    ...     y = agc2.step(1.0+0.0j)
    ...     y
    (1+0j)
    

function agc_exp10_

Fast 10^v approximation (~1e-3 relative).

JM_FORCEINLINE double agc_exp10_ (
    double v
) 

Routes through 2^z = 2^floor(z) * 2^frac with z = v*log2(10): the integer part becomes a raw IEEE-754 exponent and the fractional part a 4th-order Taylor series. Far cheaper than libm pow(); the AGC loop tolerates orders of magnitude more error than this.

** **

z is saturated into the range the exponent field can hold before it is used. Without that, assembling `((int64\_t)zi + 1023) &lt;&lt; 52 overflows into the SIGN bit for|v|past ~308, and the function returns a **negative** result where the true answer is+infor 0 — measured,agc_exp10_(309)gave-3.09e-308andagc_exp10_(-320) gave-3.23e+296. A gain function that returns a negative gain does not merely lose precision, it inverts the signal. Past the rails this now saturates at2^±1023` instead.

NaN takes the LOW rail, and the direction is the same one AGC_POWER_CEIL uses: when the input is unknown, attenuate. A gain saturated low is silence; a gain saturated high rails everything downstream of it.


function agc_get_applied_gain_db

Return the gain (in dB) actually applied to the most recent sample. Computes 20*log10 (g_last), whereg_last is the linear multiplier that was used on the most recently processed sample. This differs fromgain_db (the loop integrator's current command) because the loop filter advances the command one step ahead after each sample: immediately afteragc_step() __gain_db already reflects the updated command whileapplied_gain_db still reflects what the signal actually saw. At loop convergence the two values are numerically equal. At create/reset both are 0.0 dB (unity).

double agc_get_applied_gain_db (
    const agc_state_t * state
) 

Returns:

Applied gain in dB; 0.0 at create / reset.

>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.applied_gain_db   # unity before any sample
0.0
>>> _ = agc.step(4.0+0.0j)
>>> agc.applied_gain_db   # gain USED on that sample was still 0 dB
0.0
>>> round(agc.gain_db, 6)  # loop already advanced to new command
-0.024276


function agc_get_state

void agc_get_state (
    const agc_state_t * state,
    void * blob
) 

function agc_log10_

Fast log10(p) approximation for p > 0 (~1e-3 absolute).

JM_FORCEINLINE double agc_log10_ (
    double p
) 

Splits p = m * 2^e via the IEEE-754 fields, takes log2(m) from the atanh series with t = (m-1)/(m+1) in [0, 1/3] (two terms), and scales log2 by log10(2). Used only on the decimated control path, so even the divide is amortised across a decimation chunk.

** **

p is saturated into [AGC_POWER_FLOOR, AGC_POWER_CEIL] first. The bit-field split has no notion of a special value: handed a NaN it reads the exponent as an ordinary 1024 and returns a perfectly plausible +308, where libm's log10 returns NaN. Measured on the unguarded version, that fabricated level was what turned a stalled AGC into a runaway one — the loop believed it was seeing +3084 dB and drove the gain the other way, forever. A wrong answer that looks like a right one is worse than an infinity.

The floor is why silence reads as about -300 dB rather than -INF, so that promise is now structural rather than something each caller has to remember to add.


function agc_power_

Power |y|^2 in the detector's working precision (double).

JM_FORCEINLINE double agc_power_ (
    float _Complex y
) 

The power detector EMA, the dB loop filter and agc_log10_ all work in double across the AGC's full (dB) dynamic range, so the squaring promotes the float components once. Defined here so agc_step() — and any composing sample loop that accumulates AGC input power — measures power identically.


function agc_reset

Reset the AGC loop state to its post-create condition. Sets gain_db back to 0 dB (unity), clearsg_last , and re-seeds the power-detector EMAp_avg from the currentref_db so that the first post-reset block produces no transient. All configuration fields (ref_db ,loop_bw ,alpha ,decim ,clip_db ) are left untouched. Use this to process a new, independent signal segment without re-allocating.

void agc_reset (
    agc_state_t * state
) 

Parameters:

  • state Must be non-NULL.
    >>> from doppler.agc import AGC
    >>> import numpy as np
    >>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
    >>> _ = agc.steps(np.full(1000, 4.0+0.0j, dtype=np.complex64))
    >>> round(agc.gain_db, 1)   # converged to -12 dB
    -12.0
    >>> agc.reset()
    >>> agc.gain_db, agc.applied_gain_db
    (0.0, 0.0)
    

function agc_set_state

int agc_set_state (
    agc_state_t * state,
    const void * blob
) 

function agc_set_telemetry

Attach (or detach) a telemetry context and register the AGC's probes on it. Registers two probes, both recorded once per gain-update event and further thinned by decim:

int agc_set_telemetry (
    agc_state_t * state,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 

  • "<prefix>.gain_db" — the loop-filter integrator, i.e. the gain the loop is commanding, in dB.
  • "<prefix>.level_db" — the level the power detector measures, 10*log10(p_avg), in dB. This is the loop's input: the integrator drives ref_db - level_db to zero, so level_db is the zero-referenced settling indicator. Reading it says whether the loop has converged without knowing the true input level, which gain_db alone cannot — gain_db settles to an offset that depends on how loud the signal happens to be.

The pair is emitted from one update, with level_db being the belief that update was answering (measured before the correction is applied). Passing NULL detaches (probe sites revert to their single-branch disabled cost); re-attaching after a reset is idempotent (same name -> same probe id). Setup path, never hot: call before the producer thread starts stepping, and keep every object attached to one context on that one thread (the ring is SPSC — see dp_tlm/dp_tlm_core.h). The context is borrowed, not owned: it must outlive the attachment.

Parameters:

  • state Must be non-NULL.
  • tlm Telemetry context to attach, or NULL to detach.
  • prefix Probe-name prefix, e.g. "agc" or "rx.agc".
  • decim Emit every decim-th gain update; >= 1.

Returns:

DP_OK, or DP_ERR_INVALID when the probe table cannot take both probes or a prefixed name is invalid (the attach fails whole; the object stays detached).

>>> import numpy as np
>>> from doppler.agc import AGC
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 12)
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.set_telemetry(tlm, "agc")
>>> sorted(tlm.probe_names)
['agc.gain_db', 'agc.level_db']
>>> x = (0.5 + 0j) * np.ones(4096, dtype=np.complex64)
>>> _ = agc.steps(x)
>>> recs = tlm.read()      # both probes, per decim-chunk update
>>> gain = recs[recs["probe"] == tlm.probe_id("agc.gain_db")]["value"]
>>> lvl = recs[recs["probe"] == tlm.probe_id("agc.level_db")]["value"]
>>> len(gain) == len(lvl) == 4096 // agc.decim
True
>>> round(float(gain[-1]), 1)   # -6 dB input, 0 dB ref -> +6 dB gain
6.0
>>> round(float(lvl[-1]), 1)    # settled: measured level == ref
0.0


function agc_settling_samples

How many samples this loop needs to settle — the design query.

size_t agc_settling_samples (
    double loop_bw,
    double alpha,
    double gain_err_db,
    double tol_db
) 

Answers "how long must I wait before the output level can be trusted", which a caller sizing a warm-up budget, a burst preamble or an acquisition guard has to answer and could not.

** **

1/(4*loop_bw) is the loop FILTER's time constant, and the object is not the filter — the detector sits inside the loop and measures in power, so a quiet input settles more slowly (see the Linear-in-dB note above). The real settling is M/(4*loop_bw) where M depends on the starting error and on how fast the detector is relative to the filter, alpha/(4*loop_bw). Measured, M runs from about 0.8 on a loud start to nearly 5 on a quiet one with a slow detector.

** **

This runs the real agc_step loop against a constant input and counts, so there is no fitted curve to go stale: the answer is whatever the shipped loop does, and it cannot disagree with the object it describes. Design-time only — it allocates and iterates, so call it while planning a pipeline, never inside one.

Parameters:

  • loop_bw Loop noise bandwidth, as passed to agc_create().
  • alpha Detector EMA coefficient, as passed to agc_create().
  • gain_err_db How far from settled the loop starts, in dB of gain it must apply. POSITIVE for a quiet input (the loop must add gain) — the slow direction, and the one to budget for. For a cold receiver this is the whole input dynamic range it must cover, not the steady-state variation.
  • tol_db Settled means within this many dB of the target.

Returns:

Samples to settle (>= 1), or 0 if the arguments are invalid or the loop does not settle within a bounded search.

>>> from doppler.agc import settling_samples
>>> settling_samples(0.0025, 0.05, 40.0, 0.5)   # cold, 40 dB quiet
430
>>> settling_samples(0.0025, 0.05, 40.0, 3.0)   # a looser bar is cheaper
294
>>> settling_samples(0.0025, 0.05, -40.0, 0.5)  # loud: the fast direction
175
>>> settling_samples(0.01, 0.05, 40.0, 0.5)     # 4x the bandwidth, ~1/4
112
>>> settling_samples(0.0025, 0.05, 0.1, 0.5)    # already inside tol_db
1
>>> settling_samples(0.0, 0.05, 40.0, 0.5)      # refused, not guessed
0


function agc_state_bytes

size_t agc_state_bytes (
    const agc_state_t * state
) 

function agc_step

Process one complex sample through the per-sample AGC loop. Applies the current gain, measures the output power via the EMA detector, advances the loop-filter integrator, then square-clips the returned sample to clip_db . The clip is applied after the detector update, so clipping never disturbs convergence. With the defaultgain_update_period == 1 this is the exact per-sample reference path; withgain_update_period P > 1 the detector and gain-apply still run every sample but the loop-filter command (and the exp10/log10 it needs) refreshes once per P samples — a zero-order hold on the gain that amortises the transcendentals on a sample-rate hot loop, the streaming analogue ofagc_steps() ' decimation.agc_steps() is the faster block equivalent; neither is bit-identical to the P == 1 loop once decimated, but both converge to the same steady state.

JM_FORCEINLINE  JM_HOT float _Complex agc_step (
    agc_state_t * state,
    float _Complex x
) 

Parameters:

  • state Must be non-NULL.
  • x Complex input sample.

Returns:

Gained, clipped output sample x * 10^(gain_db/20) with each component independently clamped to +/-10^(clip_db/20).

>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.step(1.0+0.0j)   # unity gain at start, 0 dB in = 0 dB out
(1+0j)
>>> agc.gain_db           # loop already advanced from 0 dB
0.0
>>> agc2 = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc2.step(4.0+0.0j)  # 12 dB loud; first sample at unity gain
(4+0j)
>>> round(agc2.gain_db, 6)  # loop starts driving gain negative
-0.024276


function agc_steps

Process a block of complex samples through the decimated AGC loop. Splits the input into chunks of decim samples. Within each chunk the gain is linearly interpolated from the previous chunk's end value to the new loop-filter output (a first-order hold) so there is no inter-chunk gain staircase. The detector and loop filter run once per chunk on the chunk's mean power — O(n/decim) control-loop work versus O(n) foragc_step() . The output array may alias the input (in-place).

void agc_steps (
    agc_state_t * state,
    const float _Complex * input,
    float _Complex * output,
    size_t n
) 

Parameters:

  • state Must be non-NULL.
  • input Input complex64 array of n samples.
  • output Output buffer; must hold at least n elements. May alias input for in-place operation.
  • n Number of samples to process.
    >>> from doppler.agc import AGC
    >>> import numpy as np
    >>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
    >>> _ = agc.steps(np.full(1000, 4.0+0.0j, dtype=np.complex64))
    >>> round(agc.gain_db, 1)   # gain converged to -12 dB
    -12.0
    >>> x = np.full(8, 4.0+0.0j, dtype=np.complex64)
    >>> y = agc.steps(x)
    >>> y.shape, y.dtype
    ((8,), dtype('complex64'))
    >>> [round(abs(v)**2, 2) for v in y.tolist()]  # output power ~1.0
    [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
    

function settling_samples

size_t settling_samples (
    double loop_bw,
    double alpha,
    double gain_err_db,
    double tol_db
) 

Macro Definition Documentation

define AGC_CLIP_DB_DEFAULT

Default output clip level ( agc_state_t::clip_db ), in dB.

#define AGC_CLIP_DB_DEFAULT `120.0`

120 dB is a per-component amplitude limit of 10^6 — far above any normally scaled signal, so output clipping is effectively disabled until clip_db is lowered. See agc_state_t::clip_db.


define AGC_DECIM_DEFAULT

Default envelope decimation factor ( agc_state_t::decim ).

#define AGC_DECIM_DEFAULT `8`

agc_steps() runs the detector + loop filter once per chunk of decim samples; useful values are 8, 16 and 32. The rule is 4*decim*loop_bw <= 0.05 (see "Choosing decim" above), which 8 satisfies at every loop bandwidth this object is used at; it is also one AVX-width vector for the in-chunk gain-apply.


define AGC_POWER_CEIL

Power ceiling for the detector, in linear units.

#define AGC_POWER_CEIL `2.3158417847463238e77`

The largest |y|^2 a pair of finite float components can produce: 2*FLT_MAX^2. Any measured power above this came from a non-finite output, which in turn came from a non-finite input or an overflowed gain — never from a signal.

** **

Every power reaching the EMA is put through saturate into [0, AGC_POWER_CEIL], with NaN sent to the ceiling — an unknown level must drive the gain DOWN, since too little gain loses a signal while too much rails everything downstream.

That boundary, and not the stages around it, because the EMA is the first place an input sample becomes persistent state. The gain multiply ahead of it is transient: a bad sample makes one bad output sample and is gone. Once it folds into p_avg it is remembered, and the measured level, the loop integrator and the applied gain are all functions of p_avg. One guard here makes the whole chain total, where a clamp at each stage would be several chances to miss one.

It is sufficient because a guarded p_avg is a convex combination of a finite p_avg and a saturated p, so it cannot leave the interval once it starts inside — which agc_create() and agc_reset() guarantee by seeding it with the reference power. Measured on the unguarded loop, a single non-finite input sample drove p_avg to NaN permanently, and a following normal sample did not recover it.


define AGC_POWER_FLOOR

Power floor for the detector, in linear units.

#define AGC_POWER_FLOOR `1e-30`

The low end of agc_log10_'s saturation range, so a long run of silence yields a large-but-finite measured level — exactly -300 dB — instead of -INF / NaN, and the log10() argument stays a normal (non-denormal) double. The floor lives inside the primitive rather than at each call site, so that promise is structural and not something a caller has to remember to add.

** **

This said "never reached in normal operation — @c p_avg is seeded with the reference power at create/reset", which is true of the seed and says nothing about the steady state. Any gap in the signal reaches it: a muted source, a stream discontinuity, a receiver started on a zero-filled buffer. Measured on the unguarded object, ~800 silent samples — 100 symbols at 8 samples per symbol — left the loop permanently dead, because reaching the floor gave the filter a constant +300 dB error to integrate. AGC_POWER_CEIL is the guard that makes reaching it survivable.


define AGC_STATE_MAGIC

#define AGC_STATE_MAGIC `DP_FOURCC ('A', 'G', 'C', ' ')`

define AGC_STATE_VERSION

#define AGC_STATE_VERSION `3u /* v3: telemetry attachment (zeroed in blob) */`


The documentation for this class was generated from the following file native/inc/agc/agc_core.h