Browse Source

fix audio_compressor: flat array instead of ring buffer, +unit test (411 asserts)

Ring buffer (pending_head/pending_tail/cap) replaced with simple flat array (pending_count/cap).
u_realloc copies linearly but ring buffer needs memmove for wrap-around — mismatch caused
SIGSEGV write to 0x10 on ARM64. Flat array: append to end, process from [0], memmove down.
Performance unchanged (max ~15 blocks).

Tests: create/destroy, passthrough disabled, basic push+flush, silence, reset, stress (200 cycles)
topo_upd
evgeny 2 months ago
parent
commit
d22eac55f7
  1. 205
      tests/test_audio_compressor.c
  2. 119
      tools/chatgui-android/libutun_lite/audio_compressor.c

205
tests/test_audio_compressor.c

@ -0,0 +1,205 @@
#define _DEFAULT_SOURCE
#include "audio_compressor.h"
#include "../../../lib/debug_config.h"
#include "../../../lib/mem.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
static int tests_run = 0, tests_passed = 0;
#define TEST(expr, msg) do { \
tests_run++; \
if (expr) { tests_passed++; } \
else { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); } \
} while(0)
/* ── helpers ── */
static void fill_sine(int16_t* buf, size_t n, int freq, int sample_rate) {
for (size_t i = 0; i < n; i++)
buf[i] = (int16_t)(14000.0f * sinf(2.0f * (float)M_PI * (float)freq * (float)i / (float)sample_rate));
}
static void fill_silence(int16_t* buf, size_t n) {
memset(buf, 0, n * sizeof(int16_t));
}
/* ── tests ── */
static void test_create_destroy(void) {
struct audio_compressor* ac = audio_compressor_create();
TEST(ac != NULL, "create");
audio_compressor_configure(ac, &(audio_compressor_config_t){0});
audio_compressor_reset(ac);
audio_compressor_destroy(ac);
/* check no leaks via u_get_allocated_count */
size_t unfreed = u_get_allocated_count();
TEST(unfreed == 0, "no leaks after destroy");
if (unfreed > 0) {
fprintf(stderr, " unfreed blocks: %zu\n", unfreed);
u_report_unfreed_blocks();
}
}
static void test_disabled_passthrough(void) {
struct audio_compressor* ac = audio_compressor_create();
audio_compressor_configure(ac, &(audio_compressor_config_t){0});
audio_compressor_set_enabled(ac, 0);
int16_t in[960]; fill_sine(in, 960, 440, 48000);
audio_compressor_push(ac, in, 960);
const int16_t* out = audio_compressor_output(ac);
size_t sz = audio_compressor_output_size(ac);
TEST(sz == 960, "passthrough size");
int match = 1;
for (size_t i = 0; i < sz; i++) { if (out[i] != in[i]) { match = 0; break; } }
TEST(match, "passthrough data unchanged");
audio_compressor_destroy(ac);
}
static void test_basic_push_flush(void) {
struct audio_compressor* ac = audio_compressor_create();
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1;
cfg.block_duration_ms = 20; cfg.lookback_ms = 200; cfg.lookahead_ms = 100;
cfg.max_gain_db = 30.0f; cfg.rise_rate_per_500ms = 2.0f; cfg.target_level = 0.25f;
audio_compressor_configure(ac, &cfg);
int16_t in[960];
/* push 20 blocks (400ms) — enough to fill lookback+lookahead and flush */
for (int b = 0; b < 20; b++) {
fill_sine(in, 960, 440, 48000);
audio_compressor_push(ac, in, 960);
}
audio_compressor_flush(ac);
const int16_t* out = audio_compressor_output(ac);
size_t sz = audio_compressor_output_size(ac);
TEST(sz > 0, "output not empty");
TEST(sz >= 960 * 10, "output has reasonable size");
/* check not all silence */
float sum_sq = 0.0f;
for (size_t i = 0; i < sz; i++) { float v = (float)out[i] / 32768.0f; sum_sq += v * v; }
float rms = sqrtf(sum_sq / (float)sz);
TEST(rms > 0.001f, "output not silence");
audio_compressor_destroy(ac);
}
static void test_silence_in_silence_out(void) {
struct audio_compressor* ac = audio_compressor_create();
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1;
cfg.block_duration_ms = 20; cfg.lookback_ms = 200; cfg.lookahead_ms = 100;
cfg.max_gain_db = 30.0f; cfg.rise_rate_per_500ms = 2.0f; cfg.target_level = 0.25f;
audio_compressor_configure(ac, &cfg);
int16_t in[960]; fill_silence(in, 960);
for (int b = 0; b < 20; b++)
audio_compressor_push(ac, in, 960);
audio_compressor_flush(ac);
const int16_t* out = audio_compressor_output(ac);
size_t sz = audio_compressor_output_size(ac);
TEST(sz > 0, "output not empty (silence)");
/* silence should still be silence (envelope ≈ 0.0001, gain limited) */
float sum_sq = 0.0f;
for (size_t i = 0; i < sz; i++) { float v = (float)out[i] / 32768.0f; sum_sq += v * v; }
float rms = sqrtf(sum_sq / (float)sz);
TEST(rms < 0.01f, "silence stays near zero");
audio_compressor_destroy(ac);
}
static void test_reset(void) {
struct audio_compressor* ac = audio_compressor_create();
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1;
cfg.block_duration_ms = 20; cfg.lookback_ms = 200; cfg.lookahead_ms = 100;
cfg.max_gain_db = 30.0f; cfg.rise_rate_per_500ms = 2.0f; cfg.target_level = 0.25f;
audio_compressor_configure(ac, &cfg);
int16_t in[960];
fill_sine(in, 960, 440, 48000);
for (int b = 0; b < 15; b++) audio_compressor_push(ac, in, 960);
audio_compressor_flush(ac);
audio_compressor_reset(ac);
fill_sine(in, 960, 880, 48000);
for (int b = 0; b < 15; b++) audio_compressor_push(ac, in, 960);
audio_compressor_flush(ac);
const int16_t* out = audio_compressor_output(ac);
size_t sz = audio_compressor_output_size(ac);
TEST(sz > 960 * 10, "output after reset has data");
float sum_sq = 0.0f;
for (size_t i = 0; i < sz; i++) { float v = (float)out[i] / 32768.0f; sum_sq += v * v; }
float rms = sqrtf(sum_sq / (float)sz);
TEST(rms > 0.001f, "output after reset not silence");
audio_compressor_destroy(ac);
}
static void test_stress(void) {
srand((unsigned)time(NULL));
for (int cycle = 0; cycle < 200; cycle++) {
struct audio_compressor* ac = audio_compressor_create();
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1;
cfg.block_duration_ms = 20; cfg.lookback_ms = (rand() % 200) + 50;
cfg.lookahead_ms = (rand() % 100) + 20;
cfg.max_gain_db = (float)(rand() % 31);
cfg.rise_rate_per_500ms = 1.1f + (float)(rand() % 90) / 10.0f;
cfg.target_level = 0.1f + (float)(rand() % 30) / 100.0f;
audio_compressor_configure(ac, &cfg);
int16_t in[960];
int blocks = (rand() % 30) + 5;
for (int b = 0; b < blocks; b++) {
for (int i = 0; i < 960; i++)
in[i] = (int16_t)((rand() % 28000) - 14000);
audio_compressor_push(ac, in, 960);
}
audio_compressor_flush(ac);
audio_compressor_reset(ac);
blocks = (rand() % 10) + 1;
for (int b = 0; b < blocks; b++) {
for (int i = 0; i < 960; i++)
in[i] = (int16_t)((rand() % 2000) - 1000);
audio_compressor_push(ac, in, 960);
}
audio_compressor_flush(ac);
const int16_t* out = audio_compressor_output(ac);
size_t sz = audio_compressor_output_size(ac);
TEST(sz > 0, "stress output not empty");
audio_compressor_destroy(ac);
size_t unfreed = u_get_allocated_count();
TEST(unfreed == 0, "stress no leaks");
if (unfreed > 0) break;
}
}
int main(void) {
debug_set_level(DEBUG_LEVEL_NONE);
test_create_destroy();
test_disabled_passthrough();
test_basic_push_flush();
test_silence_in_silence_out();
test_reset();
test_stress();
fprintf(stderr, "\n%d/%d tests passed\n", tests_passed, tests_run);
return tests_passed == tests_run ? 0 : 1;
}

