// Part of dump1090, a Mode S message decoder for RTLSDR devices.
//
// adaptive.c: adaptive gain control
//
// Copyright (c) 2021 FlightAware, LLC
//
// This file is free software: you may copy, redistribute and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 2 of the License, or (at your
// option) any later version.
//
// This file is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "dump1090.h"
#include "adaptive.h"

//
// gain limits
//
static int adaptive_gain_min;
static int adaptive_gain_max;

// gain steps relative to current gain
static float adaptive_gain_up_db;
static float adaptive_gain_down_db;

//
// block handling
//
// 1 block = approx 1 second of samples. Control updates are done at the end of each block only.
// Each block is made up of an integer number of subblocks (currently 20)
//
// 1 subblock = approx 50ms of samples. Duty cycle decisions are made at the subblock level;
// either the whole subblock is processed, or the whole subblock is skipped.
// Each subblock is made up of an integer number of windows (currently 1250)
//
// 1 window = approx 40us of samples. Burst measurements are made by counting samples within each window.
//
// All three levels are aligned, i.e. every block boundary is also a subblock boundary;
// every subblock boundary is also a window boundary.


static const unsigned adaptive_subblocks_per_block = 20;   // subblocks per block
static unsigned adaptive_subblocks_remaining;              // subblocks remaining in the current block

// Duty cycle is expressed as N/D
// where N = adaptive_subblbock_dutycycle_N = adaptive_subblocks_per_block * Modes.adaptive_duty_cycle
//   and D = adaptive_subblocks_dutycycle_D = adaptive_subblocks_per_block
//
// i.e. within each block, there are exactly N active subblocks out of D total subblocks
//
// The active subblocks are distributed evenly across the block by increasing a counter by N on each
// subblock, modulo D, and marking the subblock as active each time the counter rolls over.

static unsigned adaptive_subblock_dutycycle_N;                                        // subblock duty cycle numerator N

// stretch gcc doesn't like this as a separate const
#define adaptive_subblock_dutycycle_D adaptive_subblocks_per_block

static unsigned adaptive_subblock_dutycycle_counter;   // subblock duty cycle counter (modulo D)
static bool adaptive_subblock_active;                  // is the current subblock active i.e. samples should be processed, not skipped?

static unsigned adaptive_samples_per_subblock;         // samples per subblock
static unsigned adaptive_subblock_samples_remaining;   // samples remaining in the current subblock

static unsigned adaptive_samples_per_window;           // samples per window

void adaptive_init();
void adaptive_update(uint16_t *buf, unsigned length, struct modesMessage *decoded);
static void adaptive_update_subblock(uint16_t *buf, unsigned length, struct modesMessage *decoded);
static void adaptive_end_of_block();
static void adaptive_control_update();

//
// burst handling
//

static unsigned adaptive_burst_window_remaining;       // samples remaining in the current burst window
static unsigned adaptive_burst_window_counter;         // loud samples seen in current burst window
static unsigned adaptive_burst_runlength;              // consecutive loud burst windows seen
static unsigned adaptive_burst_block_loud_undecoded;   // loud undecoded bursts seen in this block so far
static unsigned adaptive_burst_block_loud_decoded;     // loud decoded messages seen in this block so far
static double adaptive_burst_loud_undecoded_smoothed;  // smoothed rate of loud misdecodes per block
static double adaptive_burst_loud_decoded_smoothed;    // smoothed rate of loud successful decodes per block
static unsigned adaptive_burst_change_timer;           // countdown inhibiting control after changing gain
static double adaptive_burst_loud_threshold;           // current signal level threshold for a "loud decode"
static unsigned adaptive_burst_loud_blocks = 0;        // consecutive blocks with loud rate
static unsigned adaptive_burst_quiet_blocks = 0;       // consecutive blocks with quiet rate

static void adaptive_burst_update(uint16_t *buf, unsigned length);
static void adaptive_burst_skip(unsigned length);
static unsigned adaptive_burst_count_samples(uint16_t *buf, unsigned n);
static void adaptive_burst_scan_windows(uint16_t *buf, unsigned windows);
static void adaptive_burst_end_of_window(unsigned counter);
static void adaptive_burst_end_of_block();

