From 9dcf94ab01269311b4e5d39be23c95560904c626 Mon Sep 17 00:00:00 2001 From: skal Date: Tue, 27 Jan 2026 23:09:27 +0100 Subject: 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. --- src/audio/synth.cc | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/audio/synth.cc (limited to 'src/audio/synth.cc') 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 +#include // 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; +} -- cgit v1.2.3