summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-01-27 23:09:27 +0100
committerskal <pascal.massimino@gmail.com>2026-01-27 23:10:49 +0100
commit9dcf94ab01269311b4e5d39be23c95560904c626 (patch)
treeac271af3d51d7ee1bce6827f81e97f1f463336db /src
parent364d9e60e3c27cb131a598fe5f83deb74493319f (diff)
feat: Implement spectool & specview; refactor coding style; update docs
This commit introduces new tools for spectrogram manipulation and visualization, establishes a consistent coding style, and updates project documentation. Key changes include: - **Spectrogram Tools: - : A command-line utility for analyzing WAV/MP3 files into custom spectrogram format and playing back these spectrograms via the synth engine. - : A command-line tool for visualizing spectrogram files as ASCII art in the console. - **Coding Style Enforcement: - Added a configuration file enforcing LLVM-based style with 2-space indentation, no tabs, and an 80-column line limit. - Renamed all C++ source files from to for project consistency. - Applied automatic formatting using exit across the entire codebase. - **Documentation & Workflow: - Created to define a commit policy requiring tests to pass before committing. - Updated with instructions for building and using and , and referenced . - Updated and to reflect the new tools, audio architecture decisions (real-time additive synthesis, double-buffering for dynamic updates, WAV/MP3 support), coding style, and development workflow. - **Build System: - Modified to: - Include new targets for and under the option. - Update source file extensions to . - Add a new end-to-end test for to the suite.
Diffstat (limited to 'src')
-rw-r--r--src/audio/audio.cc30
-rw-r--r--src/audio/audio.cpp28
-rw-r--r--src/audio/fdct.cc17
-rw-r--r--src/audio/fdct.cpp17
-rw-r--r--src/audio/idct.cc17
-rw-r--r--src/audio/idct.cpp17
-rw-r--r--src/audio/synth.cc179
-rw-r--r--src/audio/synth.cpp167
-rw-r--r--src/audio/synth.h12
-rw-r--r--src/audio/window.cc9
-rw-r--r--src/audio/window.cpp9
-rw-r--r--src/gpu/gpu.cc8
-rw-r--r--src/gpu/gpu.cpp5
-rw-r--r--src/gpu/gpu.h2
-rw-r--r--src/main.cc70
-rw-r--r--src/main.cpp70
-rw-r--r--src/platform.cc30
-rw-r--r--src/platform.cpp30
-rw-r--r--src/platform.h3
-rw-r--r--src/tests/test_spectool.cc95
-rw-r--r--src/tests/test_spectool.cpp91
-rw-r--r--src/tests/test_synth.cc107
-rw-r--r--src/tests/test_synth.cpp107
-rw-r--r--src/tests/test_window.cc36
-rw-r--r--src/tests/test_window.cpp34
25 files changed, 606 insertions, 584 deletions
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
new file mode 100644
index 0000000..0eceed5
--- /dev/null
+++ b/src/audio/audio.cc
@@ -0,0 +1,30 @@
+#define MINIAUDIO_IMPLEMENTATION
+#include "miniaudio.h"
+#include "synth.h"
+#include <math.h>
+
+static ma_device device;
+
+static void audio_callback(ma_device *, void *output, const void *,
+ ma_uint32 frames) {
+ 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_f32;
+ cfg.playback.channels = 2;
+ cfg.sampleRate = 32000;
+ cfg.dataCallback = audio_callback;
+
+ ma_device_init(nullptr, &cfg, &device);
+ ma_device_start(&device);
+}
+
+void audio_update() {
+}
+void audio_shutdown() {
+ synth_shutdown();
+ ma_device_uninit(&device);
+}
diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp
deleted file mode 100644
index 318ccb8..0000000
--- a/src/audio/audio.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-#define MINIAUDIO_IMPLEMENTATION
-#include "miniaudio.h"
-#include "synth.h"
-#include <math.h>
-
-static ma_device device;
-
-static void audio_callback(ma_device*, void* output, const void*, ma_uint32 frames) {
- 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_f32;
- cfg.playback.channels = 2;
- cfg.sampleRate = 32000;
- cfg.dataCallback = audio_callback;
-
- ma_device_init(nullptr, &cfg, &device);
- ma_device_start(&device);
-}
-
-void audio_update() {}
-void audio_shutdown() {
- synth_shutdown();
- ma_device_uninit(&device);
-}
diff --git a/src/audio/fdct.cc b/src/audio/fdct.cc
new file mode 100644
index 0000000..5cf0211
--- /dev/null
+++ b/src/audio/fdct.cc
@@ -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/fdct.cpp b/src/audio/fdct.cpp
deleted file mode 100644
index 50ab458..0000000
--- a/src/audio/fdct.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#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.cc b/src/audio/idct.cc
new file mode 100644
index 0000000..5d6bde0
--- /dev/null
+++ b/src/audio/idct.cc
@@ -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/idct.cpp b/src/audio/idct.cpp
deleted file mode 100644
index a32f92e..0000000
--- a/src/audio/idct.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#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.cc b/src/audio/synth.cc
new file mode 100644
index 0000000..451fcdb
--- /dev/null
+++ b/src/audio/synth.cc
@@ -0,0 +1,179 @@
+#include "synth.h"
+#include "audio/window.h"
+#include <atomic>
+#include <string.h> // For memset
+
+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;
+ }
+}
+
+int synth_get_active_voice_count() {
+ int count = 0;
+ for (int i = 0; i < MAX_VOICES; ++i) {
+ if (g_voices[i].active) {
+ count++;
+ }
+ }
+ return count;
+}
diff --git a/src/audio/synth.cpp b/src/audio/synth.cpp
deleted file mode 100644
index 3c20b0b..0000000
--- a/src/audio/synth.cpp
+++ /dev/null
@@ -1,167 +0,0 @@
-#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;
- }
-}
-
-int synth_get_active_voice_count() {
- int count = 0;
- for (int i = 0; i < MAX_VOICES; ++i) {
- if (g_voices[i].active) {
- count++;
- }
- }
- return count;
-}
diff --git a/src/audio/synth.h b/src/audio/synth.h
index 17770a7..eadde10 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -6,20 +6,20 @@
#define MAX_VOICES 16
struct Spectrogram {
- float* spectral_data_a;
- float* spectral_data_b;
- int num_frames;
+ float *spectral_data_a;
+ float *spectral_data_b;
+ int num_frames;
};
void synth_init();
void synth_shutdown();
-int synth_register_spectrogram(const Spectrogram* spec);
+int synth_register_spectrogram(const Spectrogram *spec);
void synth_unregister_spectrogram(int spectrogram_id);
-float* synth_begin_update(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);
+void synth_render(float *output_buffer, int num_frames);
int synth_get_active_voice_count();
diff --git a/src/audio/window.cc b/src/audio/window.cc
new file mode 100644
index 0000000..d92f371
--- /dev/null
+++ b/src/audio/window.cc
@@ -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.cpp b/src/audio/window.cpp
deleted file mode 100644
index 3f36480..0000000
--- a/src/audio/window.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#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/gpu/gpu.cc b/src/gpu/gpu.cc
new file mode 100644
index 0000000..e538d5d
--- /dev/null
+++ b/src/gpu/gpu.cc
@@ -0,0 +1,8 @@
+#include "gpu.h"
+
+void gpu_init(GLFWwindow *) {
+}
+void gpu_draw() {
+}
+void gpu_shutdown() {
+}
diff --git a/src/gpu/gpu.cpp b/src/gpu/gpu.cpp
deleted file mode 100644
index 4e48887..0000000
--- a/src/gpu/gpu.cpp
+++ /dev/null
@@ -1,5 +0,0 @@
-#include "gpu.h"
-
-void gpu_init(GLFWwindow*) {}
-void gpu_draw() {}
-void gpu_shutdown() {}
diff --git a/src/gpu/gpu.h b/src/gpu/gpu.h
index 02204af..d36f618 100644
--- a/src/gpu/gpu.h
+++ b/src/gpu/gpu.h
@@ -1,6 +1,6 @@
#pragma once
struct GLFWwindow;
-void gpu_init(GLFWwindow* window);
+void gpu_init(GLFWwindow *window);
void gpu_draw();
void gpu_shutdown();
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..56d755c
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,70 @@
+#include "audio/audio.h"
+#include "audio/synth.h"
+#include "gpu/gpu.h"
+#include "platform.h"
+#include "util/math.h"
+#include <math.h>
+#include <string.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();
+ }
+
+ audio_shutdown();
+ gpu_shutdown();
+ platform_shutdown();
+ return 0;
+}
diff --git a/src/main.cpp b/src/main.cpp
deleted file mode 100644
index c1e2789..0000000
--- a/src/main.cpp
+++ /dev/null
@@ -1,70 +0,0 @@
-#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();
- }
-
- audio_shutdown();
- gpu_shutdown();
- platform_shutdown();
- return 0;
-}
diff --git a/src/platform.cc b/src/platform.cc
new file mode 100644
index 0000000..bfb566c
--- /dev/null
+++ b/src/platform.cc
@@ -0,0 +1,30 @@
+#include "platform.h"
+#include <GLFW/glfw3.h>
+
+static GLFWwindow *window = nullptr;
+
+void platform_init() {
+ glfwInit();
+ window = glfwCreateWindow(1280, 720, "demo64k", nullptr, nullptr);
+}
+
+void platform_shutdown() {
+ glfwDestroyWindow(window);
+ glfwTerminate();
+}
+
+void platform_poll() {
+ glfwPollEvents();
+}
+
+bool platform_should_close() {
+ return glfwWindowShouldClose(window);
+}
+
+GLFWwindow *platform_get_window() {
+ return window;
+}
+
+double platform_get_time() {
+ return glfwGetTime();
+}
diff --git a/src/platform.cpp b/src/platform.cpp
deleted file mode 100644
index 237aaf8..0000000
--- a/src/platform.cpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#include "platform.h"
-#include <GLFW/glfw3.h>
-
-static GLFWwindow* window = nullptr;
-
-void platform_init() {
- glfwInit();
- window = glfwCreateWindow(1280, 720, "demo64k", nullptr, nullptr);
-}
-
-void platform_shutdown() {
- glfwDestroyWindow(window);
- glfwTerminate();
-}
-
-void platform_poll() {
- glfwPollEvents();
-}
-
-bool platform_should_close() {
- return glfwWindowShouldClose(window);
-}
-
-GLFWwindow* platform_get_window() {
- return window;
-}
-
-double platform_get_time() {
- return glfwGetTime();
-}
diff --git a/src/platform.h b/src/platform.h
index ed08b19..4d9a1f9 100644
--- a/src/platform.h
+++ b/src/platform.h
@@ -5,6 +5,5 @@ void platform_init();
void platform_shutdown();
void platform_poll();
bool platform_should_close();
-GLFWwindow* platform_get_window();
+GLFWwindow *platform_get_window();
double platform_get_time();
-
diff --git a/src/tests/test_spectool.cc b/src/tests/test_spectool.cc
new file mode 100644
index 0000000..7581f0d
--- /dev/null
+++ b/src/tests/test_spectool.cc
@@ -0,0 +1,95 @@
+#include "audio/dct.h" // For DCT_SIZE
+#include "miniaudio.h"
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h> // For system()
+#include <vector>
+
+// Re-defining the header here to avoid dependency on the tool's source
+struct SpecHeader {
+ char magic[4];
+ int32_t version;
+ int32_t dct_size;
+ int32_t num_frames;
+};
+
+#define TEST_SAMPLE_RATE 32000
+#define TEST_DURATION_SECONDS 1
+#define TEST_FREQ 440.0f
+#define PI 3.14159265f
+
+int main() {
+ printf("Running spectool end-to-end test...\n");
+
+ const char *wav_path = "test_signal.wav";
+ const char *spec_path = "test_signal.spec";
+ const char *spectool_path =
+ "./spectool"; // Assumes ctest is run from `build` dir
+
+ // 1. Generate and save a WAV file
+ ma_encoder_config enc_config = ma_encoder_config_init(
+ ma_encoding_format_wav, ma_format_f32, 1, TEST_SAMPLE_RATE);
+ ma_encoder encoder;
+ if (ma_encoder_init_file(wav_path, &enc_config, &encoder) != MA_SUCCESS) {
+ printf("TEST FAILED: Could not initialize WAV encoder.\n");
+ return 1;
+ }
+
+ std::vector<float> pcm_data(TEST_SAMPLE_RATE * TEST_DURATION_SECONDS);
+ for (size_t i = 0; i < pcm_data.size(); ++i) {
+ pcm_data[i] = 0.5f * sinf(2.0f * PI * TEST_FREQ * i / TEST_SAMPLE_RATE);
+ }
+ ma_uint64 frames_written;
+ ma_encoder_write_pcm_frames(&encoder, pcm_data.data(), pcm_data.size(),
+ &frames_written);
+ ma_encoder_uninit(&encoder);
+ printf(" Generated %s\n", wav_path);
+
+ // 2. Run spectool analyze
+ char command[512];
+ snprintf(command, sizeof(command), "%s analyze %s %s", spectool_path,
+ wav_path, spec_path);
+ printf(" Executing: %s\n", command);
+ int return_code = system(command);
+ assert(return_code == 0);
+ printf(" spectool executed successfully.\n");
+
+ // 3. Verify the output .spec file
+ FILE *f_spec = fopen(spec_path, "rb");
+ assert(f_spec != NULL);
+
+ SpecHeader header;
+ fread(&header, sizeof(SpecHeader), 1, f_spec);
+ assert(strncmp(header.magic, "SPEC", 4) == 0);
+ assert(header.version == 1);
+ assert(header.dct_size == DCT_SIZE);
+
+ int expected_frames = (TEST_SAMPLE_RATE * TEST_DURATION_SECONDS) / DCT_SIZE;
+ if ((TEST_SAMPLE_RATE * TEST_DURATION_SECONDS) % DCT_SIZE != 0) {
+ expected_frames++;
+ }
+ assert(header.num_frames == expected_frames);
+
+ printf(" .spec header verified.\n");
+
+ // Just check that we have some non-zero data
+ std::vector<float> spec_data(header.num_frames * header.dct_size);
+ fread(spec_data.data(), sizeof(float), spec_data.size(), f_spec);
+ fclose(f_spec);
+
+ double total_energy = 0.0;
+ for (float val : spec_data) {
+ total_energy += fabs(val);
+ }
+ assert(total_energy > 0.0);
+ printf(" .spec data seems valid.\n");
+
+ // 4. Cleanup
+ remove(wav_path);
+ remove(spec_path);
+ printf(" Cleaned up temporary files.\n");
+
+ printf("...spectool test PASSED!\n");
+ return 0;
+}
diff --git a/src/tests/test_spectool.cpp b/src/tests/test_spectool.cpp
deleted file mode 100644
index ae64cc3..0000000
--- a/src/tests/test_spectool.cpp
+++ /dev/null
@@ -1,91 +0,0 @@
-#include <stdio.h>
-#include <assert.h>
-#include <stdlib.h> // For system()
-#include <vector>
-#include <math.h>
-#include "miniaudio.h"
-#include "audio/dct.h" // For DCT_SIZE
-
-// Re-defining the header here to avoid dependency on the tool's source
-struct SpecHeader {
- char magic[4];
- int32_t version;
- int32_t dct_size;
- int32_t num_frames;
-};
-
-#define TEST_SAMPLE_RATE 32000
-#define TEST_DURATION_SECONDS 1
-#define TEST_FREQ 440.0f
-#define PI 3.14159265f
-
-int main() {
- printf("Running spectool end-to-end test...\n");
-
- const char* wav_path = "test_signal.wav";
- const char* spec_path = "test_signal.spec";
- const char* spectool_path = "./spectool"; // Assumes ctest is run from `build` dir
-
- // 1. Generate and save a WAV file
- ma_encoder_config enc_config = ma_encoder_config_init(ma_encoding_format_wav, ma_format_f32, 1, TEST_SAMPLE_RATE);
- ma_encoder encoder;
- if (ma_encoder_init_file(wav_path, &enc_config, &encoder) != MA_SUCCESS) {
- printf("TEST FAILED: Could not initialize WAV encoder.\n");
- return 1;
- }
-
- std::vector<float> pcm_data(TEST_SAMPLE_RATE * TEST_DURATION_SECONDS);
- for (size_t i = 0; i < pcm_data.size(); ++i) {
- pcm_data[i] = 0.5f * sinf(2.0f * PI * TEST_FREQ * i / TEST_SAMPLE_RATE);
- }
- ma_uint64 frames_written;
- ma_encoder_write_pcm_frames(&encoder, pcm_data.data(), pcm_data.size(), &frames_written);
- ma_encoder_uninit(&encoder);
- printf(" Generated %s\n", wav_path);
-
- // 2. Run spectool analyze
- char command[512];
- snprintf(command, sizeof(command), "%s analyze %s %s", spectool_path, wav_path, spec_path);
- printf(" Executing: %s\n", command);
- int return_code = system(command);
- assert(return_code == 0);
- printf(" spectool executed successfully.\n");
-
- // 3. Verify the output .spec file
- FILE* f_spec = fopen(spec_path, "rb");
- assert(f_spec != NULL);
-
- SpecHeader header;
- fread(&header, sizeof(SpecHeader), 1, f_spec);
- assert(strncmp(header.magic, "SPEC", 4) == 0);
- assert(header.version == 1);
- assert(header.dct_size == DCT_SIZE);
-
- int expected_frames = (TEST_SAMPLE_RATE * TEST_DURATION_SECONDS) / DCT_SIZE;
- if ((TEST_SAMPLE_RATE * TEST_DURATION_SECONDS) % DCT_SIZE != 0) {
- expected_frames++;
- }
- assert(header.num_frames == expected_frames);
-
- printf(" .spec header verified.\n");
-
- // Just check that we have some non-zero data
- std::vector<float> spec_data(header.num_frames * header.dct_size);
- fread(spec_data.data(), sizeof(float), spec_data.size(), f_spec);
- fclose(f_spec);
-
- double total_energy = 0.0;
- for(float val : spec_data) {
- total_energy += fabs(val);
- }
- assert(total_energy > 0.0);
- printf(" .spec data seems valid.\n");
-
- // 4. Cleanup
- remove(wav_path);
- remove(spec_path);
- printf(" Cleaned up temporary files.\n");
-
- printf("...spectool test PASSED!\n");
- return 0;
-}
diff --git a/src/tests/test_synth.cc b/src/tests/test_synth.cc
new file mode 100644
index 0000000..3492057
--- /dev/null
+++ b/src/tests/test_synth.cc
@@ -0,0 +1,107 @@
+#include "audio/synth.h"
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <string.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_synth.cpp b/src/tests/test_synth.cpp
deleted file mode 100644
index 04b0373..0000000
--- a/src/tests/test_synth.cpp
+++ /dev/null
@@ -1,107 +0,0 @@
-#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.cc b/src/tests/test_window.cc
new file mode 100644
index 0000000..e23d97c
--- /dev/null
+++ b/src/tests/test_window.cc
@@ -0,0 +1,36 @@
+#include "audio/window.h"
+#include <assert.h>
+#include <math.h>
+#include <stdio.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;
+}
diff --git a/src/tests/test_window.cpp b/src/tests/test_window.cpp
deleted file mode 100644
index 1667dab..0000000
--- a/src/tests/test_window.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-#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;
-}