//
// noise floor measurement (adaptive dynamic range)
//

static unsigned *adaptive_range_radix;                 // radix-sort buckets for current block
static unsigned adaptive_range_radix_counter;          // sum of all radix-sort buckets (= number of samples sorted)
static double adaptive_range_smoothed;                 // smoothed noise floor estimate, dBFS
static enum { RANGE_SCAN_IDLE, RANGE_SCAN_UP, RANGE_SCAN_DOWN, RANGE_RESCAN_UP, RANGE_RESCAN_DOWN } adaptive_range_state = RANGE_SCAN_UP;
static unsigned adaptive_range_change_timer;           // countdown inhibiting control after changing gain
static unsigned adaptive_range_rescan_timer;           // countdown to next upwards gain reprobe
static int adaptive_range_gain_limit;                  // probed maximum gain step with acceptable dynamic range

static void adaptive_range_update(uint16_t *buf, unsigned length);
static void adaptive_range_end_of_block();

// Try to change the SDR gain to 'step' and tell the user about it,
// with 'why' as the reason to show. Return true if the gain actually changed.
static bool adaptive_set_gain(int step, const char *why)
{
    if (step < adaptive_gain_min)
        step = adaptive_gain_min;
    if (step > adaptive_gain_max)
        step = adaptive_gain_max;

    int current_gain = sdrGetGain();
    if (current_gain == step)
        return false;

    fprintf(stderr, "adaptive: changing gain from %.1fdB (step %d) to %.1fdB (step %d) because: %s\n",
            sdrGetGainDb(current_gain), current_gain, sdrGetGainDb(step), step, why);

    int new_gain = sdrSetGain(step);
    bool changed = (current_gain != new_gain);
    if (changed)
        ++Modes.stats_current.adaptive_gain_changes;
    return changed;
}

// Update internal state to reflect a gain change
// (usually after adaptive_set_gain returns true, but also called during init)
static void adaptive_gain_changed()
{
    int new_gain = sdrGetGain();
    adaptive_gain_up_db = sdrGetGainDb(new_gain + 1) - sdrGetGainDb(new_gain);
    adaptive_gain_down_db = sdrGetGainDb(new_gain) - sdrGetGainDb(new_gain - 1);
    
    double loud_threshold_dbfs = 0 - adaptive_gain_up_db - 3.0;
    adaptive_burst_loud_threshold = pow(10, loud_threshold_dbfs / 10.0);

    adaptive_range_change_timer = Modes.adaptive_range_change_delay;
    adaptive_burst_change_timer = Modes.adaptive_burst_change_delay;
    adaptive_burst_loud_blocks = 0;
    adaptive_burst_quiet_blocks = 0;
}

