summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt29
-rw-r--r--HOWTO.md40
-rw-r--r--src/audio/audio.cpp23
-rw-r--r--src/audio/dct.h6
-rw-r--r--src/audio/fdct.cpp17
-rw-r--r--src/audio/idct.cpp17
-rw-r--r--src/audio/synth.cpp157
-rw-r--r--src/audio/synth.h24
-rw-r--r--src/audio/window.cpp9
-rw-r--r--src/audio/window.h5
-rw-r--r--src/main.cpp50
-rw-r--r--src/platform.cpp4
-rw-r--r--src/platform.h2
-rw-r--r--src/tests/test_synth.cpp107
-rw-r--r--src/tests/test_window.cpp34
15 files changed, 510 insertions, 14 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b5ac51e..c9b8cad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,10 @@ add_executable(demo64k
src/platform.cpp
src/gpu/gpu.cpp
src/audio/audio.cpp
+ src/audio/fdct.cpp
+ src/audio/idct.cpp
+ src/audio/window.cpp
+ src/audio/synth.cpp
)
target_include_directories(demo64k PRIVATE
@@ -30,3 +34,28 @@ if (DEMO_SIZE_OPT)
target_link_options(demo64k PRIVATE -Wl,--gc-sections -s)
endif()
endif()
+
+option(DEMO_BUILD_TESTS "Build tests" OFF)
+enable_testing()
+
+if(DEMO_BUILD_TESTS)
+ add_executable(test_window
+ src/tests/test_window.cpp
+ src/audio/window.cpp
+ )
+ target_include_directories(test_window PRIVATE
+ src
+ )
+ add_test(NAME HammingWindowTest COMMAND test_window)
+
+ add_executable(test_synth
+ src/tests/test_synth.cpp
+ src/audio/synth.cpp
+ src/audio/idct.cpp
+ src/audio/window.cpp
+ )
+ target_include_directories(test_synth PRIVATE
+ src
+ )
+ add_test(NAME SynthEngineTest COMMAND test_synth)
+endif()
diff --git a/HOWTO.md b/HOWTO.md
new file mode 100644
index 0000000..afd517e
--- /dev/null
+++ b/HOWTO.md
@@ -0,0 +1,40 @@
+# How To
+
+This document describes the common commands for building and testing the project.
+
+## Features
+
+* **Real-time Audio Synthesis**: The demo features a multi-voice synthesizer that generates audio in real-time from spectrograms.
+* **Dynamic Sound Updates**: Spectrograms can be updated dynamically and safely during runtime for evolving soundscapes.
+
+## Building
+
+### Debug Build
+
+```bash
+cmake -S . -B build
+cmake --build build
+```
+
+### Size-Optimized Build
+
+```bash
+cmake -S . -B build -DDEMO_SIZE_OPT=ON
+cmake --build build
+```
+
+## Testing
+
+To build and run the tests, you need to enable the `DEMO_BUILD_TESTS` option in CMake.
+
+Available test suites:
+* `HammingWindowTest`: Verifies the properties of the Hamming window function.
+* `SynthEngineTest`: Verifies the core functionality of the audio synthesizer.
+
+```bash
+cmake -S . -B build -DDEMO_BUILD_TESTS=ON
+cmake --build build
+cd build
+ctest
+cd ..
+```
diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp
index 9e778f1..318ccb8 100644
--- a/src/audio/audio.cpp
+++ b/src/audio/audio.cpp
@@ -1,27 +1,19 @@
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
+#include "synth.h"
#include <math.h>
static ma_device device;
-static float phase = 0.0f;
static void audio_callback(ma_device*, void* output, const void*, ma_uint32 frames) {
- int16_t* out = (int16_t*)output;
- const float freq = 440.0f;
- const float sr = 32000.0f;
-
- for (ma_uint32 i = 0; i < frames; i++) {
- float s = sinf(phase) * 0.2f;
- phase += 2.0f * 3.14159265f * freq / sr;
- if (phase > 2.0f * 3.14159265f) phase -= 2.0f * 3.14159265f;
- out[i] = (int16_t)(s * 32767.0f);
- }
+ synth_render((float*)output, frames);
}
void audio_init() {
+ synth_init();
ma_device_config cfg = ma_device_config_init(ma_device_type_playback);
- cfg.playback.format = ma_format_s16;
- cfg.playback.channels = 1;
+ cfg.playback.format = ma_format_f32;
+ cfg.playback.channels = 2;
cfg.sampleRate = 32000;
cfg.dataCallback = audio_callback;
@@ -30,4 +22,7 @@ void audio_init() {
}
void audio_update() {}
-void audio_shutdown() { ma_device_uninit(&device); }
+void audio_shutdown() {
+ synth_shutdown();
+ ma_device_uninit(&device);
+}
diff --git a/src/audio/dct.h b/src/audio/dct.h
new file mode 100644
index 0000000..3e51884
--- /dev/null
+++ b/src/audio/dct.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#define DCT_SIZE 512
+
+void fdct_512(const float input[DCT_SIZE], float output[DCT_SIZE]);
+void idct_512(const float input[DCT_SIZE], float output[DCT_SIZE]);
diff --git a/src/audio/fdct.cpp b/src/audio/fdct.cpp
new file mode 100644
index 0000000..50ab458
--- /dev/null
+++ b/src/audio/fdct.cpp
@@ -0,0 +1,17 @@
+#include "dct.h"
+#include "util/math.h"
+#include <math.h>
+
+void fdct_512(const float input[DCT_SIZE], float output[DCT_SIZE]) {
+ float scale_k0 = sqrtf(1.0f / DCT_SIZE);
+ float scale_kn = sqrtf(2.0f / DCT_SIZE);
+
+ for (int k = 0; k < DCT_SIZE; ++k) {
+ float sum = 0.0f;
+ for (int n = 0; n < DCT_SIZE; ++n) {
+ sum += input[n] * cosf((PI / DCT_SIZE) * (n + 0.5f) * k);
+ }
+ float scale = (k == 0) ? scale_k0 : scale_kn;
+ output[k] = sum * scale;
+ }
+}
diff --git a/src/audio/idct.cpp b/src/audio/idct.cpp
new file mode 100644
index 0000000..a32f92e
--- /dev/null
+++ b/src/audio/idct.cpp
@@ -0,0 +1,17 @@
+#include "dct.h"
+#include "util/math.h"
+#include <math.h>
+
+void idct_512(const float input[DCT_SIZE], float output[DCT_SIZE]) {
+ float scale_k0 = sqrtf(1.0f / DCT_SIZE);
+ float scale_kn = sqrtf(2.0f / DCT_SIZE);
+
+ for (int n = 0; n < DCT_SIZE; ++n) {
+ float sum = 0.0f;
+ for (int k = 0; k < DCT_SIZE; ++k) {
+ float scale = (k == 0) ? scale_k0 : scale_kn;
+ sum += scale * input[k] * cosf((PI / DCT_SIZE) * (n + 0.5f) * k);
+ }
+ output[n] = sum;
+ }
+}
diff --git a/src/audio/synth.cpp b/src/audio/synth.cpp
new file mode 100644
index 0000000..f009876
--- /dev/null
+++ b/src/audio/synth.cpp
@@ -0,0 +1,157 @@
+#include "synth.h"
+#include "audio/window.h"
+#include <string.h> // For memset
+#include <atomic>
+
+struct Voice {
+ bool active;
+ int spectrogram_id;
+ float volume;
+ float pan_left;
+ float pan_right;
+
+ int current_spectral_frame;
+ int total_spectral_frames;
+
+ float time_domain_buffer[DCT_SIZE];
+ int buffer_pos;
+
+ const volatile float* active_spectral_data;
+};
+
+static struct {
+ Spectrogram spectrograms[MAX_SPECTROGRAMS];
+ const volatile float* active_spectrogram_data[MAX_SPECTROGRAMS];
+ bool spectrogram_registered[MAX_SPECTROGRAMS];
+} g_synth_data;
+
+static Voice g_voices[MAX_VOICES];
+
+void synth_init() {
+ memset(&g_synth_data, 0, sizeof(g_synth_data));
+ memset(g_voices, 0, sizeof(g_voices));
+}
+
+void synth_shutdown() {
+ // Nothing to do here since we are not allocating memory
+}
+
+int synth_register_spectrogram(const Spectrogram* spec) {
+ for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
+ if (!g_synth_data.spectrogram_registered[i]) {
+ g_synth_data.spectrograms[i] = *spec;
+ g_synth_data.active_spectrogram_data[i] = spec->spectral_data_a;
+ g_synth_data.spectrogram_registered[i] = true;
+ return i;
+ }
+ }
+ return -1; // No free slots
+}
+
+void synth_unregister_spectrogram(int spectrogram_id) {
+ if (spectrogram_id >= 0 && spectrogram_id < MAX_SPECTROGRAMS) {
+ g_synth_data.spectrogram_registered[spectrogram_id] = false;
+ }
+}
+
+float* synth_begin_update(int spectrogram_id) {
+ if (spectrogram_id < 0 || spectrogram_id >= MAX_SPECTROGRAMS || !g_synth_data.spectrogram_registered[spectrogram_id]) {
+ return nullptr;
+ }
+
+ const volatile float* active_ptr = g_synth_data.active_spectrogram_data[spectrogram_id];
+
+ if (active_ptr == g_synth_data.spectrograms[spectrogram_id].spectral_data_a) {
+ return g_synth_data.spectrograms[spectrogram_id].spectral_data_b;
+ } else {
+ return g_synth_data.spectrograms[spectrogram_id].spectral_data_a;
+ }
+}
+
+void synth_commit_update(int spectrogram_id) {
+ if (spectrogram_id < 0 || spectrogram_id >= MAX_SPECTROGRAMS || !g_synth_data.spectrogram_registered[spectrogram_id]) {
+ return;
+ }
+
+ const volatile float* old_active_ptr = g_synth_data.active_spectrogram_data[spectrogram_id];
+ const float* new_active_ptr = (old_active_ptr == g_synth_data.spectrograms[spectrogram_id].spectral_data_a)
+ ? g_synth_data.spectrograms[spectrogram_id].spectral_data_b
+ : g_synth_data.spectrograms[spectrogram_id].spectral_data_a;
+
+ // Atomic swap using GCC/Clang builtins for thread safety
+ __atomic_store_n((const float**)&g_synth_data.active_spectrogram_data[spectrogram_id], new_active_ptr, __ATOMIC_RELEASE);
+}
+
+
+void synth_trigger_voice(int spectrogram_id, float volume, float pan) {
+ if (spectrogram_id < 0 || spectrogram_id >= MAX_SPECTROGRAMS || !g_synth_data.spectrogram_registered[spectrogram_id]) {
+ return;
+ }
+
+ for (int i = 0; i < MAX_VOICES; ++i) {
+ if (!g_voices[i].active) {
+ Voice& v = g_voices[i];
+ v.active = true;
+ v.spectrogram_id = spectrogram_id;
+ v.volume = volume;
+
+ // Simple linear panning
+ v.pan_left = (pan > 0.0f) ? (1.0f - pan) : 1.0f;
+ v.pan_right = (pan < 0.0f) ? (1.0f + pan) : 1.0f;
+
+ v.current_spectral_frame = 0;
+ v.total_spectral_frames = g_synth_data.spectrograms[spectrogram_id].num_frames;
+ v.buffer_pos = DCT_SIZE; // Force IDCT on first render
+ v.active_spectral_data = g_synth_data.active_spectrogram_data[spectrogram_id];
+
+ return; // Voice triggered
+ }
+ }
+}
+
+
+void synth_render(float* output_buffer, int num_frames) {
+ float window[WINDOW_SIZE];
+ hamming_window_512(window);
+
+ for (int i = 0; i < num_frames; ++i) {
+ float left_sample = 0.0f;
+ float right_sample = 0.0f;
+
+ for (int v_idx = 0; v_idx < MAX_VOICES; ++v_idx) {
+ Voice& v = g_voices[v_idx];
+ if (!v.active) continue;
+
+ if (v.buffer_pos >= DCT_SIZE) {
+ if (v.current_spectral_frame >= v.total_spectral_frames) {
+ v.active = false;
+ continue;
+ }
+
+ // Fetch the latest active spectrogram pointer for this voice
+ v.active_spectral_data = g_synth_data.active_spectrogram_data[v.spectrogram_id];
+
+ const float* spectral_frame = (const float*)v.active_spectral_data + (v.current_spectral_frame * DCT_SIZE);
+
+ float windowed_frame[DCT_SIZE];
+ for(int j=0; j<DCT_SIZE; ++j) {
+ windowed_frame[j] = spectral_frame[j] * window[j];
+ }
+
+ idct_512(windowed_frame, v.time_domain_buffer);
+
+ v.buffer_pos = 0;
+ v.current_spectral_frame++;
+ }
+
+ float voice_sample = v.time_domain_buffer[v.buffer_pos] * v.volume;
+ left_sample += voice_sample * v.pan_left;
+ right_sample += voice_sample * v.pan_right;
+
+ v.buffer_pos++;
+ }
+
+ output_buffer[i * 2] = left_sample;
+ output_buffer[i * 2 + 1] = right_sample;
+ }
+}
diff --git a/src/audio/synth.h b/src/audio/synth.h
new file mode 100644
index 0000000..ce9825d
--- /dev/null
+++ b/src/audio/synth.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "dct.h"
+
+#define MAX_SPECTROGRAMS 16
+#define MAX_VOICES 16
+
+struct Spectrogram {
+ float* spectral_data_a;
+ float* spectral_data_b;
+ int num_frames;
+};
+
+void synth_init();
+void synth_shutdown();
+
+int synth_register_spectrogram(const Spectrogram* spec);
+void synth_unregister_spectrogram(int spectrogram_id);
+
+float* synth_begin_update(int spectrogram_id);
+void synth_commit_update(int spectrogram_id);
+
+void synth_trigger_voice(int spectrogram_id, float volume, float pan);
+void synth_render(float* output_buffer, int num_frames);
diff --git a/src/audio/window.cpp b/src/audio/window.cpp
new file mode 100644
index 0000000..3f36480
--- /dev/null
+++ b/src/audio/window.cpp
@@ -0,0 +1,9 @@
+#include "window.h"
+#include "util/math.h"
+#include <math.h>
+
+void hamming_window_512(float window[WINDOW_SIZE]) {
+ for (int i = 0; i < WINDOW_SIZE; ++i) {
+ window[i] = 0.54f - 0.46f * cosf(2.0f * PI * i / (WINDOW_SIZE - 1));
+ }
+}
diff --git a/src/audio/window.h b/src/audio/window.h
new file mode 100644
index 0000000..8cb5dd8
--- /dev/null
+++ b/src/audio/window.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#define WINDOW_SIZE 512
+
+void hamming_window_512(float window[WINDOW_SIZE]);
diff --git a/src/main.cpp b/src/main.cpp
index 3b61e1e..c1e2789 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,14 +1,64 @@
#include "platform.h"
#include "gpu/gpu.h"
#include "audio/audio.h"
+#include "audio/synth.h"
+#include "util/math.h"
+#include <string.h>
+#include <math.h>
+
+#define DEMO_BPM 120.0f
+#define SECONDS_PER_BEAT (60.0f / DEMO_BPM)
+#define SPEC_FRAMES 16
+
+static float g_spec_buffer_a[SPEC_FRAMES * DCT_SIZE];
+static float g_spec_buffer_b[SPEC_FRAMES * DCT_SIZE];
+
+void generate_tone(float* buffer, float freq) {
+ memset(buffer, 0, SPEC_FRAMES * DCT_SIZE * sizeof(float));
+ for (int frame = 0; frame < SPEC_FRAMES; ++frame) {
+ float* spec_frame = buffer + frame * DCT_SIZE;
+ float amplitude = powf(1.0f - (float)frame / SPEC_FRAMES, 2.0f);
+
+ int bin = (int)(freq / (32000.0f / 2.0f) * DCT_SIZE);
+ if (bin > 0 && bin < DCT_SIZE) {
+ spec_frame[bin] = amplitude;
+ }
+ }
+}
int main() {
platform_init();
gpu_init(platform_get_window());
audio_init();
+ generate_tone(g_spec_buffer_a, 440.0f); // A4
+ generate_tone(g_spec_buffer_b, 880.0f); // A5
+
+ Spectrogram spec = { g_spec_buffer_a, g_spec_buffer_b, SPEC_FRAMES };
+ int tone_id = synth_register_spectrogram(&spec);
+
+ double last_beat_time = 0.0;
+ int beat_count = 0;
+
while (!platform_should_close()) {
platform_poll();
+
+ double current_time = platform_get_time();
+ if (current_time - last_beat_time > SECONDS_PER_BEAT) {
+ synth_trigger_voice(tone_id, 0.5f, 0.0f);
+ last_beat_time = current_time;
+ beat_count++;
+
+ if (beat_count == 8) {
+ // Time to update the sound!
+ float* back_buffer = synth_begin_update(tone_id);
+ if (back_buffer) {
+ generate_tone(back_buffer, 220.0f); // A3
+ synth_commit_update(tone_id);
+ }
+ }
+ }
+
gpu_draw();
audio_update();
}
diff --git a/src/platform.cpp b/src/platform.cpp
index adb41d2..237aaf8 100644
--- a/src/platform.cpp
+++ b/src/platform.cpp
@@ -24,3 +24,7 @@ bool platform_should_close() {
GLFWwindow* platform_get_window() {
return window;
}
+
+double platform_get_time() {
+ return glfwGetTime();
+}
diff --git a/src/platform.h b/src/platform.h
index 0ebeb59..ed08b19 100644
--- a/src/platform.h
+++ b/src/platform.h
@@ -6,3 +6,5 @@ void platform_shutdown();
void platform_poll();
bool platform_should_close();
GLFWwindow* platform_get_window();
+double platform_get_time();
+
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 <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <math.h>
+
+// 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;
+}
diff --git a/src/tests/test_window.cpp b/src/tests/test_window.cpp
new file mode 100644
index 0000000..1667dab
--- /dev/null
+++ b/src/tests/test_window.cpp
@@ -0,0 +1,34 @@
+#include "audio/window.h"
+#include <stdio.h>
+#include <math.h>
+#include <assert.h>
+
+// A simple floating point comparison with a tolerance
+bool is_close(float a, float b, float epsilon = 1e-6f) {
+ return fabsf(a - b) < epsilon;
+}
+
+int main() {
+ float window[WINDOW_SIZE];
+ hamming_window_512(window);
+
+ // Test 1: Window should start and end at the same small value
+ assert(is_close(window[0], 0.08f));
+ assert(is_close(window[WINDOW_SIZE - 1], 0.08f));
+ printf("Test 1 passed: Window start and end values are correct.\n");
+
+ // Test 2: Window should be symmetric
+ for (int i = 0; i < WINDOW_SIZE / 2; ++i) {
+ assert(is_close(window[i], window[WINDOW_SIZE - 1 - i]));
+ }
+ printf("Test 2 passed: Window is symmetric.\n");
+
+ // Test 3: The two middle points of the even-sized window should be equal and the peak.
+ assert(is_close(window[WINDOW_SIZE / 2 - 1], window[WINDOW_SIZE / 2]));
+ assert(window[WINDOW_SIZE / 2] > window[WINDOW_SIZE / 2 - 2]); // Should be greater than neighbors
+ printf("Test 3 passed: Window peak is correct for even size.\n");
+
+ printf("All tests passed for Hamming window!\n");
+
+ return 0;
+}