119
tools/chatgui-android/libutun_lite/audio_compressor.c

@ -6,13 +6,7 @@
#include <stdlib.h>
#define BLOCK_LEVELS_CHUNK 64
#define PENDING_CHUNK 32
struct pending_block {
int16_t* samples;
size_t count;
size_t level_index;
};
#define PENDING_CHUNK 32
struct audio_compressor {
audio_compressor_config_t cfg;
@ -29,9 +23,12 @@ struct audio_compressor {
size_t block_levels_count;
size_t block_levels_cap;
struct pending_block* pending;
size_t pending_head;
size_t pending_tail;
struct pending_block {
int16_t* samples;
size_t count;
size_t level_index;
}* pending;
size_t pending_count;
size_t pending_cap;
int16_t* accumulator;
@ -58,7 +55,7 @@ struct audio_compressor* audio_compressor_create(void) {
void audio_compressor_destroy(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = ac->pending_head; i != ac->pending_tail; i = (i + 1) % ac->pending_cap)
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
u_free(ac->pending);
u_free(ac->block_levels);
@ -93,10 +90,9 @@ void audio_compressor_configure(struct audio_compressor* ac, const audio_compres
void audio_compressor_reset(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = ac->pending_head; i != ac->pending_tail; i = (i + 1) % ac->pending_cap)
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
ac->pending_head = 0;
ac->pending_tail = 0;
ac->pending_count = 0;
ac->block_levels_count = 0;
ac->accum_count = 0;
ac->output_size = 0;
@ -157,6 +153,15 @@ static void add_block_level(struct audio_compressor* ac, float level) {
ac->block_levels[ac->block_levels_count++] = level;
}
static void ensure_pending_cap(struct audio_compressor* ac) {
if (ac->pending_count < ac->pending_cap) return;
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
if (!tmp) return;
ac->pending = tmp;
ac->pending_cap = new_cap;
}
static void process_pending_block(struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
@ -188,46 +193,25 @@ void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples,
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
struct pending_block pb;
pb.samples = ac->accumulator;
pb.count = ac->accum_count;
pb.level_index = ac->block_levels_count - 1;
/* ring buffer enqueue */
if ((ac->pending_tail + 1) % ac->pending_cap == ac->pending_head) {
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp2 = u_realloc(ac->pending, new_cap * sizeof(struct pending_block));
if (!tmp2) { u_free(pb.samples); return; }
if (ac->pending && ac->pending_head > ac->pending_tail) {
size_t wrap = ac->pending_cap - ac->pending_head;
memmove(tmp2 + new_cap - wrap, tmp2 + ac->pending_head, wrap * sizeof(struct pending_block));
ac->pending_head = new_cap - wrap;
}
ac->pending = tmp2;
ac->pending_cap = new_cap;
}
ac->pending[ac->pending_tail] = pb;
ac->pending_tail = (ac->pending_tail + 1) % ac->pending_cap;
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
}
while (ac->pending_head != ac->pending_tail) {
size_t pending_cnt = (ac->pending_tail >= ac->pending_head)
? (ac->pending_tail - ac->pending_head)
: (ac->pending_cap - ac->pending_head + ac->pending_tail);
if (pending_cnt <= (size_t)ac->lookahead_blocks) break;
while (ac->pending_count > (size_t)ac->lookahead_blocks)
process_pending_block(ac);
}
}
static void process_pending_block(struct audio_compressor* ac) {
if (ac->pending_head == ac->pending_tail) return;
if (ac->pending_count == 0) return;
struct pending_block* block = &ac->pending[ac->pending_head];
size_t idx = block->level_index;
size_t idx = ac->pending[0].level_index;
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
size_t win_end = idx + (size_t)ac->lookahead_blocks;
@ -258,16 +242,19 @@ static void process_pending_block(struct audio_compressor* ac) {
}
ac->dbg_counter++;
for (size_t i = 0; i < block->count; i++) {
float v = (float)block->samples[i] * ac->gain_smoothed;
for (size_t i = 0; i < ac->pending[0].count; i++) {
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
if (v > 32767.0f) v = 32767.0f;
if (v < -32768.0f) v = -32768.0f;
block->samples[i] = (int16_t)(int)v;
ac->pending[0].samples[i] = (int16_t)(int)v;
}
append_output(ac, block->samples, block->count);
u_free(block->samples);
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
u_free(ac->pending[0].samples);
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
/* shift remaining pending blocks down */
if (ac->pending_count > 1)
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
ac->pending_count--;
}
void audio_compressor_flush(struct audio_compressor* ac) {
@ -278,37 +265,21 @@ void audio_compressor_flush(struct audio_compressor* ac) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
struct pending_block pb;
pb.samples = ac->accumulator;
pb.count = ac->accum_count;
pb.level_index = ac->block_levels_count - 1;
if ((ac->pending_tail + 1) % ac->pending_cap == ac->pending_head) {
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(struct pending_block));
if (!tmp) { u_free(pb.samples); return; }
if (ac->pending && ac->pending_head > ac->pending_tail) {
size_t wrap = ac->pending_cap - ac->pending_head;
memmove(tmp + new_cap - wrap, tmp + ac->pending_head, wrap * sizeof(struct pending_block));
ac->pending_head = new_cap - wrap;
}
ac->pending = tmp;
ac->pending_cap = new_cap;
}
ac->pending[ac->pending_tail] = pb;
ac->pending_tail = (ac->pending_tail + 1) % ac->pending_cap;
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
int remain = (int)((ac->pending_tail >= ac->pending_head)
? (ac->pending_tail - ac->pending_head)
: (ac->pending_cap - ac->pending_head + ac->pending_tail));
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %d pending blocks (gainSmoothed=%.1fdB)",
remain, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
if (ac->pending_count > 0)
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
while (ac->pending_head != ac->pending_tail)
while (ac->pending_count > 0)
process_pending_block(ac);
}

Loading…
Cancel
Save