// External init entry point
void adaptive_init()
{
    int maxgain = sdrGetMaxGain();

    // If the SDR doesn't support gain control, disable ourselves
    if (maxgain < 0) {
        if (Modes.adaptive_burst_control || Modes.adaptive_range_control) {
            fprintf(stderr, "warning: adaptive gain control requested, but SDR gain control not available, ignored.\n");
        }
        Modes.adaptive_burst_control = false;
        Modes.adaptive_range_control = false;
    }        

    // If we're disabled, do nothing
    if (!Modes.adaptive_burst_control && !Modes.adaptive_range_control)
        return;

    // Set up window, subblock, and block sizes
    // Look for 40us bursts
    adaptive_samples_per_window = Modes.sample_rate / 25000;

    // Use ~50ms subblocks; ensure it's an exact multiple of window size
    adaptive_samples_per_subblock = adaptive_samples_per_window * 1250;

    adaptive_subblocks_remaining = adaptive_subblocks_per_block;
    adaptive_subblock_samples_remaining = adaptive_samples_per_subblock;
    adaptive_subblock_active = false;

    float N = roundf(adaptive_subblock_dutycycle_D * Modes.adaptive_duty_cycle);
    if (N <= 0)
        N = 1;
    if (N > adaptive_subblock_dutycycle_D)
        N = adaptive_subblock_dutycycle_D;
    fprintf(stderr, "adaptive: using %.0f%% duty cycle\n", 100.0 * N / adaptive_subblock_dutycycle_D);
    adaptive_subblock_dutycycle_N = (unsigned)N;

    adaptive_burst_window_remaining = adaptive_samples_per_window;
    adaptive_burst_window_counter = 0;

    adaptive_range_radix = calloc(sizeof(unsigned), 65536);
    adaptive_range_state = RANGE_RESCAN_UP;

    // select and enforce gain limits
    for (adaptive_gain_min = 0; adaptive_gain_min < maxgain; ++adaptive_gain_min) {
        if (sdrGetGainDb(adaptive_gain_min) >= Modes.adaptive_min_gain_db)
            break;
    }

    for (adaptive_gain_max = maxgain; adaptive_gain_max > adaptive_gain_min; --adaptive_gain_max) {
        if (sdrGetGainDb(adaptive_gain_max) <= Modes.adaptive_max_gain_db)
            break;
    }

    fprintf(stderr, "adaptive: enabled adaptive gain control with gain limits %.1fdB (step %d) .. %.1fdB (step %d)\n",
            sdrGetGainDb(adaptive_gain_min), adaptive_gain_min, sdrGetGainDb(adaptive_gain_max), adaptive_gain_max);
    if (Modes.adaptive_range_control)
        fprintf(stderr, "adaptive: enabled dynamic range control, target dynamic range %.1fdB\n", Modes.adaptive_range_target);
    if (Modes.adaptive_burst_control)
        fprintf(stderr, "adaptive: enabled burst control\n");
    adaptive_set_gain(sdrGetGain(), "constraining gain to adaptive gain limits");
    adaptive_gain_changed();

    adaptive_range_gain_limit = sdrGetGain();
}

// Feed some samples into the adaptive system. Any number of samples might be passed in.
void adaptive_update(uint16_t *buf, unsigned length, struct modesMessage *decoded)
{
    if (!Modes.adaptive_burst_control && !Modes.adaptive_range_control)
        return;

    // process complete subblocks
    while (length >= adaptive_subblock_samples_remaining) {
        if (adaptive_subblock_active)
            adaptive_update_subblock(buf, adaptive_subblock_samples_remaining, decoded);

        buf += adaptive_subblock_samples_remaining;
        length -= adaptive_subblock_samples_remaining;
        adaptive_subblock_samples_remaining = adaptive_samples_per_subblock;

        adaptive_subblock_dutycycle_counter += adaptive_subblock_dutycycle_N;
        if (adaptive_subblock_dutycycle_counter >= adaptive_subblock_dutycycle_D) {
            adaptive_subblock_dutycycle_counter -= adaptive_subblock_dutycycle_D;
            adaptive_subblock_active = true;
        } else {
            adaptive_subblock_active = false;
            // fake a quiet window to reset any existing run
            adaptive_burst_end_of_window(0);
        }

        if (!--adaptive_subblocks_remaining) {
            // Block completed, do a control update
            adaptive_subblocks_remaining = adaptive_subblocks_per_block;
            adaptive_end_of_block();
        }
    }

    // process final samples that don't complete a subblock
    if (length > 0) {
        if (adaptive_subblock_active)
            adaptive_update_subblock(buf, length, decoded);
        adaptive_subblock_samples_remaining -= length;
    }
}

// Feed some samples into the adaptive system. The samples are guaranteed to not cross a subblock boundary.
// The samples should be processsed (i.e. duty cycle is in the active part)
static void adaptive_update_subblock(uint16_t *buf, unsigned length, struct modesMessage *decoded)
{
    if (decoded) {
        if (/* decoded->msgbits == 112 && */ decoded->signalLevel >= adaptive_burst_loud_threshold)
            ++adaptive_burst_block_loud_decoded;
        adaptive_burst_skip(length);
    } else {
        adaptive_burst_update(buf, length);
        adaptive_range_update(buf, length);
    }
}

