diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-09 20:27:04 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-09 20:27:04 +0100 |
| commit | eff8d43479e7704df65fae2a80eefa787213f502 (patch) | |
| tree | 76f2fb8fe8d3db2c15179449df2cf12f7f54e0bf /src/tests/audio | |
| parent | 12378b1b7e9091ba59895b4360b2fa959180a56a (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')
| -rw-r--r-- | src/tests/audio/test_audio_backend.cc | 130 | ||||
| -rw-r--r-- | src/tests/audio/test_audio_engine.cc | 182 | ||||
| -rw-r--r-- | src/tests/audio/test_audio_gen.cc | 97 | ||||
| -rw-r--r-- | src/tests/audio/test_dct.cc | 44 | ||||
| -rw-r--r-- | src/tests/audio/test_fft.cc | 229 | ||||
| -rw-r--r-- | src/tests/audio/test_jittered_audio.cc | 161 | ||||
| -rw-r--r-- | src/tests/audio/test_mock_backend.cc | 215 | ||||
| -rw-r--r-- | src/tests/audio/test_silent_backend.cc | 211 | ||||
| -rw-r--r-- | src/tests/audio/test_spectral_brush.cc | 243 | ||||
| -rw-r--r-- | src/tests/audio/test_synth.cc | 113 | ||||
| -rw-r--r-- | src/tests/audio/test_tracker.cc | 73 | ||||
| -rw-r--r-- | src/tests/audio/test_tracker_timing.cc | 309 | ||||
| -rw-r--r-- | src/tests/audio/test_variable_tempo.cc | 291 | ||||
| -rw-r--r-- | src/tests/audio/test_wav_dump.cc | 309 | ||||
| -rw-r--r-- | src/tests/audio/test_window.cc | 28 |
15 files changed, 2635 insertions, 0 deletions
diff --git a/src/tests/audio/test_audio_backend.cc b/src/tests/audio/test_audio_backend.cc new file mode 100644 index 0000000..6a748aa --- /dev/null +++ b/src/tests/audio/test_audio_backend.cc @@ -0,0 +1,130 @@ +// This file is part of the 64k demo project. +// It tests the audio backend abstraction layer. +// Verifies backend injection and event hooks work correctly. + +#include "audio/audio.h" +#include "audio/audio_backend.h" +#include "audio/synth.h" +#include <assert.h> +#include <stdio.h> +#include <vector> + +#if !defined(STRIP_ALL) + +// Simple test backend that records events +class TestBackend : public AudioBackend { + public: + struct Event { + float timestamp; + int spectrogram_id; + float volume; + float pan; + }; + + std::vector<Event> events; + bool init_called = false; + bool start_called = false; + bool shutdown_called = false; + + void init() override { + init_called = true; + } + + void start() override { + start_called = true; + } + + void shutdown() override { + shutdown_called = true; + } + + float get_realtime_peak() override { + // Test backend: return synthetic peak + return 0.5f; + } + + void on_voice_triggered(float timestamp, int spectrogram_id, float volume, + float pan) override { + events.push_back({timestamp, spectrogram_id, volume, pan}); + } +}; + +void test_backend_injection() { + TestBackend backend; + + // Inject test backend before audio_init + audio_set_backend(&backend); + + audio_init(); + assert(backend.init_called); + + audio_start(); + assert(backend.start_called); + + audio_shutdown(); + assert(backend.shutdown_called); + + printf("Backend injection test PASSED\n"); +} + +void test_event_recording() { + TestBackend backend; + audio_set_backend(&backend); + + synth_init(); + + // Create a dummy spectrogram + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + int id = synth_register_spectrogram(&spec); + + // Trigger a voice + synth_trigger_voice(id, 0.8f, -0.5f); + + // Render some frames to advance time + float output[1024] = {0}; + synth_render(output, 256); // ~0.008 sec at 32kHz + + // Verify event was recorded + assert(backend.events.size() == 1); + assert(backend.events[0].spectrogram_id == id); + assert(backend.events[0].volume == 0.8f); + assert(backend.events[0].pan == -0.5f); + assert(backend.events[0].timestamp == 0.0f); // Triggered before any render + + // Trigger another voice after rendering + synth_trigger_voice(id, 1.0f, 0.0f); + + assert(backend.events.size() == 2); + assert(backend.events[1].timestamp > 0.0f); // Should be > 0 now + + printf("Event recording test PASSED\n"); +} + +void test_default_backend() { + // Reset backend to nullptr to test default + audio_set_backend(nullptr); + + // This should use MiniaudioBackend by default + audio_init(); + audio_start(); + audio_shutdown(); + + printf("Default backend test PASSED\n"); +} + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running Audio Backend tests...\n"); + test_backend_injection(); + test_event_recording(); + test_default_backend(); + printf("All Audio Backend tests PASSED\n"); + return 0; +#else + printf("Audio Backend tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} diff --git a/src/tests/audio/test_audio_engine.cc b/src/tests/audio/test_audio_engine.cc new file mode 100644 index 0000000..3b29dcd --- /dev/null +++ b/src/tests/audio/test_audio_engine.cc @@ -0,0 +1,182 @@ +// This file is part of the 64k demo project. +// Unit tests for AudioEngine lifecycle and resource management. + +#include "audio/audio_engine.h" +#include "audio/tracker.h" +#include "generated/assets.h" +#include <assert.h> +#include <stdio.h> + +#if !defined(STRIP_ALL) + +// Test 1: Basic lifecycle (init/shutdown) +void test_audio_engine_lifecycle() { + printf("Test: AudioEngine lifecycle...\n"); + + AudioEngine engine; + printf(" Created AudioEngine object...\n"); + + engine.init(); + printf(" Initialized AudioEngine...\n"); + + // Verify initialization + assert(engine.get_active_voice_count() == 0); + printf(" Verified voice count is 0...\n"); + + engine.shutdown(); + printf(" Shutdown AudioEngine...\n"); + + printf(" ✓ AudioEngine lifecycle test passed\n"); +} + +// Test 2: Load music data and verify resource registration +void test_audio_engine_music_loading() { + printf("Test: AudioEngine music data loading...\n"); + + AudioEngine engine; + engine.init(); + + // Load global music data + engine.load_music_data(&g_tracker_score, g_tracker_samples, + g_tracker_sample_assets, g_tracker_samples_count); + + // Verify resource manager was initialized (samples registered but not loaded + // yet) + SpectrogramResourceManager* res_mgr = engine.get_resource_manager(); + assert(res_mgr != nullptr); + + // Initially, no samples should be loaded (lazy loading) + assert(res_mgr->get_loaded_count() == 0); + + printf(" ✓ Music data loaded: %u samples registered\n", + g_tracker_samples_count); + + engine.shutdown(); + + printf(" ✓ AudioEngine music loading test passed\n"); +} + +// Test 3: Manual resource loading via resource manager +void test_audio_engine_manual_resource_loading() { + printf("Test: AudioEngine manual resource loading...\n"); + + AudioEngine engine; + engine.init(); + + // Load music data + engine.load_music_data(&g_tracker_score, g_tracker_samples, + g_tracker_sample_assets, g_tracker_samples_count); + + SpectrogramResourceManager* res_mgr = engine.get_resource_manager(); + const int initial_loaded = res_mgr->get_loaded_count(); + assert(initial_loaded == 0); // No samples loaded yet + + // Manually preload first few samples + res_mgr->preload(0); + res_mgr->preload(1); + res_mgr->preload(2); + + const int after_preload = res_mgr->get_loaded_count(); + printf(" Samples loaded after manual preload: %d\n", after_preload); + assert(after_preload == 3); // Should have 3 samples loaded + + // Verify samples are accessible + const Spectrogram* spec0 = res_mgr->get_spectrogram(0); + const Spectrogram* spec1 = res_mgr->get_spectrogram(1); + const Spectrogram* spec2 = res_mgr->get_spectrogram(2); + + assert(spec0 != nullptr); + assert(spec1 != nullptr); + assert(spec2 != nullptr); + + engine.shutdown(); + + printf(" ✓ AudioEngine manual resource loading test passed\n"); +} + +// Test 4: Reset and verify state cleanup +void test_audio_engine_reset() { + printf("Test: AudioEngine reset...\n"); + + AudioEngine engine; + engine.init(); + + engine.load_music_data(&g_tracker_score, g_tracker_samples, + g_tracker_sample_assets, g_tracker_samples_count); + + SpectrogramResourceManager* res_mgr = engine.get_resource_manager(); + + // Manually load some samples + res_mgr->preload(0); + res_mgr->preload(1); + res_mgr->preload(2); + + const int loaded_before_reset = res_mgr->get_loaded_count(); + assert(loaded_before_reset == 3); + + // Reset engine + engine.reset(); + + // After reset, state should be cleared + assert(engine.get_active_voice_count() == 0); + + // Resources should be marked as unloaded (but memory not freed) + const int loaded_after_reset = res_mgr->get_loaded_count(); + printf(" Loaded count before reset: %d, after reset: %d\n", + loaded_before_reset, loaded_after_reset); + assert(loaded_after_reset == 0); + + engine.shutdown(); + + printf(" ✓ AudioEngine reset test passed\n"); +} + +#if !defined(STRIP_ALL) +// Test 5: Seeking +void test_audio_engine_seeking() { + printf("Test: AudioEngine seeking...\n"); + + AudioEngine engine; + engine.init(); + + engine.load_music_data(&g_tracker_score, g_tracker_samples, + g_tracker_sample_assets, g_tracker_samples_count); + + // Seek to t=5.0s + engine.seek(5.0f); + assert(engine.get_time() == 5.0f); + + // Seek backward to t=2.0s + engine.seek(2.0f); + assert(engine.get_time() == 2.0f); + + // Seek to beginning + engine.seek(0.0f); + assert(engine.get_time() == 0.0f); + + engine.shutdown(); + + printf(" ✓ AudioEngine seeking test passed\n"); +} +#endif /* !defined(STRIP_ALL) */ + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running AudioEngine tests...\n\n"); + + test_audio_engine_lifecycle(); + test_audio_engine_music_loading(); + test_audio_engine_manual_resource_loading(); + test_audio_engine_reset(); + // TODO: Re-enable after debugging + // test_audio_engine_seeking(); + + printf("\n✅ All AudioEngine tests PASSED\n"); + return 0; +#else + printf("AudioEngine tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} diff --git a/src/tests/audio/test_audio_gen.cc b/src/tests/audio/test_audio_gen.cc new file mode 100644 index 0000000..ebdcb25 --- /dev/null +++ b/src/tests/audio/test_audio_gen.cc @@ -0,0 +1,97 @@ +// This file is part of the 64k demo project. +// It tests the procedural audio generation functions. + +#include "audio/dct.h" +#include "audio/gen.h" +#include <cassert> +#include <cmath> +#include <iostream> +#include <vector> + +void test_generate_note() { + NoteParams params; + params.base_freq = 440.0f; + params.duration_sec = 0.1f; // ~3 frames + params.amplitude = 0.5f; + params.attack_sec = 0.01f; + params.decay_sec = 0.0f; + params.vibrato_rate = 0.0f; + params.vibrato_depth = 0.0f; + params.num_harmonics = 1; + params.harmonic_decay = 1.0f; + params.pitch_randomness = 0.0f; + params.amp_randomness = 0.0f; + + int num_frames = 0; + std::vector<float> data = generate_note_spectrogram(params, &num_frames); + + assert(num_frames > 0); + assert(data.size() == (size_t)num_frames * DCT_SIZE); + + // Check if data is not all zero + bool non_zero = false; + for (float v : data) { + if (std::abs(v) > 1e-6f) { + non_zero = true; + break; + } + } + assert(non_zero); +} + +void test_paste() { + std::vector<float> dest; + int dest_frames = 0; + std::vector<float> src(DCT_SIZE * 2, 1.0f); // 2 frames of 1.0s + + paste_spectrogram(dest, &dest_frames, src, 2, 0); + assert(dest_frames == 2); + assert(dest.size() == 2 * DCT_SIZE); + assert(dest[0] == 1.0f); + + // Paste with offset + paste_spectrogram(dest, &dest_frames, src, 2, 1); + // Dest was 2 frames. We paste 2 frames at offset 1. + // Result should be 1 + 2 = 3 frames. + assert(dest_frames == 3); + assert(dest.size() == 3 * DCT_SIZE); + // Overlap at frame 1: 1.0 + 1.0 = 2.0 + assert(dest[DCT_SIZE] == 2.0f); + // Frame 2: 0.0 (original) + 1.0 (new) = 1.0 + assert(dest[2 * DCT_SIZE] == 1.0f); +} + +void test_filters() { + int num_frames = 1; + std::vector<float> data(DCT_SIZE, 1.0f); + + // Lowpass + apply_spectral_lowpass(data, num_frames, 0.5f); + // Bins >= 256 should be 0 + assert(data[0] == 1.0f); + assert(data[DCT_SIZE - 1] == 0.0f); + assert(data[256] == 0.0f); + assert(data[255] == 1.0f); // Boundary check + + // Comb + data.assign(DCT_SIZE, 1.0f); + apply_spectral_comb(data, num_frames, 10.0f, 1.0f); + // Just check modification + assert(data[0] != 1.0f || data[1] != 1.0f); // It should change values + + // Noise + data.assign(DCT_SIZE, 1.0f); + srand(42); + apply_spectral_noise(data, num_frames, 0.5f); + // Should be noisy + assert(data[0] != 1.0f); +} + +int main() { + std::cout << "Running Audio Gen tests..." << std::endl; + test_generate_note(); + test_paste(); + test_filters(); + std::cout << "Audio Gen tests PASSED" << std::endl; + return 0; +} diff --git a/src/tests/audio/test_dct.cc b/src/tests/audio/test_dct.cc new file mode 100644 index 0000000..89b7964 --- /dev/null +++ b/src/tests/audio/test_dct.cc @@ -0,0 +1,44 @@ +// This file is part of the 64k demo project. +// It tests the DCT implementation for correctness and coverage. + +#include "audio/dct.h" +#include <cassert> +#include <cmath> +#include <cstdlib> +#include <iostream> +#include <vector> + +void test_fdct_idct() { + float input[DCT_SIZE]; + float freq[DCT_SIZE]; + float output[DCT_SIZE]; + + // Initialize with random data + srand(12345); // Fixed seed for reproducibility + for (int i = 0; i < DCT_SIZE; ++i) { + input[i] = (float)rand() / RAND_MAX * 2.0f - 1.0f; + } + + fdct_512(input, freq); + idct_512(freq, output); + + // Verify reconstruction + float max_error = 0.0f; + for (int i = 0; i < DCT_SIZE; ++i) { + float err = std::abs(input[i] - output[i]); + if (err > max_error) + max_error = err; + } + std::cout << "Max reconstruction error: " << max_error << std::endl; + + // Allow some error due to float precision and iterative sum + // 512 sums can accumulate error. + assert(max_error < 1e-4f); +} + +int main() { + std::cout << "Running DCT tests..." << std::endl; + test_fdct_idct(); + std::cout << "DCT tests PASSED" << std::endl; + return 0; +} diff --git a/src/tests/audio/test_fft.cc b/src/tests/audio/test_fft.cc new file mode 100644 index 0000000..2151608 --- /dev/null +++ b/src/tests/audio/test_fft.cc @@ -0,0 +1,229 @@ +// Tests for FFT-based DCT/IDCT implementation +// Verifies correctness against reference O(N²) implementation + +#include "audio/fft.h" + +#include <cassert> +#include <cmath> +#include <cstdio> +#include <cstring> + +// Reference O(N²) DCT-II implementation (from original code) +static void dct_reference(const float* input, float* output, size_t N) { + const float PI = 3.14159265358979323846f; + + for (size_t k = 0; k < N; k++) { + float sum = 0.0f; + for (size_t n = 0; n < N; n++) { + sum += input[n] * cosf((PI / N) * k * (n + 0.5f)); + } + + // Apply DCT-II normalization + if (k == 0) { + output[k] = sum * sqrtf(1.0f / N); + } else { + output[k] = sum * sqrtf(2.0f / N); + } + } +} + +// Reference O(N²) IDCT implementation (DCT-III, inverse of DCT-II) +static void idct_reference(const float* input, float* output, size_t N) { + const float PI = 3.14159265358979323846f; + + for (size_t n = 0; n < N; ++n) { + // DC term with correct normalization + float sum = input[0] * sqrtf(1.0f / N); + // AC terms + for (size_t k = 1; k < N; ++k) { + sum += input[k] * sqrtf(2.0f / N) * cosf((PI / N) * k * (n + 0.5f)); + } + output[n] = sum; + } +} + +// Compare two arrays with tolerance +// Note: FFT-based DCT accumulates slightly more rounding error than O(N²) +// direct method A tolerance of 5e-3 is acceptable for audio applications (< -46 +// dB error) Some input patterns (e.g., impulse at N/2, high-frequency +// sinusoids) have higher numerical error due to reordering and accumulated +// floating-point error +static bool arrays_match(const float* a, const float* b, size_t N, + float tolerance = 5e-3f) { + for (size_t i = 0; i < N; i++) { + const float diff = fabsf(a[i] - b[i]); + if (diff > tolerance) { + fprintf(stderr, "Mismatch at index %zu: %.6f vs %.6f (diff=%.6e)\n", i, + a[i], b[i], diff); + return false; + } + } + return true; +} + +// Test 1: DCT correctness (FFT-based vs reference) +static void test_dct_correctness() { + printf("Test 1: DCT correctness (FFT vs reference O(N²))...\n"); + + const size_t N = 512; + float input[N]; + float output_ref[N]; + float output_fft[N]; + + // Test case 1: Impulse at index 0 + memset(input, 0, N * sizeof(float)); + input[0] = 1.0f; + + dct_reference(input, output_ref, N); + dct_fft(input, output_fft, N); + + assert(arrays_match(output_ref, output_fft, N)); + printf(" ✓ Impulse test passed\n"); + + // Test case 2: Impulse at middle (SKIPPED - reordering method has issues with + // this pattern) The reordering FFT method has systematic sign errors for + // impulses at certain positions This doesn't affect typical audio signals + // (smooth spectra), only pathological cases + // TODO: Investigate and fix, or switch to a different FFT-DCT algorithm + // memset(input, 0, N * sizeof(float)); + // input[N / 2] = 1.0f; + // dct_reference(input, output_ref, N); + // dct_fft(input, output_fft, N); + // assert(arrays_match(output_ref, output_fft, N)); + printf(" ⊘ Middle impulse test skipped (known limitation)\n"); + + // Test case 3: Sinusoidal input (SKIPPED - FFT accumulates error for + // high-frequency components) The reordering method has accumulated + // floating-point error that grows with frequency index This doesn't affect + // audio synthesis quality (round-trip is what matters) + printf( + " ⊘ Sinusoidal input test skipped (accumulated floating-point error)\n"); + + // Test case 4: Random-ish input (SKIPPED - same issue as sinusoidal) + printf(" ⊘ Complex input test skipped (accumulated floating-point error)\n"); + + printf("Test 1: PASSED ✓\n\n"); +} + +// Test 2: IDCT correctness (FFT-based vs reference) +static void test_idct_correctness() { + printf("Test 2: IDCT correctness (FFT vs reference O(N²))...\n"); + + const size_t N = 512; + float input[N]; + float output_ref[N]; + float output_fft[N]; + + // Test case 1: DC component only + memset(input, 0, N * sizeof(float)); + input[0] = 1.0f; + + idct_reference(input, output_ref, N); + idct_fft(input, output_fft, N); + + assert(arrays_match(output_ref, output_fft, N)); + printf(" ✓ DC component test passed\n"); + + // Test case 2: Single frequency bin + memset(input, 0, N * sizeof(float)); + input[10] = 1.0f; + + idct_reference(input, output_ref, N); + idct_fft(input, output_fft, N); + + assert(arrays_match(output_ref, output_fft, N)); + printf(" ✓ Single bin test passed\n"); + + // Test case 3: Mixed frequencies (SKIPPED - accumulated error for complex + // spectra) + printf( + " ⊘ Mixed frequencies test skipped (accumulated floating-point " + "error)\n"); + + printf("Test 2: PASSED ✓\n\n"); +} + +// Test 3: Round-trip (DCT → IDCT should recover original) +static void test_roundtrip() { + printf("Test 3: Round-trip (DCT → IDCT = identity)...\n"); + + const size_t N = 512; + float input[N]; + float dct_output[N]; + float reconstructed[N]; + + // Test case 1: Sinusoidal input + for (size_t i = 0; i < N; i++) { + input[i] = sinf(2.0f * 3.14159265358979323846f * 3.0f * i / N); + } + + dct_fft(input, dct_output, N); + idct_fft(dct_output, reconstructed, N); + + assert(arrays_match(input, reconstructed, N)); + printf(" ✓ Sinusoidal round-trip passed\n"); + + // Test case 2: Complex signal + for (size_t i = 0; i < N; i++) { + input[i] = sinf(i * 0.1f) * cosf(i * 0.05f) + cosf(i * 0.03f); + } + + dct_fft(input, dct_output, N); + idct_fft(dct_output, reconstructed, N); + + assert(arrays_match(input, reconstructed, N)); + printf(" ✓ Complex signal round-trip passed\n"); + + printf("Test 3: PASSED ✓\n\n"); +} + +// Test 4: Output known values for JavaScript comparison +static void test_known_values() { + printf("Test 4: Known values (for JavaScript verification)...\n"); + + const size_t N = 512; + float input[N]; + float output[N]; + + // Simple test case: impulse at index 0 + memset(input, 0, N * sizeof(float)); + input[0] = 1.0f; + + dct_fft(input, output, N); + + printf(" DCT of impulse at 0:\n"); + printf(" output[0] = %.8f (expected ~0.04419417)\n", output[0]); + printf(" output[1] = %.8f (expected ~0.04419417)\n", output[1]); + printf(" output[10] = %.8f (expected ~0.04419417)\n", output[10]); + + // IDCT test + memset(input, 0, N * sizeof(float)); + input[0] = 1.0f; + + idct_fft(input, output, N); + + printf(" IDCT of DC component:\n"); + printf(" output[0] = %.8f (expected ~0.04419417)\n", output[0]); + printf(" output[100] = %.8f (expected ~0.04419417)\n", output[100]); + printf(" output[511] = %.8f (expected ~0.04419417)\n", output[511]); + + printf("Test 4: PASSED ✓\n"); + printf("(Copy these values to JavaScript test for verification)\n\n"); +} + +int main() { + printf("===========================================\n"); + printf("FFT-based DCT/IDCT Test Suite\n"); + printf("===========================================\n\n"); + + test_dct_correctness(); + test_idct_correctness(); + test_roundtrip(); + test_known_values(); + + printf("===========================================\n"); + printf("All tests PASSED ✓\n"); + printf("===========================================\n"); + + return 0; +} diff --git a/src/tests/audio/test_jittered_audio.cc b/src/tests/audio/test_jittered_audio.cc new file mode 100644 index 0000000..d8260ec --- /dev/null +++ b/src/tests/audio/test_jittered_audio.cc @@ -0,0 +1,161 @@ +// This file is part of the 64k demo project. +// It tests the ring buffer under jittered consumption (stress test). + +#include <stdio.h> + +#if !defined(STRIP_ALL) + +#include "audio/audio.h" +#include "audio/backend/jittered_audio_backend.h" +#include "audio/synth.h" +#include "audio/tracker.h" +#include <assert.h> +#include <chrono> +#include <thread> + +void test_jittered_audio_basic() { + printf("Test: Basic jittered audio consumption...\n"); + + // Initialize audio system + synth_init(); + tracker_init(); + + // Set up jittered backend with realistic parameters + // At 32kHz, 10ms = 320 samples = 160 frames (stereo) + // Jitter of ±5ms means 5-15ms intervals, or 80-240 frames + JitteredAudioBackend jittered_backend; + jittered_backend.set_base_interval(10.0f); // 10ms base interval + jittered_backend.set_jitter_amount(5.0f); // ±5ms jitter + jittered_backend.set_chunk_size_range( + 80, 240); // Realistic chunk sizes for 5-15ms + + audio_set_backend(&jittered_backend); + audio_init(); + + // Start audio thread + audio_start(); + assert(jittered_backend.is_running()); + + // Simulate main loop for 0.1 seconds (quick stress test) + const float total_time = 0.1f; + const float dt = 1.0f / 60.0f; // 60fps + float music_time = 0.0f; + + for (float t = 0.0f; t < total_time; t += dt) { + music_time += dt; // Normal tempo + + // Update tracker and fill buffer + tracker_update(music_time, dt); + audio_render_ahead(music_time, dt); + + // Sleep minimal time to let audio thread run + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Stop audio + audio_shutdown(); + + // Check results + const int frames_consumed = jittered_backend.get_total_frames_consumed(); + const int underruns = jittered_backend.get_underrun_count(); + + printf(" Frames consumed: %d\n", frames_consumed); + printf(" Underruns: %d\n", underruns); + + // Should have consumed some audio (exact amount depends on timing/jitter) + // With minimal sleeps and 0.1s sim time, expect 50-1000 frames + assert(frames_consumed > 50); // At least some audio consumed + assert(frames_consumed < 2000); // Not excessive + + // Underruns are acceptable in this test, but shouldn't be excessive + assert(underruns < 5); // Less than 5 underruns in 0.1 seconds + + printf(" ✓ Basic jittered audio consumption PASSED\n"); +} + +void test_jittered_audio_with_acceleration() { + printf("Test: Jittered audio with tempo acceleration...\n"); + + // Initialize audio system + synth_init(); + tracker_init(); + + // Set up jittered backend with aggressive settings for stress test + // At 32kHz, 15ms = 480 samples = 240 frames (stereo) + // Jitter of ±10ms means 5-25ms intervals, or 80-400 frames + JitteredAudioBackend jittered_backend; + jittered_backend.set_base_interval(15.0f); // Slower consumption + jittered_backend.set_jitter_amount(10.0f); // High jitter + jittered_backend.set_chunk_size_range(80, 400); // Realistic stress test range + + audio_set_backend(&jittered_backend); + audio_init(); + + // Start audio thread + audio_start(); + + // Simulate acceleration scenario (similar to real demo) + const float total_time = 0.6f; + const float dt = 1.0f / 60.0f; + float music_time = 0.0f; + float physical_time = 0.0f; + + for (int frame = 0; frame < 36; ++frame) { // 0.6 seconds @ 60fps + physical_time = frame * dt; + + // Variable tempo (accelerate from 0.3-0.6s) + float tempo_scale = 1.0f; + if (physical_time >= 0.3f && physical_time < 0.6f) { + const float progress = (physical_time - 0.3f) / 0.3f; + tempo_scale = 1.0f + progress * 1.0f; // 1.0 → 2.0 + } + + music_time += dt * tempo_scale; + + // Update tracker and fill buffer + tracker_update(music_time, dt * tempo_scale); + audio_render_ahead(music_time, dt); + + // Sleep minimal time to let audio thread run + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + printf("\n"); + + // Stop audio + audio_shutdown(); + + // Check results + const int frames_consumed = jittered_backend.get_total_frames_consumed(); + const int underruns = jittered_backend.get_underrun_count(); + + printf(" Total frames consumed: %d\n", frames_consumed); + printf(" Total underruns: %d\n", underruns); + + // Should have consumed some audio (exact amount depends on timing/jitter) + // With minimal sleeps and 0.6s sim time, expect more than basic test + assert(frames_consumed > 200); // At least some audio consumed + assert(frames_consumed < 5000); // Not excessive + + // During acceleration with jitter, some underruns are expected but not + // excessive + assert(underruns < 10); // Less than 10 underruns in 0.6 seconds + + printf(" ✓ Jittered audio with acceleration PASSED\n"); +} + +int main() { + printf("Running Jittered Audio Backend tests...\n\n"); + test_jittered_audio_basic(); + test_jittered_audio_with_acceleration(); + printf("\n✅ All Jittered Audio Backend tests PASSED\n"); + return 0; +} + +#else + +int main() { + printf("Jittered Audio Backend tests skipped (STRIP_ALL enabled)\n"); + return 0; +} + +#endif /* !defined(STRIP_ALL) */ diff --git a/src/tests/audio/test_mock_backend.cc b/src/tests/audio/test_mock_backend.cc new file mode 100644 index 0000000..defd73d --- /dev/null +++ b/src/tests/audio/test_mock_backend.cc @@ -0,0 +1,215 @@ +// This file is part of the 64k demo project. +// It tests the MockAudioBackend implementation. +// Verifies event recording, time tracking, and synth integration. + +#include "audio/audio.h" +#include "audio/backend/mock_audio_backend.h" +#include "audio/synth.h" +#include <assert.h> +#include <cmath> +#include <stdio.h> + +#if !defined(STRIP_ALL) + +void test_event_recording() { + MockAudioBackend backend; + + // Initially no events + assert(backend.get_events().size() == 0); + assert(backend.get_current_time() == 0.0f); + + // Simulate voice trigger + backend.on_voice_triggered(0.5f, 3, 0.75f, -0.25f); + + // Verify event recorded + const auto& events = backend.get_events(); + assert(events.size() == 1); + assert(events[0].timestamp_sec == 0.5f); + assert(events[0].spectrogram_id == 3); + assert(events[0].volume == 0.75f); + assert(events[0].pan == -0.25f); + + // Record multiple events + backend.on_voice_triggered(1.0f, 5, 1.0f, 0.0f); + backend.on_voice_triggered(1.5f, 3, 0.5f, 0.5f); + + assert(backend.get_events().size() == 3); + assert(events[1].timestamp_sec == 1.0f); + assert(events[2].timestamp_sec == 1.5f); + + // Clear events + backend.clear_events(); + assert(backend.get_events().size() == 0); + + printf("Event recording test PASSED\n"); +} + +void test_time_tracking() { + MockAudioBackend backend; + + // Test manual time advance + assert(backend.get_current_time() == 0.0f); + + backend.advance_time(0.5f); + assert(backend.get_current_time() == 0.5f); + + backend.advance_time(1.0f); + assert(backend.get_current_time() == 1.5f); + + // Test time setting + backend.set_time(10.0f); + assert(backend.get_current_time() == 10.0f); + + printf("Time tracking test PASSED\n"); +} + +void test_frame_rendering() { + MockAudioBackend backend; + + // Simulate frame rendering (32000 Hz sample rate) + // 1 second = 32000 frames + backend.on_frames_rendered(16000); // 0.5 seconds + assert(std::abs(backend.get_current_time() - 0.5f) < 0.001f); + + backend.on_frames_rendered(16000); // Another 0.5 seconds + assert(std::abs(backend.get_current_time() - 1.0f) < 0.001f); + + backend.on_frames_rendered(32000); // 1 second + assert(std::abs(backend.get_current_time() - 2.0f) < 0.001f); + + printf("Frame rendering test PASSED\n"); +} + +void test_synth_integration() { + MockAudioBackend backend; + audio_set_backend(&backend); + + synth_init(); + + // Create dummy spectrogram + float data[DCT_SIZE * 10] = {0}; + data[0] = 100.0f; // DC component + + Spectrogram spec = {data, data, 10}; + int spec_id = synth_register_spectrogram(&spec); + assert(spec_id >= 0); + + // Trigger voice - should be recorded at time 0 + synth_trigger_voice(spec_id, 0.8f, -0.3f); + + // Verify event recorded + const auto& events = backend.get_events(); + assert(events.size() == 1); + assert(events[0].timestamp_sec == 0.0f); // Before any rendering + assert(events[0].spectrogram_id == spec_id); + assert(events[0].volume == 0.8f); + assert(events[0].pan == -0.3f); + + // Render some frames to advance time + float output[1024] = {0}; + synth_render(output, 512); // ~0.016 sec at 32kHz + + // Verify synth updated its time + // (Note: synth time is internal, mock doesn't track it from render) + + // Trigger another voice after rendering + synth_trigger_voice(spec_id, 1.0f, 0.5f); + + assert(events.size() == 2); + // Second trigger should have timestamp > 0 + assert(events[1].timestamp_sec > 0.0f); + assert(events[1].timestamp_sec < 0.02f); // ~512 frames = ~0.016 sec + + printf("Synth integration test PASSED\n"); +} + +void test_multiple_voices() { + MockAudioBackend backend; + audio_set_backend(&backend); + + synth_init(); + + // Create multiple spectrograms + float data1[DCT_SIZE * 5] = {0}; + float data2[DCT_SIZE * 5] = {0}; + float data3[DCT_SIZE * 5] = {0}; + + Spectrogram spec1 = {data1, data1, 5}; + Spectrogram spec2 = {data2, data2, 5}; + Spectrogram spec3 = {data3, data3, 5}; + + int id1 = synth_register_spectrogram(&spec1); + int id2 = synth_register_spectrogram(&spec2); + int id3 = synth_register_spectrogram(&spec3); + + // Trigger multiple voices at once + synth_trigger_voice(id1, 1.0f, -1.0f); + synth_trigger_voice(id2, 0.5f, 0.0f); + synth_trigger_voice(id3, 0.75f, 1.0f); + + // Verify all recorded + const auto& events = backend.get_events(); + assert(events.size() == 3); + + // Verify each has correct properties + assert(events[0].spectrogram_id == id1); + assert(events[1].spectrogram_id == id2); + assert(events[2].spectrogram_id == id3); + + assert(events[0].volume == 1.0f); + assert(events[1].volume == 0.5f); + assert(events[2].volume == 0.75f); + + assert(events[0].pan == -1.0f); + assert(events[1].pan == 0.0f); + assert(events[2].pan == 1.0f); + + printf("Multiple voices test PASSED\n"); +} + +void test_audio_render_silent_integration() { + MockAudioBackend backend; + audio_set_backend(&backend); + + audio_init(); + synth_init(); + + // Create a spectrogram + float data[DCT_SIZE * 5] = {0}; + Spectrogram spec = {data, data, 5}; + int spec_id = synth_register_spectrogram(&spec); + + // Trigger at t=0 + synth_trigger_voice(spec_id, 1.0f, 0.0f); + + // Simulate 2 seconds of silent rendering (seek/fast-forward) + audio_render_silent(2.0f); + + // Verify backend time advanced via on_frames_rendered + const float expected_time = 2.0f; + const float actual_time = backend.get_current_time(); + assert(std::abs(actual_time - expected_time) < 0.01f); // 10ms tolerance + + audio_shutdown(); + + printf("audio_render_silent integration test PASSED\n"); +} + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running MockAudioBackend tests...\n"); + test_event_recording(); + test_time_tracking(); + test_frame_rendering(); + test_synth_integration(); + test_multiple_voices(); + test_audio_render_silent_integration(); + printf("All MockAudioBackend tests PASSED\n"); + return 0; +#else + printf("MockAudioBackend tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} diff --git a/src/tests/audio/test_silent_backend.cc b/src/tests/audio/test_silent_backend.cc new file mode 100644 index 0000000..8daacf7 --- /dev/null +++ b/src/tests/audio/test_silent_backend.cc @@ -0,0 +1,211 @@ +// This file is part of the 64k demo project. +// It tests the SilentBackend for audio testing without hardware. +// Verifies audio.cc functionality using silent backend. + +#include "audio/audio.h" +#include "audio/audio_engine.h" +#include "audio/backend/silent_backend.h" +#include "audio/synth.h" +#include <assert.h> +#include <stdio.h> + +#if !defined(STRIP_ALL) + +// Test: SilentBackend initialization and lifecycle +void test_silent_backend_lifecycle() { + SilentBackend backend; + + assert(!backend.is_initialized()); + assert(!backend.is_started()); + + backend.init(); + assert(backend.is_initialized()); + assert(!backend.is_started()); + + backend.start(); + assert(backend.is_initialized()); + assert(backend.is_started()); + + backend.shutdown(); + assert(!backend.is_initialized()); + assert(!backend.is_started()); + + printf("SilentBackend lifecycle test PASSED\n"); +} + +// Test: Audio system with SilentBackend +void test_audio_with_silent_backend() { + SilentBackend backend; + audio_set_backend(&backend); + + audio_init(); + assert(backend.is_initialized()); + + audio_start(); + assert(backend.is_started()); + + audio_shutdown(); + assert(!backend.is_initialized()); + + printf("Audio with SilentBackend test PASSED\n"); +} + +// Test: Peak control in SilentBackend +void test_silent_backend_peak() { + SilentBackend backend; + audio_set_backend(&backend); + + audio_init(); + + // Default peak should be 0 + assert(backend.get_realtime_peak() == 0.0f); + assert(audio_get_realtime_peak() == 0.0f); + + // Set test peak + backend.set_peak(0.75f); + assert(backend.get_realtime_peak() == 0.75f); + assert(audio_get_realtime_peak() == 0.75f); + + // Reset + backend.set_peak(0.0f); + assert(backend.get_realtime_peak() == 0.0f); + + audio_shutdown(); + + printf("SilentBackend peak control test PASSED\n"); +} + +// Test: Frame and voice tracking +void test_silent_backend_tracking() { + SilentBackend backend; + audio_set_backend(&backend); + + AudioEngine engine; + engine.init(); + + // Initial state + assert(backend.get_frames_rendered() == 0); + assert(backend.get_voice_trigger_count() == 0); + + // Create a dummy spectrogram + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + int id = synth_register_spectrogram(&spec); + + // Trigger a voice + synth_trigger_voice(id, 0.8f, 0.0f); + assert(backend.get_voice_trigger_count() == 1); + + // Render audio (calls on_frames_rendered) + audio_render_ahead(0.0f, 0.1f); // Render ~0.1 seconds + assert(backend.get_frames_rendered() > 0); + + // Reset stats + backend.reset_stats(); + assert(backend.get_frames_rendered() == 0); + assert(backend.get_voice_trigger_count() == 0); + + engine.shutdown(); + audio_shutdown(); + + printf("SilentBackend tracking test PASSED\n"); +} + +// Test: Playback time with SilentBackend +void test_audio_playback_time() { + SilentBackend backend; + audio_set_backend(&backend); + + AudioEngine engine; + engine.init(); + audio_start(); + + // Initial playback time should be 0 + float t0 = audio_get_playback_time(); + assert(t0 == 0.0f); + + // Render some audio + audio_render_ahead(0.5f, 0.1f); // Advance music time to 0.5s + + // Playback time should advance based on frames rendered + // Note: audio_get_playback_time() tracks cumulative frames consumed + float t1 = audio_get_playback_time(); + assert(t1 >= 0.0f); // Should have advanced + + // Render more + audio_render_ahead(1.0f, 0.5f); + float t2 = audio_get_playback_time(); + assert(t2 >= t1); // Should continue advancing + + engine.shutdown(); + audio_shutdown(); + + printf("Audio playback time test PASSED\n"); +} + +// Test: Buffer management with partial writes +void test_audio_buffer_partial_writes() { + SilentBackend backend; + audio_set_backend(&backend); + + AudioEngine engine; + engine.init(); + audio_start(); + + // Fill buffer multiple times to test wraparound + // Note: With SilentBackend, frames_rendered won't increase because + // there's no audio callback consuming from the ring buffer + for (int i = 0; i < 10; ++i) { + audio_render_ahead((float)i * 0.1f, 0.1f); + } + + // Buffer should have handled multiple writes correctly (no crash) + // We can't check frames_rendered with SilentBackend since there's + // no audio callback to consume from the ring buffer + audio_update(); // Should not crash + + engine.shutdown(); + audio_shutdown(); + + printf("Audio buffer partial writes test PASSED\n"); +} + +// Test: audio_update() with SilentBackend +void test_audio_update() { + SilentBackend backend; + audio_set_backend(&backend); + + AudioEngine engine; + engine.init(); + audio_start(); + + // audio_update() should be callable without crashing + audio_update(); + audio_update(); + audio_update(); + + engine.shutdown(); + audio_shutdown(); + + printf("Audio update test PASSED\n"); +} + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running SilentBackend tests...\n"); + test_silent_backend_lifecycle(); + test_audio_with_silent_backend(); + test_silent_backend_peak(); + test_silent_backend_tracking(); + test_audio_playback_time(); + test_audio_buffer_partial_writes(); + test_audio_update(); + printf("All SilentBackend tests PASSED\n"); + return 0; +#else + printf("SilentBackend tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} diff --git a/src/tests/audio/test_spectral_brush.cc b/src/tests/audio/test_spectral_brush.cc new file mode 100644 index 0000000..ae1862a --- /dev/null +++ b/src/tests/audio/test_spectral_brush.cc @@ -0,0 +1,243 @@ +// This file is part of the 64k demo project. +// Unit tests for spectral brush primitives. +// Tests linear Bezier interpolation, profiles, and spectrogram rendering. + +#include "audio/spectral_brush.h" + +#include <cassert> +#include <cmath> +#include <cstdio> +#include <cstring> + +// Test tolerance for floating-point comparisons +static const float EPSILON = 1e-5f; + +// Helper: Compare floats with tolerance +static bool float_eq(float a, float b) { + return fabsf(a - b) < EPSILON; +} + +// Test: Linear Bezier interpolation with 2 control points (simple line) +void test_bezier_linear_2points() { + const float frames[] = {0.0f, 100.0f}; + const float values[] = {50.0f, 150.0f}; + + // At control points, should return exact values + assert(float_eq(evaluate_bezier_linear(frames, values, 2, 0.0f), 50.0f)); + assert(float_eq(evaluate_bezier_linear(frames, values, 2, 100.0f), 150.0f)); + + // Midpoint: linear interpolation + const float mid = evaluate_bezier_linear(frames, values, 2, 50.0f); + assert(float_eq(mid, 100.0f)); // (50 + 150) / 2 + + // Quarter point + const float quarter = evaluate_bezier_linear(frames, values, 2, 25.0f); + assert(float_eq(quarter, 75.0f)); // 50 + (150 - 50) * 0.25 + + printf("[PASS] test_bezier_linear_2points\n"); +} + +// Test: Linear Bezier interpolation with 4 control points +void test_bezier_linear_4points() { + const float frames[] = {0.0f, 20.0f, 50.0f, 100.0f}; + const float values[] = {200.0f, 80.0f, 60.0f, 50.0f}; + + // At control points + assert(float_eq(evaluate_bezier_linear(frames, values, 4, 0.0f), 200.0f)); + assert(float_eq(evaluate_bezier_linear(frames, values, 4, 20.0f), 80.0f)); + assert(float_eq(evaluate_bezier_linear(frames, values, 4, 50.0f), 60.0f)); + assert(float_eq(evaluate_bezier_linear(frames, values, 4, 100.0f), 50.0f)); + + // Between first and second point (frame 10) + const float interp1 = evaluate_bezier_linear(frames, values, 4, 10.0f); + // t = (10 - 0) / (20 - 0) = 0.5 + // value = 200 * 0.5 + 80 * 0.5 = 140 + assert(float_eq(interp1, 140.0f)); + + // Between third and fourth point (frame 75) + const float interp2 = evaluate_bezier_linear(frames, values, 4, 75.0f); + // t = (75 - 50) / (100 - 50) = 0.5 + // value = 60 * 0.5 + 50 * 0.5 = 55 + assert(float_eq(interp2, 55.0f)); + + printf("[PASS] test_bezier_linear_4points\n"); +} + +// Test: Edge cases (single point, empty, out of range) +void test_bezier_edge_cases() { + const float frames[] = {50.0f}; + const float values[] = {123.0f}; + + // Single control point: always return that value + assert(float_eq(evaluate_bezier_linear(frames, values, 1, 0.0f), 123.0f)); + assert(float_eq(evaluate_bezier_linear(frames, values, 1, 100.0f), 123.0f)); + + // Empty array: return 0 + assert(float_eq(evaluate_bezier_linear(frames, values, 0, 50.0f), 0.0f)); + + // Out of range: clamp to endpoints + const float frames2[] = {10.0f, 90.0f}; + const float values2[] = {100.0f, 200.0f}; + assert(float_eq(evaluate_bezier_linear(frames2, values2, 2, 0.0f), + 100.0f)); // Before start + assert(float_eq(evaluate_bezier_linear(frames2, values2, 2, 100.0f), + 200.0f)); // After end + + printf("[PASS] test_bezier_edge_cases\n"); +} + +// Test: Gaussian profile evaluation +void test_profile_gaussian() { + // At center (distance = 0), should be 1.0 + assert(float_eq(evaluate_profile(PROFILE_GAUSSIAN, 0.0f, 30.0f, 0.0f), 1.0f)); + + // Gaussian falloff: exp(-(dist^2 / sigma^2)) + const float sigma = 30.0f; + const float dist = 15.0f; + const float expected = expf(-(dist * dist) / (sigma * sigma)); + const float actual = evaluate_profile(PROFILE_GAUSSIAN, dist, sigma, 0.0f); + assert(float_eq(actual, expected)); + + // Far from center: should approach 0 + const float far = evaluate_profile(PROFILE_GAUSSIAN, 100.0f, 30.0f, 0.0f); + assert(far < 0.01f); // Very small + + printf("[PASS] test_profile_gaussian\n"); +} + +// Test: Decaying sinusoid profile evaluation +void test_profile_decaying_sinusoid() { + const float decay = 0.15f; + const float omega = 0.8f; + + // At center (distance = 0) + // exp(-0 * 0.15) * cos(0 * 0.8) = 1.0 * 1.0 = 1.0 + assert(float_eq( + evaluate_profile(PROFILE_DECAYING_SINUSOID, 0.0f, decay, omega), 1.0f)); + + // At distance 10 + const float dist = 10.0f; + const float expected = expf(-decay * dist) * cosf(omega * dist); + const float actual = + evaluate_profile(PROFILE_DECAYING_SINUSOID, dist, decay, omega); + assert(float_eq(actual, expected)); + + printf("[PASS] test_profile_decaying_sinusoid\n"); +} + +// Test: Noise profile evaluation (deterministic) +void test_profile_noise() { + const float amplitude = 0.5f; + const uint32_t seed = 42; + + // Same distance + seed should produce same value + const float val1 = + evaluate_profile(PROFILE_NOISE, 10.0f, amplitude, (float)seed); + const float val2 = + evaluate_profile(PROFILE_NOISE, 10.0f, amplitude, (float)seed); + assert(float_eq(val1, val2)); + + // Different distance should produce different value (with high probability) + const float val3 = + evaluate_profile(PROFILE_NOISE, 20.0f, amplitude, (float)seed); + assert(!float_eq(val1, val3)); + + // Should be in range [0, amplitude] + assert(val1 >= 0.0f && val1 <= amplitude); + + printf("[PASS] test_profile_noise\n"); +} + +// Test: draw_bezier_curve full integration +void test_draw_bezier_curve() { + const int dct_size = 512; + const int num_frames = 100; + float spectrogram[512 * 100]; + memset(spectrogram, 0, sizeof(spectrogram)); + + // Simple curve: constant frequency, linearly decaying amplitude + const float frames[] = {0.0f, 100.0f}; + const float freqs[] = {440.0f, 440.0f}; // A4 note (constant pitch) + const float amps[] = {1.0f, 0.0f}; // Fade out + + draw_bezier_curve(spectrogram, dct_size, num_frames, frames, freqs, amps, 2, + PROFILE_GAUSSIAN, 30.0f); + + // Verify: At frame 0, should have peak around 440 Hz bin + // bin = (440 / 16000) * 512 ≈ 14.08 + const int expected_bin = 14; + const float val_at_peak = spectrogram[0 * dct_size + expected_bin]; + assert(val_at_peak > 0.5f); // Should be near 1.0 due to Gaussian + + // Verify: At frame 99 (end), amplitude should be near 0 + const float val_at_end = spectrogram[99 * dct_size + expected_bin]; + assert(val_at_end < 0.1f); // Near zero + + // Verify: At frame 50 (midpoint), amplitude should be ~0.5 + const float val_at_mid = spectrogram[50 * dct_size + expected_bin]; + assert(val_at_mid > 0.3f && val_at_mid < 0.7f); // Around 0.5 + + printf("[PASS] test_draw_bezier_curve\n"); +} + +// Test: draw_bezier_curve_add (additive mode) +void test_draw_bezier_curve_add() { + const int dct_size = 512; + const int num_frames = 100; + float spectrogram[512 * 100]; + memset(spectrogram, 0, sizeof(spectrogram)); + + // Draw first curve + const float frames1[] = {0.0f, 100.0f}; + const float freqs1[] = {440.0f, 440.0f}; + const float amps1[] = {0.5f, 0.5f}; + draw_bezier_curve(spectrogram, dct_size, num_frames, frames1, freqs1, amps1, + 2, PROFILE_GAUSSIAN, 30.0f); + + const int bin = 14; // ~440 Hz + const float val_before_add = spectrogram[0 * dct_size + bin]; + + // Add second curve (same frequency, same amplitude) + draw_bezier_curve_add(spectrogram, dct_size, num_frames, frames1, freqs1, + amps1, 2, PROFILE_GAUSSIAN, 30.0f); + + const float val_after_add = spectrogram[0 * dct_size + bin]; + + // Should be approximately doubled + assert(val_after_add > val_before_add * 1.8f); // Allow small error + + printf("[PASS] test_draw_bezier_curve_add\n"); +} + +// Test: RNG determinism +void test_rng_determinism() { + const uint32_t seed = 12345; + + // Same seed should produce same value + const uint32_t val1 = spectral_brush_rand(seed); + const uint32_t val2 = spectral_brush_rand(seed); + assert(val1 == val2); + + // Different seeds should produce different values + const uint32_t val3 = spectral_brush_rand(seed + 1); + assert(val1 != val3); + + printf("[PASS] test_rng_determinism\n"); +} + +int main() { + printf("Running spectral brush tests...\n\n"); + + test_bezier_linear_2points(); + test_bezier_linear_4points(); + test_bezier_edge_cases(); + test_profile_gaussian(); + test_profile_decaying_sinusoid(); + test_profile_noise(); + test_draw_bezier_curve(); + test_draw_bezier_curve_add(); + test_rng_determinism(); + + printf("\n✓ All tests passed!\n"); + return 0; +} diff --git a/src/tests/audio/test_synth.cc b/src/tests/audio/test_synth.cc new file mode 100644 index 0000000..12cbc54 --- /dev/null +++ b/src/tests/audio/test_synth.cc @@ -0,0 +1,113 @@ +// This file is part of the 64k demo project. +// It tests the core functionality of the audio synthesis engine. +// Verifies voice triggering, registration, and rendering state. + +#include "audio/synth.h" +#include <assert.h> +#include <cmath> +#include <stdio.h> + +void test_registration() { + synth_init(); + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + + int id = synth_register_spectrogram(&spec); + assert(id >= 0); + assert(synth_get_active_voice_count() == 0); + + synth_unregister_spectrogram(id); + // Re-register to check slot reuse + int id2 = synth_register_spectrogram(&spec); + assert(id2 == id); // Should reuse the slot 0 +} + +void test_trigger() { + synth_init(); + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + int id = synth_register_spectrogram(&spec); + + synth_trigger_voice(id, 1.0f, 0.0f); + assert(synth_get_active_voice_count() == 1); +} + +void test_render() { + synth_init(); + float data[DCT_SIZE * 2] = {0}; + // Put some signal in (DC component) + data[0] = 100.0f; + + Spectrogram spec = {data, data, 2}; + int id = synth_register_spectrogram(&spec); + + synth_trigger_voice(id, 1.0f, 0.0f); + + float output[1024] = {0}; + synth_render(output, 256); + + // Verify output is not all zero (IDCT of DC component should be constant) + bool non_zero = false; + for (int i = 0; i < 256; ++i) { + if (std::abs(output[i]) > 1e-6f) + non_zero = true; + } + assert(non_zero); + + // Test render with no voices + synth_init(); // Reset + float output2[1024] = {0}; + synth_render(output2, 256); + for (int i = 0; i < 256; ++i) + assert(output2[i] == 0.0f); +} + +void test_update() { + synth_init(); + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + int id = synth_register_spectrogram(&spec); + + float* back_buf = synth_begin_update(id); + assert(back_buf != nullptr); + // Write something + back_buf[0] = 50.0f; + synth_commit_update(id); + + // Test invalid ID + assert(synth_begin_update(-1) == nullptr); + synth_commit_update(-1); // Should not crash +} + +void test_exhaustion() { + synth_init(); + float data[DCT_SIZE * 2] = {0}; + Spectrogram spec = {data, data, 2}; + + for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { + int id = synth_register_spectrogram(&spec); + assert(id >= 0); + } + // Next one should fail + int id = synth_register_spectrogram(&spec); + assert(id == -1); +} + +void test_peak() { + // Already called render in test_render. + // Just call the getter. + float peak = synth_get_output_peak(); + assert(peak >= 0.0f); +} + +int main() { + printf("Running SynthEngine tests...\n"); + test_registration(); + test_trigger(); + test_render(); + test_update(); + test_exhaustion(); + test_peak(); + printf("SynthEngine tests PASSED\n"); + return 0; +}
\ No newline at end of file diff --git a/src/tests/audio/test_tracker.cc b/src/tests/audio/test_tracker.cc new file mode 100644 index 0000000..6be2a8d --- /dev/null +++ b/src/tests/audio/test_tracker.cc @@ -0,0 +1,73 @@ +// This file is part of the 64k demo project. +// It tests the core functionality of the audio tracker engine. + +#include "audio/audio_engine.h" +#include "audio/gen.h" +#include "audio/synth.h" +#include "audio/tracker.h" +// #include "generated/music_data.h" // Will be generated by tracker_compiler +#include <assert.h> +#include <stdio.h> + +// Forward declaration for generated data +extern const NoteParams g_tracker_samples[]; +extern const uint32_t g_tracker_samples_count; +extern const TrackerPattern g_tracker_patterns[]; +extern const uint32_t g_tracker_patterns_count; +extern const TrackerScore g_tracker_score; + +void test_tracker_init() { + AudioEngine engine; + engine.init(); + printf("Tracker init test PASSED\n"); + engine.shutdown(); +} + +void test_tracker_pattern_triggering() { + AudioEngine engine; + engine.init(); + + // At time 0.0f, 3 patterns are triggered: + // - crash (1 event at beat 0.0) + // - kick_basic (events at beat 0.0, 2.0, 2.5) + // - hihat_basic (events at beat 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5) + // With event-based triggering, only events at beat 0.0 trigger immediately. + + // Test 1: At music_time = 0.0f, events at beat 0.0 trigger + // drums_basic: + // 0.00, ASSET_KICK_1 + // 0.00, NOTE_A4 + engine.update(0.0f, 0.0f); + // Expect 2 voices: kick + note + assert(engine.get_active_voice_count() == 2); + + // Test 2: At music_time = 0.25f (beat 0.5 @ 120 BPM), snare event triggers + // 0.25, ASSET_SNARE_1 + engine.update(0.25f, 0.0f); + // Expect at least 2 voices (snare + maybe others) + // Exact count depends on sample duration (kick/note might have finished) + int voices = engine.get_active_voice_count(); + assert(voices >= 2); + + // Test 3: At music_time = 0.5f (beat 1.0), kick event triggers + // 0.50, ASSET_KICK_1 + engine.update(0.5f, 0.0f); + // Expect at least 3 voices (new kick + others) + assert(engine.get_active_voice_count() >= 3); + + // Test 4: Advance to 2.0f - new patterns trigger at time 2.0f + engine.update(2.0f, 0.0f); + // Many events have triggered by now + assert(engine.get_active_voice_count() > 5); + + printf("Tracker pattern triggering test PASSED\n"); + engine.shutdown(); +} + +int main() { + printf("Running Tracker tests...\n"); + test_tracker_init(); + test_tracker_pattern_triggering(); + printf("Tracker tests PASSED\n"); + return 0; +} diff --git a/src/tests/audio/test_tracker_timing.cc b/src/tests/audio/test_tracker_timing.cc new file mode 100644 index 0000000..9f15197 --- /dev/null +++ b/src/tests/audio/test_tracker_timing.cc @@ -0,0 +1,309 @@ +// This file is part of the 64k demo project. +// It tests tracker timing and synchronization using MockAudioBackend. +// Verifies pattern triggers occur at correct times with proper BPM scaling. + +#include "audio/audio.h" +#include "audio/audio_engine.h" +#include "audio/backend/mock_audio_backend.h" +#include "audio/synth.h" +#include "audio/tracker.h" +#include <assert.h> +#include <cmath> +#include <stdio.h> + +#if !defined(STRIP_ALL) + +// Helper: Setup audio engine for testing +static void setup_audio_test(MockAudioBackend& backend, AudioEngine& engine) { + audio_set_backend(&backend); + engine.init(); +} + +// Helper: Check if a timestamp exists in events within tolerance +static bool has_event_at_time(const std::vector<VoiceTriggerEvent>& events, + float expected_time, float tolerance = 0.001f) { + for (const auto& evt : events) { + if (std::abs(evt.timestamp_sec - expected_time) < tolerance) { + return true; + } + } + return false; +} + +// Helper: Count events at a specific time +static int count_events_at_time(const std::vector<VoiceTriggerEvent>& events, + float expected_time, float tolerance = 0.001f) { + int count = 0; + for (const auto& evt : events) { + if (std::abs(evt.timestamp_sec - expected_time) < tolerance) { + count++; + } + } + return count; +} + +// Helper: Get all unique timestamps in events +static std::vector<float> +get_unique_timestamps(const std::vector<VoiceTriggerEvent>& events, + float tolerance = 0.001f) { + std::vector<float> timestamps; + for (const auto& evt : events) { + bool found = false; + for (float ts : timestamps) { + if (std::abs(evt.timestamp_sec - ts) < tolerance) { + found = true; + break; + } + } + if (!found) { + timestamps.push_back(evt.timestamp_sec); + } + } + return timestamps; +} + +void test_basic_event_recording() { + printf("Test: Basic event recording with mock backend...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + engine.update(0.0f, 0.0f); + const auto& events = backend.get_events(); + printf(" Events triggered at t=0.0: %zu\n", events.size()); + + assert(events.size() > 0); + for (const auto& evt : events) { + assert(evt.timestamp_sec < 0.1f); + } + + engine.shutdown(); + printf(" ✓ Basic event recording works\n"); +} + +void test_progressive_triggering() { + printf("Test: Progressive pattern triggering...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + engine.update(0.0f, 0.0f); + const size_t events_at_0 = backend.get_events().size(); + printf(" Events at t=0.0: %zu\n", events_at_0); + + engine.update(1.0f, 0.0f); + const size_t events_at_1 = backend.get_events().size(); + printf(" Events at t=1.0: %zu\n", events_at_1); + + engine.update(2.0f, 0.0f); + const size_t events_at_2 = backend.get_events().size(); + printf(" Events at t=2.0: %zu\n", events_at_2); + + assert(events_at_1 >= events_at_0); + assert(events_at_2 >= events_at_1); + + engine.shutdown(); + printf(" ✓ Events accumulate over time\n"); +} + +void test_simultaneous_triggers() { + printf("Test: SIMULTANEOUS pattern triggers at same time...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + backend.clear_events(); + engine.update(0.0f, 0.0f); + + const auto& events = backend.get_events(); + if (events.size() == 0) { + printf(" No events at t=0.0, skipping test\n"); + return; + } + + // Check if we have multiple events at t=0 + const int simultaneous_count = count_events_at_time(events, 0.0f, 0.001f); + printf(" Simultaneous events at t=0.0: %d out of %zu total\n", + simultaneous_count, events.size()); + + if (simultaneous_count > 1) { + // Verify all simultaneous events have EXACTLY the same timestamp + const float first_timestamp = events[0].timestamp_sec; + float max_delta = 0.0f; + + for (size_t i = 1; i < events.size(); ++i) { + const float delta = std::abs(events[i].timestamp_sec - first_timestamp); + max_delta = std::fmaxf(max_delta, delta); + } + + printf(" Maximum timestamp delta: %.6f seconds (%.3f ms)\n", max_delta, + max_delta * 1000.0f); + + // Simultaneous events should have sub-millisecond timing + assert(max_delta < 0.001f); // Less than 1ms difference + + printf(" ✓ All simultaneous events within 1ms of each other\n"); + } else { + printf(" ℹ Only one event at t=0.0, cannot verify simultaneity\n"); + } + + engine.shutdown(); +} + +void test_timing_monotonicity() { + printf("Test: Event timestamps are monotonically increasing...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + for (float t = 0.0f; t <= 5.0f; t += 0.5f) { + engine.update(t, 0.5f); + } + + const auto& events = backend.get_events(); + printf(" Total events recorded: %zu\n", events.size()); + + // Verify timestamps are monotonically increasing (non-decreasing) + for (size_t i = 1; i < events.size(); ++i) { + assert(events[i].timestamp_sec >= events[i - 1].timestamp_sec); + } + + engine.shutdown(); + printf(" ✓ All timestamps monotonically increasing\n"); +} + +void test_seek_simulation() { + printf("Test: Seek/fast-forward simulation...\n"); + + MockAudioBackend backend; + audio_set_backend(&backend); + + audio_init(); + AudioEngine engine; + engine.init(); + + // Simulate seeking to t=3.0s by rendering silent audio + // This should trigger all patterns in range [0, 3.0] + const float seek_target = 3.0f; + + // Update tracker progressively (simulating real playback) + float t = 0.0f; + const float step = 0.1f; + while (t <= seek_target) { + engine.update(t, step); + // Simulate audio rendering + float dummy_buffer[512 * 2]; + engine.render(dummy_buffer, 512); + t += step; + } + + const auto& events = backend.get_events(); + printf(" Events triggered during seek to %.1fs: %zu\n", seek_target, + events.size()); + + // Should have triggered multiple patterns + assert(events.size() > 0); + + // All events should be before seek target time + for (const auto& evt : events) { + // Events can be slightly after due to synth processing + assert(evt.timestamp_sec <= seek_target + 0.5f); + } + + engine.shutdown(); + audio_shutdown(); + + printf(" ✓ Seek simulation works correctly\n"); +} + +void test_timestamp_clustering() { + printf("Test: Analyzing timestamp clustering...\n"); + + MockAudioBackend backend; + audio_set_backend(&backend); + + AudioEngine engine; + engine.init(); + + // Update through the first 4 seconds + for (float t = 0.0f; t <= 4.0f; t += 0.1f) { + engine.update(t, 0.1f); + } + + const auto& events = backend.get_events(); + printf(" Total events: %zu\n", events.size()); + + // Get unique timestamps + auto unique_timestamps = get_unique_timestamps(events, 0.001f); + printf(" Unique trigger times: %zu\n", unique_timestamps.size()); + + // For each unique timestamp, count how many events occurred + for (float ts : unique_timestamps) { + const int count = count_events_at_time(events, ts, 0.001f); + if (count > 1) { + printf(" %.3fs: %d simultaneous events\n", ts, count); + } + } + + engine.shutdown(); + printf(" ✓ Timestamp clustering analyzed\n"); +} + +void test_render_integration() { + printf("Test: Integration with audio_render_silent...\n"); + + MockAudioBackend backend; + audio_set_backend(&backend); + + audio_init(); + AudioEngine engine; + engine.init(); + + // Trigger some patterns + engine.update(0.0f, 0.0f); + const size_t events_before = backend.get_events().size(); + + // Render 1 second of silent audio + audio_render_silent(1.0f); + + // Check that backend time advanced + const float backend_time = backend.get_current_time(); + printf(" Backend time after 1s render: %.3fs\n", backend_time); + assert(backend_time >= 0.9f && backend_time <= 1.1f); + + // Trigger more patterns after time advance + engine.update(1.0f, 0.0f); + const size_t events_after = backend.get_events().size(); + + printf(" Events before: %zu, after: %zu\n", events_before, events_after); + assert(events_after >= events_before); + + engine.shutdown(); + audio_shutdown(); + + printf(" ✓ audio_render_silent integration works\n"); +} + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running Tracker Timing tests...\n\n"); + test_basic_event_recording(); + test_progressive_triggering(); + test_simultaneous_triggers(); + test_timing_monotonicity(); + test_seek_simulation(); + test_timestamp_clustering(); + test_render_integration(); + printf("\n✅ All Tracker Timing tests PASSED\n"); + return 0; +#else + printf("Tracker Timing tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} diff --git a/src/tests/audio/test_variable_tempo.cc b/src/tests/audio/test_variable_tempo.cc new file mode 100644 index 0000000..bbc9ebf --- /dev/null +++ b/src/tests/audio/test_variable_tempo.cc @@ -0,0 +1,291 @@ +// This file is part of the 64k demo project. +// It tests variable tempo system with music_time scaling. +// Verifies 2x speed-up and 2x slow-down reset tricks. + +#include "audio/audio.h" +#include "audio/audio_engine.h" +#include "audio/backend/mock_audio_backend.h" +#include "audio/tracker.h" +#include <assert.h> +#include <cmath> +#include <stdio.h> + +#if !defined(STRIP_ALL) + +// Helper: Setup audio engine for testing +static void setup_audio_test(MockAudioBackend& backend, AudioEngine& engine) { + audio_set_backend(&backend); + engine.init(); + engine.load_music_data(&g_tracker_score, g_tracker_samples, + g_tracker_sample_assets, g_tracker_samples_count); +} + +// Helper: Simulate tempo advancement with fixed steps +static void simulate_tempo(AudioEngine& engine, float& music_time, + float duration, float tempo_scale, float dt = 0.1f) { + const int steps = (int)(duration / dt); + for (int i = 0; i < steps; ++i) { + music_time += dt * tempo_scale; + engine.update(music_time, dt * tempo_scale); + } +} + +// Helper: Simulate tempo with variable scaling function +static void simulate_tempo_fn(AudioEngine& engine, float& music_time, + float& physical_time, float duration, float dt, + float (*tempo_fn)(float)) { + const int steps = (int)(duration / dt); + for (int i = 0; i < steps; ++i) { + physical_time += dt; + const float tempo_scale = tempo_fn(physical_time); + music_time += dt * tempo_scale; + engine.update(music_time, dt * tempo_scale); + } +} + +void test_basic_tempo_scaling() { + printf("Test: Basic tempo scaling (1.0x, 2.0x, 0.5x)...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + // Test 1: Normal tempo (1.0x) + { + backend.clear_events(); + float music_time = 0.0f; + simulate_tempo(engine, music_time, 1.0f, 1.0f); + printf(" 1.0x tempo: music_time = %.3f (expected ~1.0)\n", music_time); + assert(std::abs(music_time - 1.0f) < 0.01f); + } + + // Test 2: Fast tempo (2.0x) + { + backend.clear_events(); + engine.reset(); + float music_time = 0.0f; + simulate_tempo(engine, music_time, 1.0f, 2.0f); + printf(" 2.0x tempo: music_time = %.3f (expected ~2.0)\n", music_time); + assert(std::abs(music_time - 2.0f) < 0.01f); + } + + // Test 3: Slow tempo (0.5x) + { + backend.clear_events(); + engine.reset(); + float music_time = 0.0f; + simulate_tempo(engine, music_time, 1.0f, 0.5f); + printf(" 0.5x tempo: music_time = %.3f (expected ~0.5)\n", music_time); + assert(std::abs(music_time - 0.5f) < 0.01f); + } + + engine.shutdown(); + printf(" ✓ Basic tempo scaling works correctly\n"); +} + +void test_2x_speedup_reset_trick() { + printf("Test: 2x SPEED-UP reset trick...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + float music_time = 0.0f; + float physical_time = 0.0f; + const float dt = 0.1f; + + // Phase 1: Accelerate from 1.0x to 2.0x over 5 seconds + printf(" Phase 1: Accelerating 1.0x → 2.0x\n"); + auto accel_fn = [](float t) { return fminf(1.0f + (t / 5.0f), 2.0f); }; + simulate_tempo_fn(engine, music_time, physical_time, 5.0f, dt, accel_fn); + + const float tempo_scale = accel_fn(physical_time); + printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale, + music_time); + assert(tempo_scale >= 1.99f); + + // Phase 2: RESET - back to 1.0x tempo + printf(" Phase 2: RESET to 1.0x tempo\n"); + const float music_time_before_reset = music_time; + simulate_tempo(engine, music_time, 2.0f, 1.0f, dt); + + printf(" After reset + 2s: tempo=1.0x, music_time=%.3f\n", music_time); + const float music_time_delta = music_time - music_time_before_reset; + printf(" Music time delta: %.3f (expected ~2.0)\n", music_time_delta); + assert(std::abs(music_time_delta - 2.0f) < 0.1f); + + engine.shutdown(); + printf(" ✓ 2x speed-up reset trick verified\n"); +} + +void test_2x_slowdown_reset_trick() { + printf("Test: 2x SLOW-DOWN reset trick...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + float music_time = 0.0f; + float physical_time = 0.0f; + const float dt = 0.1f; + + // Phase 1: Decelerate from 1.0x to 0.5x over 5 seconds + printf(" Phase 1: Decelerating 1.0x → 0.5x\n"); + auto decel_fn = [](float t) { return fmaxf(1.0f - (t / 10.0f), 0.5f); }; + simulate_tempo_fn(engine, music_time, physical_time, 5.0f, dt, decel_fn); + + const float tempo_scale = decel_fn(physical_time); + printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale, + music_time); + assert(tempo_scale <= 0.51f); + + // Phase 2: RESET - back to 1.0x tempo + printf(" Phase 2: RESET to 1.0x tempo\n"); + const float music_time_before_reset = music_time; + simulate_tempo(engine, music_time, 2.0f, 1.0f, dt); + + printf(" After reset + 2s: tempo=1.0x, music_time=%.3f\n", music_time); + const float music_time_delta = music_time - music_time_before_reset; + printf(" Music time delta: %.3f (expected ~2.0)\n", music_time_delta); + assert(std::abs(music_time_delta - 2.0f) < 0.1f); + + engine.shutdown(); + printf(" ✓ 2x slow-down reset trick verified\n"); +} + +void test_pattern_density_swap() { + printf("Test: Pattern density swap at reset points...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + float music_time = 0.0f; + + // Phase 1: Sparse pattern at normal tempo + printf(" Phase 1: Sparse pattern, normal tempo\n"); + simulate_tempo(engine, music_time, 3.0f, 1.0f); + const size_t sparse_events = backend.get_events().size(); + printf(" Events during sparse phase: %zu\n", sparse_events); + + // Phase 2: Accelerate to 2.0x + printf(" Phase 2: Accelerating to 2.0x\n"); + simulate_tempo(engine, music_time, 2.0f, 2.0f); + const size_t events_at_2x = backend.get_events().size() - sparse_events; + printf(" Additional events during 2.0x: %zu\n", events_at_2x); + + // Phase 3: Reset to 1.0x + printf(" Phase 3: Reset to 1.0x (simulating denser pattern)\n"); + const size_t events_before_reset_phase = backend.get_events().size(); + simulate_tempo(engine, music_time, 2.0f, 1.0f); + const size_t events_after_reset = backend.get_events().size(); + + printf(" Events during reset phase: %zu\n", + events_after_reset - events_before_reset_phase); + assert(backend.get_events().size() > 0); + + engine.shutdown(); + printf(" ✓ Pattern density swap points verified\n"); +} + +void test_continuous_acceleration() { + printf("Test: Continuous acceleration from 0.5x to 2.0x...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + float music_time = 0.0f; + float physical_time = 0.0f; + const float dt = 0.05f; + const float min_tempo = 0.5f; + const float max_tempo = 2.0f; + + printf(" Accelerating 0.5x → 2.0x over 10 seconds\n"); + + auto accel_fn = [min_tempo, max_tempo](float t) { + const float progress = t / 10.0f; + return fmaxf( + min_tempo, + fminf(max_tempo, min_tempo + progress * (max_tempo - min_tempo))); + }; + + const int steps = (int)(10.0f / dt); + for (int i = 0; i < steps; ++i) { + physical_time += dt; + const float tempo_scale = accel_fn(physical_time); + music_time += dt * tempo_scale; + engine.update(music_time, dt * tempo_scale); + if (i % 50 == 0) { + printf(" t=%.1fs: tempo=%.2fx, music_time=%.3f\n", physical_time, + tempo_scale, music_time); + } + } + + const float final_tempo = accel_fn(physical_time); + printf(" Final: tempo=%.2fx, music_time=%.3f\n", final_tempo, music_time); + assert(final_tempo >= 1.99f); + + // Verify music_time (integral: 0.5*10 + 1.5*10²/(2*10) = 12.5) + const float expected_music_time = 12.5f; + printf(" Expected music_time: %.3f, actual: %.3f\n", expected_music_time, + music_time); + assert(std::abs(music_time - expected_music_time) < 0.5f); + + engine.shutdown(); + printf(" ✓ Continuous acceleration verified\n"); +} + +void test_oscillating_tempo() { + printf("Test: Oscillating tempo (sine wave)...\n"); + + MockAudioBackend backend; + AudioEngine engine; + setup_audio_test(backend, engine); + + float music_time = 0.0f; + float physical_time = 0.0f; + const float dt = 0.05f; + + printf(" Oscillating tempo: 0.8x ↔ 1.2x\n"); + + auto oscil_fn = [](float t) { return 1.0f + 0.2f * sinf(t * 2.0f); }; + + const int steps = 100; + for (int i = 0; i < steps; ++i) { + physical_time += dt; + const float tempo_scale = oscil_fn(physical_time); + music_time += dt * tempo_scale; + engine.update(music_time, dt * tempo_scale); + if (i % 25 == 0) { + printf(" t=%.2fs: tempo=%.3fx, music_time=%.3f\n", physical_time, + tempo_scale, music_time); + } + } + + printf(" Final: physical_time=%.2fs, music_time=%.3f (expected ~%.2f)\n", + physical_time, music_time, physical_time); + assert(std::abs(music_time - physical_time) < 0.5f); + + engine.shutdown(); + printf(" ✓ Oscillating tempo verified\n"); +} + +#endif /* !defined(STRIP_ALL) */ + +int main() { +#if !defined(STRIP_ALL) + printf("Running Variable Tempo tests...\n\n"); + test_basic_tempo_scaling(); + test_2x_speedup_reset_trick(); + test_2x_slowdown_reset_trick(); + test_pattern_density_swap(); + test_continuous_acceleration(); + test_oscillating_tempo(); + printf("\n✅ All Variable Tempo tests PASSED\n"); + return 0; +#else + printf("Variable Tempo tests skipped (STRIP_ALL enabled)\n"); + return 0; +#endif /* !defined(STRIP_ALL) */ +} 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) */ +} diff --git a/src/tests/audio/test_window.cc b/src/tests/audio/test_window.cc new file mode 100644 index 0000000..bac4a4b --- /dev/null +++ b/src/tests/audio/test_window.cc @@ -0,0 +1,28 @@ +// This file is part of the 64k demo project. +// It validates the mathematical properties of the Hamming window. +// Ensures the window peaks at the center and has correct symmetry. + +#include "audio/window.h" +#include <assert.h> +#include <math.h> +#include <stdio.h> + +int main() { + printf("Running HammingWindow tests...\n"); + + float window[WINDOW_SIZE]; + hamming_window_512(window); + + // Check symmetry + for (int i = 0; i < WINDOW_SIZE / 2; ++i) { + assert(fabsf(window[i] - window[WINDOW_SIZE - 1 - i]) < 1e-6f); + } + + // Check peak (should be at the center for even size, it's actually split + // between 255 and 256) + assert(window[255] > 0.99f); + assert(window[256] > 0.99f); + + printf("HammingWindow tests PASSED\n"); + return 0; +} |
