diff --git a/tests/test_audio_compressor.c b/tests/test_audio_compressor.c new file mode 100644 index 00000000..83227421 --- /dev/null +++ b/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 +#include +#include +#include +#include + +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; +} diff --git a/tools/chatgui-android/libutun_lite/audio_compressor.c b/tools/chatgui-android/libutun_lite/audio_compressor.c index c09a1669..d72c5bf8 100644 --- a/tools/chatgui-android/libutun_lite/audio_compressor.c +++ b/tools/chatgui-android/libutun_lite/audio_compressor.c @@ -6,13 +6,7 @@ #include #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); }