// Burst measurement: ignore the next 'length' samples (they are a successfully decoded message)
static void adaptive_burst_skip(unsigned length)
{
    if (!Modes.adaptive_burst_control)
        return;

    // first window
    if (length < adaptive_burst_window_remaining) {
        // partial fill
        adaptive_burst_window_remaining -= length;
        return;
    }

    // skip remainder of first window, dispatch it
    adaptive_burst_end_of_window(adaptive_burst_window_counter);
    length -= adaptive_burst_window_remaining;

    // skip remaining windows, dispatch them
    unsigned windows = length / adaptive_samples_per_window;
    unsigned samples = windows * adaptive_samples_per_window;
    while (windows--)
        adaptive_burst_end_of_window(0);

    length -= samples;

    // final partial window
    adaptive_burst_window_counter = 0;
    adaptive_burst_window_remaining = adaptive_samples_per_window - length;
}

// Burst measurement: process 'length' samples from 'buf', look for loud bursts;
// the samples might cross burst window boundaries;
// the samples will not cross a block boundary.
static void adaptive_burst_update(uint16_t *buf, unsigned length)
{
    if (!Modes.adaptive_burst_control)
        return;

    // first window
    if (length < adaptive_burst_window_remaining) {
        // partial fill
        adaptive_burst_window_counter += adaptive_burst_count_samples(buf, length);
        adaptive_burst_window_remaining -= length;
        return;
    }

    // complete fill of first partial window
    unsigned n = adaptive_burst_window_remaining;
    unsigned counter = adaptive_burst_window_counter + adaptive_burst_count_samples(buf, n);
    adaptive_burst_end_of_window(counter);
    buf += n;
    length -= n;

    // remaining windows
    unsigned windows = length / adaptive_samples_per_window;
    unsigned samples = windows * adaptive_samples_per_window;
    adaptive_burst_scan_windows(buf, windows);
    buf += samples;
    length -= samples;

    // final partial window
    adaptive_burst_window_counter = adaptive_burst_count_samples(buf, length);
    adaptive_burst_window_remaining = adaptive_samples_per_window - length;
}

// Burst measurement: process 'windows' complete burst windows starting at 'buf';
// 'buf' is aligned to the start of a burst window
static void adaptive_burst_scan_windows(uint16_t *buf, unsigned windows)
{
    while (windows--) {
        unsigned counter = adaptive_burst_count_samples(buf, adaptive_samples_per_window);
        buf += adaptive_samples_per_window;
        adaptive_burst_end_of_window(counter);
    }
}

// Burst measurement: process 'n' samples from 'buf', look for loud samples;
// the samples are guaranteed not to cross window boundaries;
// return the number of loud samples seen
static inline unsigned adaptive_burst_count_samples(uint16_t *buf, unsigned n)
{
    unsigned counter;
    starch_count_above_u16(buf, n, 46395 /* -3dBFS */, &counter);
    return counter;
}

// Burst measurement: we reached the end of a burst window with 'counter'
// loud samples seen, handle that window.
static void adaptive_burst_end_of_window(unsigned counter)
{
    if (counter > adaptive_samples_per_window / 4) {
        // This window is loud, extend any existing run of loud windows
        ++adaptive_burst_runlength;
    } else {
        // Quiet window. If we saw a run of loud windows >= 80us long, count
        // that as a candidate for an over-amplified message that was
        // not decoded.
        if (adaptive_burst_runlength >= 2 && adaptive_burst_runlength <= 5)
            ++adaptive_burst_block_loud_undecoded;
        adaptive_burst_runlength = 0;
    }
}

// Noise measurement: process 'length' samples from 'buf'.
// The samples will not cross a block boundary.
static void adaptive_range_update(uint16_t *buf, unsigned length)
{
    if (!Modes.adaptive_range_control)
        return;

    adaptive_range_radix_counter += length;
    while (length--) {
        // do a very simple radix sort of sample magnitudes
        // so we can later find the Nth percentile value
        ++adaptive_range_radix[buf[0]];
        ++buf;
    }
}

