Browse Source
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
2 changed files with 250 additions and 74 deletions
@ -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; |
||||||
|
} |
||||||
Loading…
Reference in new issue