From ad4f87e0ebfd361c69c7ba9adc29292305f21f7c Mon Sep 17 00:00:00 2001 From: skal Date: Tue, 27 Jan 2026 22:16:23 +0100 Subject: feat(audio): Implement real-time spectrogram synthesizer Adds a multi-voice, real-time audio synthesis engine that generates sound from spectrogram data using an Inverse Discrete Cosine Transform (IDCT). Key features: - A thread-safe, double-buffered system for dynamically updating spectrograms in real-time without interrupting audio playback. - Core DSP components: FDCT, IDCT, and Hamming window functions. - A simple sequencer in the main loop to demonstrate scripted audio events and dynamic updates. - Unit tests for the new synth engine and Hamming window, integrated with CTest. - A file documenting the build process, features, and how to run tests. --- src/tests/test_synth.cpp | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/tests/test_synth.cpp (limited to 'src/tests/test_synth.cpp') diff --git a/src/tests/test_synth.cpp b/src/tests/test_synth.cpp new file mode 100644 index 0000000..04b0373 --- /dev/null +++ b/src/tests/test_synth.cpp @@ -0,0 +1,107 @@ +#include "audio/synth.h" +#include +#include +#include +#include + +// A simple floating point comparison with a tolerance +bool is_close(float a, float b, float epsilon = 1e-6f) { + return fabsf(a - b) < epsilon; +} + +void test_registration() { + synth_init(); + printf("Running test: Registration...\n"); + + float spec_buf_a[DCT_SIZE], spec_buf_b[DCT_SIZE]; + Spectrogram spec = { spec_buf_a, spec_buf_b, 1 }; + + // Fill up all slots + for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { + int id = synth_register_spectrogram(&spec); + assert(id == i); + } + + // Next one should fail + int fail_id = synth_register_spectrogram(&spec); + assert(fail_id == -1); + + // Unregister one + synth_unregister_spectrogram(5); + + // Now we should be able to register again in the freed slot + int new_id = synth_register_spectrogram(&spec); + assert(new_id == 5); + + printf("...Registration test PASSED.\n"); +} + +void test_render() { + synth_init(); + printf("Running test: Render...\n"); + + float spec_buf_a[DCT_SIZE] = {0}; + Spectrogram spec = { spec_buf_a, nullptr, 1 }; + + // Create a simple spectrum with one active bin + spec_buf_a[10] = 1.0f; + + int id = synth_register_spectrogram(&spec); + assert(id != -1); + + synth_trigger_voice(id, 1.0f, 0.0f); + + float output_buffer[DCT_SIZE * 2] = {0}; // Stereo + synth_render(output_buffer, DCT_SIZE); + + float total_energy = 0.0f; + for(int i = 0; i < DCT_SIZE * 2; ++i) { + total_energy += fabsf(output_buffer[i]); + } + + // If we rendered a sound, the buffer should not be silent + assert(total_energy > 0.01f); + + printf("...Render test PASSED.\n"); +} + +void test_update() { + synth_init(); + printf("Running test: Update...\n"); + float spec_buf_a[DCT_SIZE] = {0}; + float spec_buf_b[DCT_SIZE] = {0}; + Spectrogram spec = { spec_buf_a, spec_buf_b, 1 }; + + spec_buf_a[10] = 1.0f; // Original sound + spec_buf_b[20] = 1.0f; // Updated sound + + int id = synth_register_spectrogram(&spec); + + // Begin update - should get back buffer B + float* back_buffer = synth_begin_update(id); + assert(back_buffer == spec_buf_b); + + // We could modify it here, but it's already different. + // Let's just commit. + synth_commit_update(id); + + // Now if we trigger a voice, it should play from buffer B. + // To test this, we'd need to analyze the output, which is complex. + // For this test, we'll just ensure the mechanism runs and we can + // begin an update on the *new* back buffer (A). + back_buffer = synth_begin_update(id); + assert(back_buffer == spec_buf_a); + + printf("...Update test PASSED.\n"); +} + +int main() { + test_registration(); + test_render(); + test_update(); + + synth_shutdown(); + + printf("\nAll synth tests passed!\n"); + return 0; +} -- cgit v1.2.3