// Noise measurement: we reached the end of a block, update
// our noise estimate
static void adaptive_range_end_of_block()
{
    if (!Modes.adaptive_range_control)
        return;

    unsigned n = 0, i = 0;

    // measure Nth percentile magnitude
    unsigned count_n = adaptive_range_radix_counter * Modes.adaptive_range_percentile / 100;
    while (i < 65536 && n <= count_n)
        n += adaptive_range_radix[i++];
    uint16_t percentile_n = i - 1;

    // maintain an EMA of the Nth percentile
    adaptive_range_smoothed = adaptive_range_smoothed * (1 - Modes.adaptive_range_alpha) + percentile_n * Modes.adaptive_range_alpha;
    // .. report to stats in dBFS
    if (adaptive_range_smoothed > 0) {
        Modes.stats_current.adaptive_noise_dbfs = 20 * log10(adaptive_range_smoothed / 65536.0);
    } else {
        Modes.stats_current.adaptive_noise_dbfs = 0;
    }

    // reset radix sort for the next block
    memset(adaptive_range_radix, 0, 65536 * sizeof(unsigned));
    adaptive_range_radix_counter = 0;
}

// Burst measurement: we reached the end of a block, update our burst rate estimate
static void adaptive_burst_end_of_block()
{
    if (!Modes.adaptive_burst_control)
        return;

    // scale rates based on the actual duty cycle fraction
    //  (e.g. if we are only inspecting 2/5 of samples, then scale the rate by 5/2)
    double scale = (double)adaptive_subblock_dutycycle_D / adaptive_subblock_dutycycle_N;

    // maintain an EMA of the number of undecoded loud bursts seen per block
    Modes.stats_current.adaptive_loud_undecoded += adaptive_burst_block_loud_undecoded;
    adaptive_burst_loud_undecoded_smoothed = adaptive_burst_loud_undecoded_smoothed * (1 - Modes.adaptive_burst_alpha) + scale * adaptive_burst_block_loud_undecoded * Modes.adaptive_burst_alpha;
    adaptive_burst_block_loud_undecoded = 0;

    // maintain an EMA of the number of decoded, but loud, messages seen per block
    Modes.stats_current.adaptive_loud_decoded += adaptive_burst_block_loud_decoded;
    adaptive_burst_loud_decoded_smoothed = adaptive_burst_loud_decoded_smoothed * (1 - Modes.adaptive_burst_alpha) + scale * adaptive_burst_block_loud_decoded * Modes.adaptive_burst_alpha;
    adaptive_burst_block_loud_decoded = 0;
}


void flush_stats(uint64_t now);

static void adaptive_increase_gain(const char *why)
{
    if (adaptive_set_gain(sdrGetGain() + 1, why))
        adaptive_gain_changed();
}

static void adaptive_decrease_gain(const char *why)
{
    if (adaptive_set_gain(sdrGetGain() - 1, why))
        adaptive_gain_changed();
}

// Adaptive gain: we reached a block boundary. Update measurements and act on them.
static void adaptive_end_of_block()
{
    adaptive_range_end_of_block();
    adaptive_burst_end_of_block();

    adaptive_control_update();

    Modes.stats_current.adaptive_valid = true;
    Modes.stats_current.adaptive_range_gain_limit = adaptive_range_gain_limit;

    int current = sdrGetGain();
    if (current >= 0)
        ++Modes.stats_current.adaptive_gain_seconds[current < STATS_GAIN_COUNT ? current : STATS_GAIN_COUNT-1];
}

