From da51f3d80cf9ea90a9cdb683803191ff28922597 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 2 May 2026 10:50:50 +0000 Subject: [PATCH] test: add comprehensive test suite for wav_io.c Co-authored-by: aider (deepseek/deepseek-coder) --- test_wav_io.c | 609 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 609 insertions(+) diff --git a/test_wav_io.c b/test_wav_io.c index e69de29..900aa44 100644 --- a/test_wav_io.c +++ b/test_wav_io.c @@ -0,0 +1,609 @@ +#include "wav_io.h" +#include +#include +#include +#include + +#define TEST_FILE "test_output.wav" +#define SAMPLE_RATE 44100 +#define EPSILON 1e-6f + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST(name, expr) do { \ + if (expr) { \ + tests_passed++; \ + printf(" PASS: %s\n", name); \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s\n", name); \ + } \ +} while(0) + +static int float_close(float a, float b) { + return fabsf(a - b) < EPSILON; +} + +// Test 1: Save and load a simple float buffer (round-trip) +static void test_round_trip_float(void) { + printf("Test: Round-trip float WAV\n"); + + size_t num_samples = 1000; + float *original = (float *)malloc(num_samples * sizeof(float)); + for (size_t i = 0; i < num_samples; i++) { + original[i] = sinf(2.0f * 3.14159f * 440.0f * i / SAMPLE_RATE); + } + + int ret = save_wav_float(TEST_FILE, original, num_samples, SAMPLE_RATE); + TEST("save_wav_float returns 0", ret == 0); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load_wav_float returns 0", ret == 0); + TEST("loaded sample count matches", loaded_samples == num_samples); + TEST("loaded sample rate matches", loaded_rate == SAMPLE_RATE); + + int samples_match = 1; + for (size_t i = 0; i < num_samples; i++) { + if (!float_close(original[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("all samples match after round-trip", samples_match); + + free(original); + free(loaded); + remove(TEST_FILE); +} + +// Test 2: Save with NULL filename +static void test_save_null_filename(void) { + printf("Test: save_wav_float with NULL filename\n"); + float buf[10] = {0}; + int ret = save_wav_float(NULL, buf, 10, SAMPLE_RATE); + TEST("returns -1", ret == -1); +} + +// Test 3: Save with NULL buffer +static void test_save_null_buffer(void) { + printf("Test: save_wav_float with NULL buffer\n"); + int ret = save_wav_float("test.wav", NULL, 10, SAMPLE_RATE); + TEST("returns -1", ret == -1); +} + +// Test 4: Save with zero samples +static void test_save_zero_samples(void) { + printf("Test: save_wav_float with zero samples\n"); + float buf[10] = {0}; + int ret = save_wav_float("test.wav", buf, 0, SAMPLE_RATE); + TEST("returns -1", ret == -1); +} + +// Test 5: Load with NULL filename +static void test_load_null_filename(void) { + printf("Test: load_wav_float with NULL filename\n"); + float *buf = NULL; + size_t samples = 0; + unsigned int rate = 0; + int ret = load_wav_float(NULL, &buf, &samples, &rate); + TEST("returns -1", ret == -1); +} + +// Test 6: Load with NULL buffer pointer +static void test_load_null_buffer(void) { + printf("Test: load_wav_float with NULL buffer pointer\n"); + size_t samples = 0; + unsigned int rate = 0; + int ret = load_wav_float("test.wav", NULL, &samples, &rate); + TEST("returns -1", ret == -1); +} + +// Test 7: Load with NULL num_samples +static void test_load_null_num_samples(void) { + printf("Test: load_wav_float with NULL num_samples\n"); + float *buf = NULL; + unsigned int rate = 0; + int ret = load_wav_float("test.wav", &buf, NULL, &rate); + TEST("returns -1", ret == -1); +} + +// Test 8: Load with NULL sample_rate +static void test_load_null_sample_rate(void) { + printf("Test: load_wav_float with NULL sample_rate\n"); + float *buf = NULL; + size_t samples = 0; + int ret = load_wav_float("test.wav", &buf, &samples, NULL); + TEST("returns -1", ret == -1); +} + +// Test 9: Load non-existent file +static void test_load_nonexistent(void) { + printf("Test: load_wav_float with non-existent file\n"); + float *buf = NULL; + size_t samples = 0; + unsigned int rate = 0; + int ret = load_wav_float("nonexistent.wav", &buf, &samples, &rate); + TEST("returns -1", ret == -1); +} + +// Test 10: Save and load a large buffer +static void test_large_buffer(void) { + printf("Test: Large buffer round-trip\n"); + + size_t num_samples = 100000; + float *original = (float *)malloc(num_samples * sizeof(float)); + for (size_t i = 0; i < num_samples; i++) { + original[i] = (float)i / num_samples; + } + + int ret = save_wav_float(TEST_FILE, original, num_samples, 48000); + TEST("save returns 0", ret == 0); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches", loaded_samples == num_samples); + TEST("sample rate matches", loaded_rate == 48000); + + int samples_match = 1; + for (size_t i = 0; i < num_samples; i++) { + if (!float_close(original[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("all samples match", samples_match); + + free(original); + free(loaded); + remove(TEST_FILE); +} + +// Test 11: Save and load with different sample rates +static void test_different_sample_rates(void) { + printf("Test: Different sample rates\n"); + + float buf[] = {0.0f, 0.5f, 1.0f, -0.5f, -1.0f}; + size_t num_samples = 5; + unsigned int rates[] = {8000, 22050, 44100, 48000, 96000}; + + for (int i = 0; i < 5; i++) { + char filename[64]; + snprintf(filename, sizeof(filename), "test_rate_%u.wav", rates[i]); + + int ret = save_wav_float(filename, buf, num_samples, rates[i]); + TEST("save returns 0", ret == 0); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + ret = load_wav_float(filename, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample rate matches", loaded_rate == rates[i]); + TEST("sample count matches", loaded_samples == num_samples); + + int samples_match = 1; + for (size_t j = 0; j < num_samples; j++) { + if (!float_close(buf[j], loaded[j])) { + samples_match = 0; + break; + } + } + TEST("samples match", samples_match); + + free(loaded); + remove(filename); + } +} + +// Test 12: Save and load with negative values +static void test_negative_values(void) { + printf("Test: Negative values\n"); + + float buf[] = {-1.0f, -0.75f, -0.5f, -0.25f, 0.0f, 0.25f, 0.5f, 0.75f, 1.0f}; + size_t num_samples = 9; + + int ret = save_wav_float(TEST_FILE, buf, num_samples, SAMPLE_RATE); + TEST("save returns 0", ret == 0); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches", loaded_samples == num_samples); + + int samples_match = 1; + for (size_t i = 0; i < num_samples; i++) { + if (!float_close(buf[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("all samples match", samples_match); + + free(loaded); + remove(TEST_FILE); +} + +// Test 13: Save and load with silence (all zeros) +static void test_silence(void) { + printf("Test: Silence (all zeros)\n"); + + size_t num_samples = 100; + float *buf = (float *)calloc(num_samples, sizeof(float)); + + int ret = save_wav_float(TEST_FILE, buf, num_samples, SAMPLE_RATE); + TEST("save returns 0", ret == 0); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches", loaded_samples == num_samples); + + int all_zeros = 1; + for (size_t i = 0; i < num_samples; i++) { + if (loaded[i] != 0.0f) { + all_zeros = 0; + break; + } + } + TEST("all samples are zero", all_zeros); + + free(buf); + free(loaded); + remove(TEST_FILE); +} + +// Test 14: Load a manually crafted 16-bit PCM WAV file +static void test_load_16bit_pcm(void) { + printf("Test: Load 16-bit PCM WAV\n"); + + // Create a minimal 16-bit PCM WAV file manually + FILE *f = fopen(TEST_FILE, "wb"); + if (!f) { + printf(" SKIP: Could not create test file\n"); + return; + } + + int16_t pcm_data[] = {0, 16384, 32767, -16384, -32768}; + size_t num_samples = 5; + uint32_t data_size = num_samples * sizeof(int16_t); + uint32_t chunk_size = 36 + data_size; + + fwrite("RIFF", 1, 4, f); + unsigned char buf[4]; + buf[0] = chunk_size & 0xFF; + buf[1] = (chunk_size >> 8) & 0xFF; + buf[2] = (chunk_size >> 16) & 0xFF; + buf[3] = (chunk_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + fwrite("WAVE", 1, 4, f); + + fwrite("fmt ", 1, 4, f); + uint32_t fmt_size = 16; + buf[0] = fmt_size & 0xFF; + buf[1] = (fmt_size >> 8) & 0xFF; + buf[2] = (fmt_size >> 16) & 0xFF; + buf[3] = (fmt_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t audio_format = 1; // PCM + buf[0] = audio_format & 0xFF; + buf[1] = (audio_format >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t channels = 1; + buf[0] = channels & 0xFF; + buf[1] = (channels >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint32_t sample_rate = 44100; + buf[0] = sample_rate & 0xFF; + buf[1] = (sample_rate >> 8) & 0xFF; + buf[2] = (sample_rate >> 16) & 0xFF; + buf[3] = (sample_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint32_t byte_rate = sample_rate * sizeof(int16_t); + buf[0] = byte_rate & 0xFF; + buf[1] = (byte_rate >> 8) & 0xFF; + buf[2] = (byte_rate >> 16) & 0xFF; + buf[3] = (byte_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t block_align = sizeof(int16_t); + buf[0] = block_align & 0xFF; + buf[1] = (block_align >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t bits_per_sample = 16; + buf[0] = bits_per_sample & 0xFF; + buf[1] = (bits_per_sample >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + fwrite("data", 1, 4, f); + buf[0] = data_size & 0xFF; + buf[1] = (data_size >> 8) & 0xFF; + buf[2] = (data_size >> 16) & 0xFF; + buf[3] = (data_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + fwrite(pcm_data, sizeof(int16_t), num_samples, f); + fclose(f); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + int ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches", loaded_samples == num_samples); + TEST("sample rate matches", loaded_rate == 44100); + + // Expected float values: 0/32768, 16384/32768, 32767/32768, -16384/32768, -32768/32768 + float expected[] = {0.0f, 0.5f, 0.999969f, -0.5f, -1.0f}; + int samples_match = 1; + for (size_t i = 0; i < num_samples; i++) { + if (!float_close(expected[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("samples match expected values", samples_match); + + free(loaded); + remove(TEST_FILE); +} + +// Test 15: Load a stereo WAV file (should mix to mono) +static void test_load_stereo(void) { + printf("Test: Load stereo WAV (mixes to mono)\n"); + + // Create a stereo 16-bit PCM WAV file + FILE *f = fopen(TEST_FILE, "wb"); + if (!f) { + printf(" SKIP: Could not create test file\n"); + return; + } + + int16_t pcm_data[] = {10000, 20000, -10000, -20000, 0, 0}; // L,R pairs + size_t num_frames = 3; + uint32_t data_size = num_frames * 2 * sizeof(int16_t); + uint32_t chunk_size = 36 + data_size; + + fwrite("RIFF", 1, 4, f); + unsigned char buf[4]; + buf[0] = chunk_size & 0xFF; + buf[1] = (chunk_size >> 8) & 0xFF; + buf[2] = (chunk_size >> 16) & 0xFF; + buf[3] = (chunk_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + fwrite("WAVE", 1, 4, f); + + fwrite("fmt ", 1, 4, f); + uint32_t fmt_size = 16; + buf[0] = fmt_size & 0xFF; + buf[1] = (fmt_size >> 8) & 0xFF; + buf[2] = (fmt_size >> 16) & 0xFF; + buf[3] = (fmt_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t audio_format = 1; + buf[0] = audio_format & 0xFF; + buf[1] = (audio_format >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t channels = 2; + buf[0] = channels & 0xFF; + buf[1] = (channels >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint32_t sample_rate = 44100; + buf[0] = sample_rate & 0xFF; + buf[1] = (sample_rate >> 8) & 0xFF; + buf[2] = (sample_rate >> 16) & 0xFF; + buf[3] = (sample_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint32_t byte_rate = sample_rate * 2 * sizeof(int16_t); + buf[0] = byte_rate & 0xFF; + buf[1] = (byte_rate >> 8) & 0xFF; + buf[2] = (byte_rate >> 16) & 0xFF; + buf[3] = (byte_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t block_align = 2 * sizeof(int16_t); + buf[0] = block_align & 0xFF; + buf[1] = (block_align >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t bits_per_sample = 16; + buf[0] = bits_per_sample & 0xFF; + buf[1] = (bits_per_sample >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + fwrite("data", 1, 4, f); + buf[0] = data_size & 0xFF; + buf[1] = (data_size >> 8) & 0xFF; + buf[2] = (data_size >> 16) & 0xFF; + buf[3] = (data_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + fwrite(pcm_data, sizeof(int16_t), num_frames * 2, f); + fclose(f); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + int ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches frames", loaded_samples == num_frames); + + // Expected: (10000 + 20000)/2 / 32768 = 0.457764, (-10000 + -20000)/2 / 32768 = -0.457764, (0+0)/2 = 0 + float expected[] = {0.457764f, -0.457764f, 0.0f}; + int samples_match = 1; + for (size_t i = 0; i < num_frames; i++) { + if (!float_close(expected[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("stereo mixed to mono correctly", samples_match); + + free(loaded); + remove(TEST_FILE); +} + +// Test 16: Load a file with extra chunks (like LIST) +static void test_load_with_extra_chunks(void) { + printf("Test: Load WAV with extra chunks\n"); + + // Create a WAV with a LIST chunk before data + FILE *f = fopen(TEST_FILE, "wb"); + if (!f) { + printf(" SKIP: Could not create test file\n"); + return; + } + + float samples[] = {0.5f, -0.5f, 0.25f, -0.25f}; + size_t num_samples = 4; + uint32_t data_size = num_samples * sizeof(float); + uint32_t list_size = 8; // "INFO" + padding + uint32_t chunk_size = 36 + list_size + data_size; + + fwrite("RIFF", 1, 4, f); + unsigned char buf[4]; + buf[0] = chunk_size & 0xFF; + buf[1] = (chunk_size >> 8) & 0xFF; + buf[2] = (chunk_size >> 16) & 0xFF; + buf[3] = (chunk_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + fwrite("WAVE", 1, 4, f); + + // fmt chunk + fwrite("fmt ", 1, 4, f); + uint32_t fmt_size = 16; + buf[0] = fmt_size & 0xFF; + buf[1] = (fmt_size >> 8) & 0xFF; + buf[2] = (fmt_size >> 16) & 0xFF; + buf[3] = (fmt_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t audio_format = 3; + buf[0] = audio_format & 0xFF; + buf[1] = (audio_format >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t channels = 1; + buf[0] = channels & 0xFF; + buf[1] = (channels >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint32_t sample_rate = 44100; + buf[0] = sample_rate & 0xFF; + buf[1] = (sample_rate >> 8) & 0xFF; + buf[2] = (sample_rate >> 16) & 0xFF; + buf[3] = (sample_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint32_t byte_rate = sample_rate * sizeof(float); + buf[0] = byte_rate & 0xFF; + buf[1] = (byte_rate >> 8) & 0xFF; + buf[2] = (byte_rate >> 16) & 0xFF; + buf[3] = (byte_rate >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + uint16_t block_align = sizeof(float); + buf[0] = block_align & 0xFF; + buf[1] = (block_align >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + uint16_t bits_per_sample = 32; + buf[0] = bits_per_sample & 0xFF; + buf[1] = (bits_per_sample >> 8) & 0xFF; + fwrite(buf, 1, 2, f); + + // LIST chunk + fwrite("LIST", 1, 4, f); + buf[0] = list_size & 0xFF; + buf[1] = (list_size >> 8) & 0xFF; + buf[2] = (list_size >> 16) & 0xFF; + buf[3] = (list_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + fwrite("INFO", 1, 4, f); + fwrite("\0\0\0\0", 1, 4, f); // padding + + // data chunk + fwrite("data", 1, 4, f); + buf[0] = data_size & 0xFF; + buf[1] = (data_size >> 8) & 0xFF; + buf[2] = (data_size >> 16) & 0xFF; + buf[3] = (data_size >> 24) & 0xFF; + fwrite(buf, 1, 4, f); + + fwrite(samples, sizeof(float), num_samples, f); + fclose(f); + + float *loaded = NULL; + size_t loaded_samples = 0; + unsigned int loaded_rate = 0; + int ret = load_wav_float(TEST_FILE, &loaded, &loaded_samples, &loaded_rate); + TEST("load returns 0", ret == 0); + TEST("sample count matches", loaded_samples == num_samples); + + int samples_match = 1; + for (size_t i = 0; i < num_samples; i++) { + if (!float_close(samples[i], loaded[i])) { + samples_match = 0; + break; + } + } + TEST("samples match despite extra chunks", samples_match); + + free(loaded); + remove(TEST_FILE); +} + +int main(void) { + printf("=== WAV I/O Tests ===\n\n"); + + test_round_trip_float(); + printf("\n"); + test_save_null_filename(); + test_save_null_buffer(); + test_save_zero_samples(); + printf("\n"); + test_load_null_filename(); + test_load_null_buffer(); + test_load_null_num_samples(); + test_load_null_sample_rate(); + test_load_nonexistent(); + printf("\n"); + test_large_buffer(); + printf("\n"); + test_different_sample_rates(); + printf("\n"); + test_negative_values(); + printf("\n"); + test_silence(); + printf("\n"); + test_load_16bit_pcm(); + printf("\n"); + test_load_stereo(); + printf("\n"); + test_load_with_extra_chunks(); + + printf("\n=== Results ===\n"); + printf("Passed: %d\n", tests_passed); + printf("Failed: %d\n", tests_failed); + + return tests_failed > 0 ? 1 : 0; +}