Add --adaptive-duty-cycle option.

This makes the adaptive gain code only process some fraction of all samples,
rather than every sample. This helps a lot with CPU on slower machines.
We default to a 50% duty cycle to save some CPU without really affecting
the behavior much.

We split the one-second adaptive gain blocks into 20* 50ms "subblocks"
Each subblock is either entirely processed or not processed at all,
based on the configured duty cycle. (This means that the duty cycle
can only be specified in approx 5% increments, but that should be
fine)

For adaptive dynamic range, there's no further complications - we
just look at fewer samples to compute the noise floor.

For adaptive burst, we need to scale up the measured burst rate by
1/duty cycle to account for the samples that we didn't process. It's
also more likely to cause functional changes as it's quite possible
for a relatively small number of loud messages being missed to
affect the computed rate substantially. Not many options there other
than increasing the duty cycle when adaptive burst handling is wanted.
This commit is contained in:
Oliver Jowett 2021-07-15 16:44:26 +08:00
parent e0f7a33df4
commit be3c9930f1
3 changed files with 106 additions and 34 deletions

View File

@ -33,13 +33,44 @@ 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 unsigned adaptive_block_remaining; // samples in each block
static unsigned adaptive_block_size; // samples remaining in the current block
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
static const unsigned adaptive_subblock_dutycycle_D = adaptive_subblocks_per_block; // subblock duty cycle denominator D
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_single(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();
@ -47,7 +78,6 @@ static void adaptive_control_update();
// burst handling
//
static unsigned adaptive_burst_window_size; // samples in each burst window
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
@ -140,14 +170,27 @@ void adaptive_init()
if (!Modes.adaptive_burst_control && !Modes.adaptive_range_control)
return;
// Set up window, subblock, and block sizes
// Look for 40us bursts
adaptive_burst_window_size = Modes.sample_rate / 25000;
adaptive_burst_window_remaining = adaptive_burst_window_size;
adaptive_burst_window_counter = 0;
adaptive_samples_per_window = Modes.sample_rate / 25000;
// Use an overall block size that is an exact multiple of the burst window, close to 1 second long
adaptive_block_size = adaptive_burst_window_size * 25000;
adaptive_block_remaining = adaptive_block_size;
// 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 > adaptive_subblock_dutycycle_D) {
fprintf(stderr, "warning: --adaptive-duty-cycle value %.1f%% is out of range, using 100%% instead\n",
Modes.adaptive_duty_cycle * 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_SCAN_UP;
@ -180,26 +223,44 @@ void adaptive_update(uint16_t *buf, unsigned length, struct modesMessage *decode
{
if (!Modes.adaptive_burst_control && !Modes.adaptive_range_control)
return;
// process samples up to a block boundary, then process the completed block
while (length >= adaptive_block_remaining) {
adaptive_update_single(buf, adaptive_block_remaining, decoded);
buf += adaptive_block_remaining;
length -= adaptive_block_remaining;
adaptive_end_of_block();
adaptive_block_remaining = adaptive_block_size;
// 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 block
// process final samples that don't complete a subblock
if (length > 0) {
adaptive_update_single(buf, length, decoded);
adaptive_block_remaining -= length;
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 block boundary.
static void adaptive_update_single(uint16_t *buf, unsigned length, struct modesMessage *decoded)
// 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)
@ -229,8 +290,8 @@ static void adaptive_burst_skip(unsigned length)
length -= adaptive_burst_window_remaining;
// skip remaining windows, dispatch them
unsigned windows = length / adaptive_burst_window_size;
unsigned samples = windows * adaptive_burst_window_size;
unsigned windows = length / adaptive_samples_per_window;
unsigned samples = windows * adaptive_samples_per_window;
while (windows--)
adaptive_burst_end_of_window(0);
@ -238,7 +299,7 @@ static void adaptive_burst_skip(unsigned length)
// final partial window
adaptive_burst_window_counter = 0;
adaptive_burst_window_remaining = adaptive_burst_window_size - length;
adaptive_burst_window_remaining = adaptive_samples_per_window - length;
}
// Burst measurement: process 'length' samples from 'buf', look for loud bursts;
@ -265,15 +326,15 @@ static void adaptive_burst_update(uint16_t *buf, unsigned length)
length -= n;
// remaining windows
unsigned windows = length / adaptive_burst_window_size;
unsigned samples = windows * adaptive_burst_window_size;
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_burst_window_size - length;
adaptive_burst_window_remaining = adaptive_samples_per_window - length;
}
// Burst measurement: process 'windows' complete burst windows starting at 'buf';
@ -281,8 +342,8 @@ static void adaptive_burst_update(uint16_t *buf, unsigned length)
static void adaptive_burst_scan_windows(uint16_t *buf, unsigned windows)
{
while (windows--) {
unsigned counter = adaptive_burst_count_samples(buf, adaptive_burst_window_size);
buf += adaptive_burst_window_size;
unsigned counter = adaptive_burst_count_samples(buf, adaptive_samples_per_window);
buf += adaptive_samples_per_window;
adaptive_burst_end_of_window(counter);
}
}
@ -301,7 +362,7 @@ static inline unsigned adaptive_burst_count_samples(uint16_t *buf, unsigned n)
// loud samples seen, handle that window.
static void adaptive_burst_end_of_window(unsigned counter)
{
if (counter > adaptive_burst_window_size / 4) {
if (counter > adaptive_samples_per_window / 4) {
// This window is loud, extend any existing run of loud windows
++adaptive_burst_runlength;
} else {
@ -365,14 +426,18 @@ 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) + adaptive_burst_block_loud_undecoded * Modes.adaptive_burst_alpha;
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) + adaptive_burst_block_loud_decoded * Modes.adaptive_burst_alpha;
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;
}

View File

@ -126,6 +126,8 @@ static void modesInitConfig(void) {
Modes.adaptive_min_gain_db = 0;
Modes.adaptive_max_gain_db = 99999;
Modes.adaptive_duty_cycle = 0.5;
Modes.adaptive_burst_control = false;
Modes.adaptive_burst_alpha = 2.0 / (5 + 1);
Modes.adaptive_burst_change_delay = 5;
@ -372,6 +374,7 @@ static void showHelp(void)
" gain scanning (seconds)\n"
"--adaptive-min-gain <g> Set gain adjustment range lower limit (dB)\n"
"--adaptive-max-gain <g> Set gain adjustment range upper limit (dB)\n"
"--adaptive-duty-cycle <p> Set adaptive gain duty cycle %% (1..100)\n"
"\n"
// ------ 80 char limit ----------------------------------------------------------|
" Network connections\n"
@ -764,6 +767,8 @@ int main(int argc, char **argv) {
Modes.adaptive_min_gain_db = atof(argv[++j]);
} else if (!strcmp(argv[j], "--adaptive-max-gain") && more) {
Modes.adaptive_max_gain_db = atof(argv[++j]);
} else if (!strcmp(argv[j], "--adaptive-duty-cycle") && more) {
Modes.adaptive_duty_cycle = atof(argv[++j]) / 100.0;
} else if (!strcmp(argv[j], "--adaptive-burst")) {
Modes.adaptive_burst_control = true;
} else if (!strcmp(argv[j], "--adaptive-burst-alpha") && more) {

View File

@ -394,6 +394,8 @@ struct _Modes { // Internal state
float adaptive_min_gain_db;
float adaptive_max_gain_db;
float adaptive_duty_cycle;
bool adaptive_burst_control;
float adaptive_burst_alpha;
unsigned adaptive_burst_change_delay;