static void adaptive_control_update()
{
    // votes for what to do with the gain
    // "gain_not_up" overlaps somewhat with "gain_down", but they are not identical;
    // burst control may want to prevent gain from increasing, but not necessarily
    // decrease gain.

    bool gain_up = false;
    const char *gain_up_reason = NULL;
    bool gain_down = false;
    const char *gain_down_reason = NULL;
    bool gain_not_up = false;

    int current_gain = sdrGetGain();

    if (adaptive_burst_change_timer)
        --adaptive_burst_change_timer;
    if (adaptive_range_change_timer > 0)
        --adaptive_range_change_timer;
    if (adaptive_range_rescan_timer > 0)
        --adaptive_range_rescan_timer;

    if (Modes.adaptive_burst_control && !adaptive_burst_change_timer) {
        if (adaptive_burst_loud_undecoded_smoothed > Modes.adaptive_burst_loud_rate) {
            adaptive_burst_quiet_blocks = 0;
            ++adaptive_burst_loud_blocks;
        } else if (adaptive_burst_loud_decoded_smoothed < Modes.adaptive_burst_quiet_rate) {
            adaptive_burst_loud_blocks = 0;
            ++adaptive_burst_quiet_blocks;
        } else {
            adaptive_burst_loud_blocks = 0;
            adaptive_burst_quiet_blocks = 0;
        }

        if (adaptive_burst_loud_blocks >= Modes.adaptive_burst_loud_runlength) {
            // we need to reduce gain (further)
            gain_down = gain_not_up = true;
            gain_down_reason = "high rate of loud undecoded messages";

            // if we're currently doing a downward scan, reducing gain further may confuse it;
            // stop that scan and restart it once we are no longer in a reduced-gain state
            if (adaptive_range_state == RANGE_SCAN_DOWN || adaptive_range_state == RANGE_RESCAN_DOWN) {
                adaptive_range_state = RANGE_SCAN_IDLE;
                adaptive_range_rescan_timer = 0;
            }
        } else if (adaptive_burst_quiet_blocks < Modes.adaptive_burst_quiet_runlength) {
            // we're OK at the current gain, but should not increase it
            gain_not_up = true;
        } else if (current_gain < adaptive_range_gain_limit) {
            // we're OK at the current gain, and can increase gain to the previously discovered
            // dynamic range limit
            gain_up = true;
            gain_up_reason = "low loud message rate and gain below dynamic range limit";
        }
    }

    if (Modes.adaptive_range_control && !adaptive_range_change_timer) {
        float available_range = -20 * log10(adaptive_range_smoothed / 65536.0);
        // allow the gain limit to increase if this gain setting is acceptable
        // (decreasing the limit is done separately depending on the current state as we make slightly different decisions in IDLE
        // to provide hysteresis)
        if (available_range >= Modes.adaptive_range_target && current_gain > adaptive_range_gain_limit) {
            adaptive_range_gain_limit = current_gain;
        }
        switch (adaptive_range_state) {
        case RANGE_SCAN_UP:
        case RANGE_RESCAN_UP:
            if (available_range < Modes.adaptive_range_target) {
                // Current gain fails to meet our target. Switch to downward scanning.
                fprintf(stderr, "adaptive: available dynamic range (%.1fdB) < required dynamic range (%.1fdB), switching to downward scan\n", available_range, Modes.adaptive_range_target);
                gain_down = gain_not_up = true;
                gain_down_reason = "probing dynamic range gain lower bound";
                adaptive_range_state = (adaptive_range_state == RANGE_RESCAN_UP ? RANGE_RESCAN_DOWN : RANGE_SCAN_DOWN);
                if (adaptive_range_gain_limit >= current_gain) {
                    adaptive_range_gain_limit = current_gain - 1;
                }
                break;
            }

            if (sdrGetGain() >= adaptive_gain_max) {
                // We have reached our upper gain limit
                fprintf(stderr, "adaptive: reached upper gain limit, halting dynamic range scan here\n");
                adaptive_range_state = RANGE_SCAN_IDLE;
                adaptive_range_rescan_timer = Modes.adaptive_range_rescan_delay;
                break;
            }

            // This gain step is OK and we have more to try, try the next gain step up.
            // (But if burst detection has inhibited increasing gain, don't do anything yet, just try again next block)
            if (!gain_not_up) {
                fprintf(stderr, "adaptive: available dynamic range (%.1fdB) >= required dynamic range (%.1fdB), continuing upward scan\n", available_range, Modes.adaptive_range_target);
                gain_up = true;
                gain_up_reason = "probing dynamic range gain upper bound";
            }
            break;

        case RANGE_SCAN_DOWN:
        case RANGE_RESCAN_DOWN:
            if (available_range >= Modes.adaptive_range_target) {
                // Current gain meets our target; we are done with the scan.
                fprintf(stderr, "adaptive: available dynamic range (%.1fdB) >= required dynamic range (%.1fdB), stopping downwards scan here\n", available_range, Modes.adaptive_range_target);
                adaptive_range_state = RANGE_SCAN_IDLE;
                adaptive_range_rescan_timer = (adaptive_range_state == RANGE_SCAN_DOWN ? Modes.adaptive_range_scan_delay : Modes.adaptive_range_rescan_delay);
                break;
            }

            if (adaptive_range_gain_limit >= current_gain) {
                adaptive_range_gain_limit = current_gain - 1;
            }

            if (sdrGetGain() <= adaptive_gain_min) {
                fprintf(stderr, "adaptive: reached lower gain limit, halting dynamic range scan here\n");
                adaptive_range_state = RANGE_SCAN_IDLE;
                adaptive_range_rescan_timer = Modes.adaptive_range_rescan_delay;
                break;
            }

            // This gain step is too loud and we have more to try, try the next gain step down
            fprintf(stderr, "adaptive: available dynamic range (%.1fdB) < required dynamic range (%.1fdB), continuing downwards scan\n", available_range, Modes.adaptive_range_target);
            gain_down = gain_not_up = true;
            gain_down_reason = "probing dynamic range gain lower bound";
            break;

        case RANGE_SCAN_IDLE:
            // Look for increased noise that could be compensated for by decreasing gain.
            // Do this even if we're waiting to rescan or if burst control is also active
            if (available_range + adaptive_gain_down_db / 2 < Modes.adaptive_range_target && sdrGetGain() > adaptive_gain_min) {
                fprintf(stderr, "adaptive: available dynamic range (%.1fdB) + half gain step down (%.1fdB) < required dynamic range (%.1fdB), starting downward scan\n",
                        available_range, adaptive_gain_down_db / 2, Modes.adaptive_range_target);
                if (adaptive_range_gain_limit >= current_gain) {
                    adaptive_range_gain_limit = current_gain - 1;
                }
                adaptive_range_state = RANGE_SCAN_DOWN;
                gain_down = gain_not_up = true;
                gain_down_reason = "dynamic range fell below target value";
                break;
            }

            // Infrequently consider increasing gain to handle the case where we've selected a too-low gain where the noise floor is dominated by noise unrelated to the gain setting.
            // But don't do this while burst control is preventing gain increases.
            if (!adaptive_range_rescan_timer && !gain_not_up) {
                if (available_range >= Modes.adaptive_range_target && sdrGetGain() < adaptive_gain_max) {
                    fprintf(stderr, "adaptive: start periodic scan for acceptable dynamic range at increased gain\n");
                    gain_up = true;
                    gain_up_reason = "periodic re-probing of dynamic range gain upper bound";
                    adaptive_range_state = RANGE_RESCAN_UP;
                    break;
                }

                // Nothing to do for a while.
                adaptive_range_rescan_timer = Modes.adaptive_range_rescan_delay;
            }

            break;

        default:
            fprintf(stderr, "adaptive: in a weird state (%u), trying to fix it\n", (unsigned) adaptive_range_state);
            adaptive_range_state = RANGE_SCAN_IDLE;
            adaptive_range_rescan_timer = Modes.adaptive_range_rescan_delay;
            break;
        }
    }

    // now actually perform any gain changes

    if (gain_down)
        adaptive_decrease_gain(gain_down_reason);
    else if (gain_up && !gain_not_up)
        adaptive_increase_gain(gain_up_reason);
}
