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 "util/util_core.h"#include <math.h>
Classes¶
| Type | Name |
|---|---|
| struct | agc_state_t AGC state. |
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) |
| 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). |
Macros¶
| Type | Name |
|---|---|
| define | AGC_CLIP_DB_DEFAULT 120.0Default output clip level ( agc_state_t::clip_db ), in dB. |
| define | AGC_DECIM_DEFAULT 8Default envelope decimation factor ( agc_state_t::decim ). |
| define | AGC_POWER_FLOOR 1e-30Power 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 2u /\* v2: gain\_update\_period + gain\_phase/clip\_lin \*/ |
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
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. A 60 dB-loud signal and a 0 dB-quiet signal settle in the same number of samples; only a level-dependent loop would not.
** **
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.
** **
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. The per-block detector and loop coefficients are rescaled from alpha / loop_bw internally, so those keep their per-sample meaning; keep loop_bw well below 1/(4*decim) for loop stability.
** **
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.
Parameters:
ref_dbTarget output power in dB (e.g.0.0for unity power).loop_bwLoop noise bandwidth in cycles/sample; the loop settles in roughly1/(4*loop_bw) samples. Smaller values are slower and smoother; keep well below1/(4*decim) when using agc_steps().alphaPower-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:).
Parameters:
statePointer to the state to free; may beNULL(no-op).
function agc_exp10_¶
Fast 10^v approximation (~1e-3 relative).
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.
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).
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¶
function agc_log10_¶
Fast log10(p) approximation for p > 0 (~1e-3 absolute).
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.
function agc_power_¶
Power |y|^2 in the detector's working precision (double).
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.
Parameters:
stateMust be non-NULL.
function agc_set_state¶
function agc_state_bytes¶
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.
Parameters:
stateMust be non-NULL.xComplex 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 passes 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:
stateMust be non-NULL.inputInput complex64 array ofnsamples.outputOutput buffer; must hold at leastnelements. May aliasinputfor in-place operation.nNumber 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]
Macro Definition Documentation¶
define AGC_CLIP_DB_DEFAULT¶
Default output clip level ( agc_state_t::clip_db ), in dB.
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 ).
agc_steps() runs the detector + loop filter once per chunk of decim samples. decim must stay small relative to the loop time constant ~1/(4*loop_bw); useful values are 8, 16 and 32. 8 keeps the gain trajectory well inside the default loop bandwidth and is one AVX-width vector for the in-chunk gain-apply.
define AGC_POWER_FLOOR¶
Power floor for the detector, in linear units.
Substituted for p_avg inside log10() so that a long run of silence yields a large-but-finite measured level (about -300 dB) instead of -INF / NaN. Also keeps the log10() argument a normal (non-denormal) double. Never reached in normal operation — p_avg is seeded with the reference power at create/reset.
define AGC_STATE_MAGIC¶
define AGC_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/agc/agc_core.h