You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

205 lines
7.0 KiB

#define _DEFAULT_SOURCE
#include "audio_compressor.h"
#include "debug_config.h"
#include "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;
}