summaryrefslogtreecommitdiff
path: root/src/tests/audio/test_wav_dump.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-09 20:27:04 +0100
committerskal <pascal.massimino@gmail.com>2026-02-09 20:27:04 +0100
commiteff8d43479e7704df65fae2a80eefa787213f502 (patch)
tree76f2fb8fe8d3db2c15179449df2cf12f7f54e0bf /src/tests/audio/test_wav_dump.cc
parent12378b1b7e9091ba59895b4360b2fa959180a56a (diff)
refactor: Reorganize tests into subsystem subdirectories
Restructured test suite for better organization and targeted testing: **Structure:** - src/tests/audio/ - 15 audio system tests - src/tests/gpu/ - 12 GPU/shader tests - src/tests/3d/ - 6 3D rendering tests - src/tests/assets/ - 2 asset system tests - src/tests/util/ - 3 utility tests - src/tests/common/ - 3 shared test helpers - src/tests/scripts/ - 2 bash test scripts (moved conceptually, not physically) **CMake changes:** - Updated add_demo_test macro to accept LABEL parameter - Applied CTest labels to all 36 tests for subsystem filtering - Updated all test file paths in CMakeLists.txt - Fixed common helper paths (webgpu_test_fixture, etc.) - Added custom targets for subsystem testing: - run_audio_tests, run_gpu_tests, run_3d_tests - run_assets_tests, run_util_tests, run_all_tests **Include path updates:** - Fixed relative includes in GPU tests to reference ../common/ **Documentation:** - Updated doc/HOWTO.md with subsystem test commands - Updated doc/CONTRIBUTING.md with new test organization - Updated scripts/check_all.sh to reflect new structure **Verification:** - All 36 tests passing (100%) - ctest -L <subsystem> filters work correctly - make run_<subsystem>_tests targets functional - scripts/check_all.sh passes Backward compatible: make test and ctest continue to work unchanged. handoff(Gemini): Test reorganization complete. 36/36 tests passing.
Diffstat (limited to 'src/tests/audio/test_wav_dump.cc')
-rw-r--r--src/tests/audio/test_wav_dump.cc309
1 files changed, 309 insertions, 0 deletions
diff --git a/src/tests/audio/test_wav_dump.cc b/src/tests/audio/test_wav_dump.cc
new file mode 100644
index 0000000..eb14652
--- /dev/null
+++ b/src/tests/audio/test_wav_dump.cc
@@ -0,0 +1,309 @@
+// This file is part of the 64k demo project.
+// Regression test for WAV dump backend to prevent format mismatches.
+
+#include "audio/audio.h"
+#include "audio/audio_engine.h"
+#include "audio/backend/wav_dump_backend.h"
+#include "audio/ring_buffer.h"
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <vector>
+
+#if !defined(STRIP_ALL)
+
+// Helper to read WAV header and verify format
+struct WavHeader {
+ char riff[4]; // "RIFF"
+ uint32_t chunk_size; // File size - 8
+ char wave[4]; // "WAVE"
+ char fmt[4]; // "fmt "
+ uint32_t subchunk1_size;
+ uint16_t audio_format; // 1 = PCM
+ uint16_t num_channels;
+ uint32_t sample_rate;
+ uint32_t byte_rate;
+ uint16_t block_align;
+ uint16_t bits_per_sample;
+ char data[4]; // "data"
+ uint32_t data_size;
+};
+
+void test_wav_format_matches_live_audio() {
+ printf("Test: WAV format matches live audio output...\n");
+
+ const char* test_file = "test_format.wav";
+
+ // Initialize audio system
+ audio_init();
+
+ // Initialize AudioEngine
+ AudioEngine engine;
+ engine.init();
+
+ // Create WAV dump backend
+ WavDumpBackend wav_backend;
+ wav_backend.set_output_file(test_file);
+ wav_backend.init();
+ wav_backend.start();
+
+ // Simulate 2 seconds of audio rendering (frontend-driven)
+ const float duration = 2.0f;
+ const float update_dt = 1.0f / 60.0f;
+ const int frames_per_update = (int)(32000 * update_dt);
+ const int samples_per_update = frames_per_update * 2; // Stereo
+
+ AudioRingBuffer* ring_buffer = audio_get_ring_buffer();
+ std::vector<float> chunk_buffer(samples_per_update);
+
+ float music_time = 0.0f;
+ for (float t = 0.0f; t < duration; t += update_dt) {
+ // Update audio engine (triggers patterns)
+ engine.update(music_time, update_dt);
+ music_time += update_dt;
+
+ // Render audio ahead
+ audio_render_ahead(music_time, update_dt);
+
+ // Read from ring buffer
+ if (ring_buffer != nullptr) {
+ ring_buffer->read(chunk_buffer.data(), samples_per_update);
+ }
+
+ // Write to WAV file
+ wav_backend.write_audio(chunk_buffer.data(), samples_per_update);
+ }
+
+ // Shutdown
+ wav_backend.shutdown();
+ engine.shutdown();
+ audio_shutdown();
+
+ // Read and verify WAV header
+ FILE* f = fopen(test_file, "rb");
+ assert(f != nullptr);
+
+ WavHeader header;
+ size_t bytes_read = fread(&header, 1, sizeof(WavHeader), f);
+ assert(bytes_read == sizeof(WavHeader));
+
+ // Verify RIFF header
+ assert(memcmp(header.riff, "RIFF", 4) == 0);
+ assert(memcmp(header.wave, "WAVE", 4) == 0);
+ assert(memcmp(header.fmt, "fmt ", 4) == 0);
+ assert(memcmp(header.data, "data", 4) == 0);
+
+ // CRITICAL: Verify stereo format (matches miniaudio config)
+ printf(" Checking num_channels...\n");
+ assert(header.num_channels == 2); // MUST be stereo!
+
+ // Verify sample rate matches miniaudio
+ printf(" Checking sample_rate...\n");
+ assert(header.sample_rate == 32000);
+
+ // Verify bit depth
+ printf(" Checking bits_per_sample...\n");
+ assert(header.bits_per_sample == 16);
+
+ // Verify audio format is PCM
+ printf(" Checking audio_format...\n");
+ assert(header.audio_format == 1); // PCM
+
+ // Verify calculated values
+ printf(" Checking byte_rate...\n");
+ const uint32_t expected_byte_rate =
+ header.sample_rate * header.num_channels * (header.bits_per_sample / 8);
+ assert(header.byte_rate == expected_byte_rate);
+
+ printf(" Checking block_align...\n");
+ const uint16_t expected_block_align =
+ header.num_channels * (header.bits_per_sample / 8);
+ assert(header.block_align == expected_block_align);
+
+ // Verify data size is reasonable (2 seconds of audio)
+ printf(" Checking data_size...\n");
+ const uint32_t bytes_per_sample = header.bits_per_sample / 8;
+ const uint32_t expected_bytes_per_sec =
+ header.sample_rate * header.num_channels * bytes_per_sample;
+ const uint32_t expected_size_2s = expected_bytes_per_sec * 2;
+
+ printf(" Data size: %u bytes (expected ~%u bytes for 2s)\n",
+ header.data_size, expected_size_2s);
+
+ // Be lenient: allow 1.5-2.5 seconds worth of data
+ const uint32_t expected_min_size = expected_bytes_per_sec * 1.5;
+ const uint32_t expected_max_size = expected_bytes_per_sec * 2.5;
+
+ // For now, accept if stereo format is correct (main regression test goal)
+ if (header.data_size < expected_min_size ||
+ header.data_size > expected_max_size) {
+ printf(" WARNING: Data size outside expected range\n");
+ // Don't fail on this for now - stereo format is the critical check
+ }
+
+ // Verify file contains actual audio data (not all zeros)
+ fseek(f, sizeof(WavHeader), SEEK_SET);
+ int16_t samples[1000];
+ size_t samples_read = fread(samples, sizeof(int16_t), 1000, f);
+ assert(samples_read == 1000);
+
+ int non_zero_count = 0;
+ for (int i = 0; i < 1000; ++i) {
+ if (samples[i] != 0) {
+ non_zero_count++;
+ }
+ }
+
+ printf(" Checking for actual audio data...\n");
+ printf(" Non-zero samples: %d / 1000\n", non_zero_count);
+ assert(non_zero_count > 100); // Should have plenty of non-zero samples
+
+ fclose(f);
+
+ // Clean up test file
+ remove(test_file);
+
+ printf(" ✓ WAV format verified: stereo, 32kHz, 16-bit PCM\n");
+ printf(" ✓ Matches live audio output configuration\n");
+ printf(" ✓ Backend is passive (frontend-driven)\n");
+}
+
+void test_wav_stereo_buffer_size() {
+ printf("Test: WAV buffer handles stereo correctly...\n");
+
+ // This test verifies that the buffer size calculations are correct
+ // for stereo audio (frames * 2 samples per frame)
+
+ const int sample_rate = 32000;
+ const float update_dt = 1.0f / 60.0f;
+ const int frames_per_update = (int)(sample_rate * update_dt); // ~533
+ const int samples_per_update = frames_per_update * 2; // ~1066 (stereo)
+
+ printf(" Update rate: 60 Hz\n");
+ printf(" Frames per update: %d\n", frames_per_update);
+ printf(" Samples per update: %d (stereo)\n", samples_per_update);
+
+ // Verify calculations
+ assert(frames_per_update > 500 && frames_per_update < 550);
+ assert(samples_per_update == frames_per_update * 2);
+
+ printf(" ✓ Buffer size calculations correct for stereo\n");
+}
+
+void test_clipping_detection() {
+ printf("Test: Clipping detection and reporting...\n");
+
+ const char* test_file = "test_clipping.wav";
+
+ audio_init();
+ AudioEngine engine;
+ engine.init();
+
+ WavDumpBackend wav_backend;
+ wav_backend.set_output_file(test_file);
+ wav_backend.init();
+ wav_backend.start();
+
+ // Create test samples with intentional clipping
+ const int num_samples = 1000;
+ float test_samples[1000];
+
+ // Mix of normal and clipped samples
+ for (int i = 0; i < num_samples; ++i) {
+ if (i % 10 == 0) {
+ test_samples[i] = 1.5f; // Clipped high
+ } else if (i % 10 == 1) {
+ test_samples[i] = -1.2f; // Clipped low
+ } else {
+ test_samples[i] = 0.5f; // Normal
+ }
+ }
+
+ // Write samples
+ wav_backend.write_audio(test_samples, num_samples);
+
+ // Verify clipping was detected (20% of samples should be clipped)
+ const size_t clipped = wav_backend.get_clipped_samples();
+ assert(clipped == 200); // 10% + 10% = 20% of 1000
+
+ printf(" Detected %zu clipped samples (expected 200)\n", clipped);
+
+ wav_backend.shutdown();
+ engine.shutdown();
+ audio_shutdown();
+
+ // Clean up
+ remove(test_file);
+
+ printf(" ✓ Clipping detection works correctly\n");
+}
+
+void test_invalid_file_paths() {
+ printf("Test: Error handling for invalid file paths...\n");
+
+ // Test 1: Null filename (should handle gracefully)
+ {
+ WavDumpBackend wav_backend;
+ wav_backend.set_output_file(nullptr);
+ wav_backend.init(); // Should print error but not crash
+
+ // Verify file didn't open
+ float samples[10] = {0.5f};
+ wav_backend.write_audio(samples, 10); // Should do nothing
+
+ assert(wav_backend.get_samples_written() == 0);
+ wav_backend.shutdown();
+
+ printf(" ✓ Null filename handled gracefully\n");
+ }
+
+ // Test 2: Invalid directory path
+ {
+ WavDumpBackend wav_backend;
+ wav_backend.set_output_file("/nonexistent/directory/test.wav");
+ wav_backend.init(); // Should print error but not crash
+
+ float samples[10] = {0.5f};
+ wav_backend.write_audio(samples, 10); // Should do nothing
+
+ assert(wav_backend.get_samples_written() == 0);
+ wav_backend.shutdown();
+
+ printf(" ✓ Invalid directory path handled gracefully\n");
+ }
+
+ // Test 3: Read-only location (permissions error)
+ {
+ WavDumpBackend wav_backend;
+ wav_backend.set_output_file(
+ "/test.wav"); // Root directory (no write permission)
+ wav_backend.init(); // Should print error but not crash
+
+ float samples[10] = {0.5f};
+ wav_backend.write_audio(samples, 10); // Should do nothing
+
+ assert(wav_backend.get_samples_written() == 0);
+ wav_backend.shutdown();
+
+ printf(" ✓ Permission denied handled gracefully\n");
+ }
+
+ printf(" ✓ All error cases handled without crashes\n");
+}
+
+#endif /* !defined(STRIP_ALL) */
+
+int main() {
+#if !defined(STRIP_ALL)
+ printf("Running WAV Dump Backend tests...\n\n");
+ test_wav_format_matches_live_audio();
+ test_wav_stereo_buffer_size();
+ test_clipping_detection();
+ test_invalid_file_paths();
+ printf("\n✅ All WAV Dump tests PASSED\n");
+ return 0;
+#else
+ printf("WAV Dump tests skipped (STRIP_ALL enabled)\n");
+ return 0;
+#endif /* !defined(STRIP_ALL) */
+}