summaryrefslogtreecommitdiff
path: root/src/audio/spectral_brush.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-06 11:12:34 +0100
committerskal <pascal.massimino@gmail.com>2026-02-06 11:12:34 +0100
commit5a1adde097e489c259bd052971546e95683c3596 (patch)
treebf03cf8b803604638ad84ddd9cc26de64baea64f /src/audio/spectral_brush.cc
parent83f34fb955524c09b7f3e124b97c3d4feef02a0c (diff)
feat(audio): Add Spectral Brush runtime (Phase 1 of Task #5)
Implement C++ runtime foundation for procedural audio tracing tool. Changes: - Created spectral_brush.h/cc with core API - Linear Bezier interpolation - Vertical profile evaluation (Gaussian, Decaying Sinusoid, Noise) - draw_bezier_curve() for spectrogram rendering - Home-brew deterministic RNG for noise profile - Added comprehensive unit tests (test_spectral_brush.cc) - Tests Bezier interpolation, profiles, edge cases - Tests full spectrogram rendering pipeline - All 9 tests pass - Integrated into CMake build system - Fixed test_assets.cc include (asset_manager_utils.h) Design: - Spectral Brush = Central Curve (Bezier) + Vertical Profile - Enables 50-100x compression (5KB .spec to 100 bytes C++ code) - Future: Cubic Bezier, composite profiles, multi-dimensional curves Documentation: - Added doc/SPECTRAL_BRUSH_EDITOR.md (complete architecture) - Updated TODO.md with Phase 1-4 implementation plan - Updated PROJECT_CONTEXT.md to mark Task #5 in progress Test results: 21/21 tests pass (100%) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/audio/spectral_brush.cc')
-rw-r--r--src/audio/spectral_brush.cc171
1 files changed, 171 insertions, 0 deletions
diff --git a/src/audio/spectral_brush.cc b/src/audio/spectral_brush.cc
new file mode 100644
index 0000000..c6eb64d
--- /dev/null
+++ b/src/audio/spectral_brush.cc
@@ -0,0 +1,171 @@
+// This file is part of the 64k demo project.
+// It implements the "Spectral Brush" primitive for procedural audio generation.
+// Implementation of Bezier curves, vertical profiles, and spectrogram rendering.
+
+#include "spectral_brush.h"
+
+#include <cmath>
+
+// Sample rate constant (matches demo audio configuration)
+static const float SAMPLE_RATE = 32000.0f;
+
+// Evaluate linear Bezier interpolation between control points
+float evaluate_bezier_linear(const float* control_frames,
+ const float* control_values,
+ int n_points,
+ float frame) {
+ if (n_points == 0) {
+ return 0.0f;
+ }
+ if (n_points == 1) {
+ return control_values[0];
+ }
+
+ // Clamp to first/last value if outside range
+ if (frame <= control_frames[0]) {
+ return control_values[0];
+ }
+ if (frame >= control_frames[n_points - 1]) {
+ return control_values[n_points - 1];
+ }
+
+ // Find segment containing frame
+ for (int i = 0; i < n_points - 1; ++i) {
+ if (frame >= control_frames[i] && frame <= control_frames[i + 1]) {
+ // Linear interpolation: value = v0 + (v1 - v0) * t
+ const float t =
+ (frame - control_frames[i]) / (control_frames[i + 1] - control_frames[i]);
+ return control_values[i] * (1.0f - t) + control_values[i + 1] * t;
+ }
+ }
+
+ // Should not reach here (fallback to last value)
+ return control_values[n_points - 1];
+}
+
+// Home-brew deterministic RNG (LCG algorithm)
+uint32_t spectral_brush_rand(uint32_t seed) {
+ // LCG parameters (from Numerical Recipes)
+ // X_{n+1} = (a * X_n + c) mod m
+ const uint32_t a = 1664525;
+ const uint32_t c = 1013904223;
+ return a * seed + c; // Implicit mod 2^32
+}
+
+// Evaluate vertical profile at distance from curve center
+float evaluate_profile(ProfileType type, float distance, float param1, float param2) {
+ switch (type) {
+ case PROFILE_GAUSSIAN: {
+ // Gaussian: exp(-(dist^2 / sigma^2))
+ // param1 = sigma (width in bins)
+ const float sigma = param1;
+ if (sigma <= 0.0f) {
+ return 0.0f;
+ }
+ return expf(-(distance * distance) / (sigma * sigma));
+ }
+
+ case PROFILE_DECAYING_SINUSOID: {
+ // Decaying sinusoid: exp(-decay * dist) * cos(omega * dist)
+ // param1 = decay rate
+ // param2 = oscillation frequency (omega)
+ const float decay = param1;
+ const float omega = param2;
+ const float envelope = expf(-decay * distance);
+ const float oscillation = cosf(omega * distance);
+ return envelope * oscillation;
+ }
+
+ case PROFILE_NOISE: {
+ // Random noise: deterministic RNG based on distance
+ // param1 = amplitude scale
+ // param2 = seed
+ const float amplitude = param1;
+ const uint32_t seed = (uint32_t)(param2) + (uint32_t)(distance * 1000.0f);
+ const uint32_t rand_val = spectral_brush_rand(seed);
+ // Map to [0, 1]
+ const float normalized = (float)(rand_val % 10000) / 10000.0f;
+ return amplitude * normalized;
+ }
+ }
+
+ return 0.0f;
+}
+
+// Internal implementation: Render Bezier curve with profile
+static void draw_bezier_curve_impl(float* spectrogram,
+ int dct_size,
+ int num_frames,
+ const float* control_frames,
+ const float* control_freqs_hz,
+ const float* control_amps,
+ int n_control_points,
+ ProfileType profile_type,
+ float profile_param1,
+ float profile_param2,
+ bool additive) {
+ if (n_control_points < 1) {
+ return; // Nothing to draw
+ }
+
+ // For each frame in the spectrogram
+ for (int f = 0; f < num_frames; ++f) {
+ // 1. Evaluate Bezier curve at this frame
+ const float freq_hz =
+ evaluate_bezier_linear(control_frames, control_freqs_hz, n_control_points, (float)f);
+ const float amplitude =
+ evaluate_bezier_linear(control_frames, control_amps, n_control_points, (float)f);
+
+ // 2. Convert frequency (Hz) to frequency bin index
+ // Nyquist frequency = SAMPLE_RATE / 2
+ // bin = (freq_hz / nyquist) * dct_size
+ const float nyquist = SAMPLE_RATE / 2.0f;
+ const float freq_bin_0 = (freq_hz / nyquist) * dct_size;
+
+ // 3. Apply vertical profile around freq_bin_0
+ for (int b = 0; b < dct_size; ++b) {
+ const float dist = fabsf(b - freq_bin_0);
+ const float profile_val = evaluate_profile(profile_type, dist, profile_param1, profile_param2);
+ const float contribution = amplitude * profile_val;
+
+ const int idx = f * dct_size + b;
+ if (additive) {
+ spectrogram[idx] += contribution;
+ } else {
+ spectrogram[idx] = contribution;
+ }
+ }
+ }
+}
+
+// Draw spectral brush (overwrites spectrogram content)
+void draw_bezier_curve(float* spectrogram,
+ int dct_size,
+ int num_frames,
+ const float* control_frames,
+ const float* control_freqs_hz,
+ const float* control_amps,
+ int n_control_points,
+ ProfileType profile_type,
+ float profile_param1,
+ float profile_param2) {
+ draw_bezier_curve_impl(spectrogram, dct_size, num_frames, control_frames, control_freqs_hz,
+ control_amps, n_control_points, profile_type, profile_param1,
+ profile_param2, false);
+}
+
+// Draw spectral brush (adds to existing spectrogram content)
+void draw_bezier_curve_add(float* spectrogram,
+ int dct_size,
+ int num_frames,
+ const float* control_frames,
+ const float* control_freqs_hz,
+ const float* control_amps,
+ int n_control_points,
+ ProfileType profile_type,
+ float profile_param1,
+ float profile_param2) {
+ draw_bezier_curve_impl(spectrogram, dct_size, num_frames, control_frames, control_freqs_hz,
+ control_amps, n_control_points, profile_type, profile_param1,
+ profile_param2, true);
+}