summaryrefslogtreecommitdiff
path: root/src/tests
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/tests
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/tests')
-rw-r--r--src/tests/test_assets.cc2
-rw-r--r--src/tests/test_spectral_brush.cc236
2 files changed, 238 insertions, 0 deletions
diff --git a/src/tests/test_assets.cc b/src/tests/test_assets.cc
index 86b4ba4..2ee18d6 100644
--- a/src/tests/test_assets.cc
+++ b/src/tests/test_assets.cc
@@ -8,6 +8,8 @@
#include "generated/assets.h"
#endif /* defined(USE_TEST_ASSETS) */
+#include "util/asset_manager_utils.h"
+
#include <assert.h>
#include <stdio.h>
#include <string.h>
diff --git a/src/tests/test_spectral_brush.cc b/src/tests/test_spectral_brush.cc
new file mode 100644
index 0000000..1431ba7
--- /dev/null
+++ b/src/tests/test_spectral_brush.cc
@@ -0,0 +1,236 @@
+// This file is part of the 64k demo project.
+// Unit tests for spectral brush primitives.
+// Tests linear Bezier interpolation, profiles, and spectrogram rendering.
+
+#include "audio/spectral_brush.h"
+
+#include <cassert>
+#include <cmath>
+#include <cstdio>
+#include <cstring>
+
+// Test tolerance for floating-point comparisons
+static const float EPSILON = 1e-5f;
+
+// Helper: Compare floats with tolerance
+static bool float_eq(float a, float b) {
+ return fabsf(a - b) < EPSILON;
+}
+
+// Test: Linear Bezier interpolation with 2 control points (simple line)
+void test_bezier_linear_2points() {
+ const float frames[] = {0.0f, 100.0f};
+ const float values[] = {50.0f, 150.0f};
+
+ // At control points, should return exact values
+ assert(float_eq(evaluate_bezier_linear(frames, values, 2, 0.0f), 50.0f));
+ assert(float_eq(evaluate_bezier_linear(frames, values, 2, 100.0f), 150.0f));
+
+ // Midpoint: linear interpolation
+ const float mid = evaluate_bezier_linear(frames, values, 2, 50.0f);
+ assert(float_eq(mid, 100.0f)); // (50 + 150) / 2
+
+ // Quarter point
+ const float quarter = evaluate_bezier_linear(frames, values, 2, 25.0f);
+ assert(float_eq(quarter, 75.0f)); // 50 + (150 - 50) * 0.25
+
+ printf("[PASS] test_bezier_linear_2points\n");
+}
+
+// Test: Linear Bezier interpolation with 4 control points
+void test_bezier_linear_4points() {
+ const float frames[] = {0.0f, 20.0f, 50.0f, 100.0f};
+ const float values[] = {200.0f, 80.0f, 60.0f, 50.0f};
+
+ // At control points
+ assert(float_eq(evaluate_bezier_linear(frames, values, 4, 0.0f), 200.0f));
+ assert(float_eq(evaluate_bezier_linear(frames, values, 4, 20.0f), 80.0f));
+ assert(float_eq(evaluate_bezier_linear(frames, values, 4, 50.0f), 60.0f));
+ assert(float_eq(evaluate_bezier_linear(frames, values, 4, 100.0f), 50.0f));
+
+ // Between first and second point (frame 10)
+ const float interp1 = evaluate_bezier_linear(frames, values, 4, 10.0f);
+ // t = (10 - 0) / (20 - 0) = 0.5
+ // value = 200 * 0.5 + 80 * 0.5 = 140
+ assert(float_eq(interp1, 140.0f));
+
+ // Between third and fourth point (frame 75)
+ const float interp2 = evaluate_bezier_linear(frames, values, 4, 75.0f);
+ // t = (75 - 50) / (100 - 50) = 0.5
+ // value = 60 * 0.5 + 50 * 0.5 = 55
+ assert(float_eq(interp2, 55.0f));
+
+ printf("[PASS] test_bezier_linear_4points\n");
+}
+
+// Test: Edge cases (single point, empty, out of range)
+void test_bezier_edge_cases() {
+ const float frames[] = {50.0f};
+ const float values[] = {123.0f};
+
+ // Single control point: always return that value
+ assert(float_eq(evaluate_bezier_linear(frames, values, 1, 0.0f), 123.0f));
+ assert(float_eq(evaluate_bezier_linear(frames, values, 1, 100.0f), 123.0f));
+
+ // Empty array: return 0
+ assert(float_eq(evaluate_bezier_linear(frames, values, 0, 50.0f), 0.0f));
+
+ // Out of range: clamp to endpoints
+ const float frames2[] = {10.0f, 90.0f};
+ const float values2[] = {100.0f, 200.0f};
+ assert(float_eq(evaluate_bezier_linear(frames2, values2, 2, 0.0f), 100.0f)); // Before start
+ assert(float_eq(evaluate_bezier_linear(frames2, values2, 2, 100.0f), 200.0f)); // After end
+
+ printf("[PASS] test_bezier_edge_cases\n");
+}
+
+// Test: Gaussian profile evaluation
+void test_profile_gaussian() {
+ // At center (distance = 0), should be 1.0
+ assert(float_eq(evaluate_profile(PROFILE_GAUSSIAN, 0.0f, 30.0f, 0.0f), 1.0f));
+
+ // Gaussian falloff: exp(-(dist^2 / sigma^2))
+ const float sigma = 30.0f;
+ const float dist = 15.0f;
+ const float expected = expf(-(dist * dist) / (sigma * sigma));
+ const float actual = evaluate_profile(PROFILE_GAUSSIAN, dist, sigma, 0.0f);
+ assert(float_eq(actual, expected));
+
+ // Far from center: should approach 0
+ const float far = evaluate_profile(PROFILE_GAUSSIAN, 100.0f, 30.0f, 0.0f);
+ assert(far < 0.01f); // Very small
+
+ printf("[PASS] test_profile_gaussian\n");
+}
+
+// Test: Decaying sinusoid profile evaluation
+void test_profile_decaying_sinusoid() {
+ const float decay = 0.15f;
+ const float omega = 0.8f;
+
+ // At center (distance = 0)
+ // exp(-0 * 0.15) * cos(0 * 0.8) = 1.0 * 1.0 = 1.0
+ assert(float_eq(evaluate_profile(PROFILE_DECAYING_SINUSOID, 0.0f, decay, omega), 1.0f));
+
+ // At distance 10
+ const float dist = 10.0f;
+ const float expected = expf(-decay * dist) * cosf(omega * dist);
+ const float actual = evaluate_profile(PROFILE_DECAYING_SINUSOID, dist, decay, omega);
+ assert(float_eq(actual, expected));
+
+ printf("[PASS] test_profile_decaying_sinusoid\n");
+}
+
+// Test: Noise profile evaluation (deterministic)
+void test_profile_noise() {
+ const float amplitude = 0.5f;
+ const uint32_t seed = 42;
+
+ // Same distance + seed should produce same value
+ const float val1 = evaluate_profile(PROFILE_NOISE, 10.0f, amplitude, (float)seed);
+ const float val2 = evaluate_profile(PROFILE_NOISE, 10.0f, amplitude, (float)seed);
+ assert(float_eq(val1, val2));
+
+ // Different distance should produce different value (with high probability)
+ const float val3 = evaluate_profile(PROFILE_NOISE, 20.0f, amplitude, (float)seed);
+ assert(!float_eq(val1, val3));
+
+ // Should be in range [0, amplitude]
+ assert(val1 >= 0.0f && val1 <= amplitude);
+
+ printf("[PASS] test_profile_noise\n");
+}
+
+// Test: draw_bezier_curve full integration
+void test_draw_bezier_curve() {
+ const int dct_size = 512;
+ const int num_frames = 100;
+ float spectrogram[512 * 100];
+ memset(spectrogram, 0, sizeof(spectrogram));
+
+ // Simple curve: constant frequency, linearly decaying amplitude
+ const float frames[] = {0.0f, 100.0f};
+ const float freqs[] = {440.0f, 440.0f}; // A4 note (constant pitch)
+ const float amps[] = {1.0f, 0.0f}; // Fade out
+
+ draw_bezier_curve(spectrogram, dct_size, num_frames, frames, freqs, amps, 2, PROFILE_GAUSSIAN,
+ 30.0f);
+
+ // Verify: At frame 0, should have peak around 440 Hz bin
+ // bin = (440 / 16000) * 512 ā‰ˆ 14.08
+ const int expected_bin = 14;
+ const float val_at_peak = spectrogram[0 * dct_size + expected_bin];
+ assert(val_at_peak > 0.5f); // Should be near 1.0 due to Gaussian
+
+ // Verify: At frame 99 (end), amplitude should be near 0
+ const float val_at_end = spectrogram[99 * dct_size + expected_bin];
+ assert(val_at_end < 0.1f); // Near zero
+
+ // Verify: At frame 50 (midpoint), amplitude should be ~0.5
+ const float val_at_mid = spectrogram[50 * dct_size + expected_bin];
+ assert(val_at_mid > 0.3f && val_at_mid < 0.7f); // Around 0.5
+
+ printf("[PASS] test_draw_bezier_curve\n");
+}
+
+// Test: draw_bezier_curve_add (additive mode)
+void test_draw_bezier_curve_add() {
+ const int dct_size = 512;
+ const int num_frames = 100;
+ float spectrogram[512 * 100];
+ memset(spectrogram, 0, sizeof(spectrogram));
+
+ // Draw first curve
+ const float frames1[] = {0.0f, 100.0f};
+ const float freqs1[] = {440.0f, 440.0f};
+ const float amps1[] = {0.5f, 0.5f};
+ draw_bezier_curve(spectrogram, dct_size, num_frames, frames1, freqs1, amps1, 2, PROFILE_GAUSSIAN,
+ 30.0f);
+
+ const int bin = 14; // ~440 Hz
+ const float val_before_add = spectrogram[0 * dct_size + bin];
+
+ // Add second curve (same frequency, same amplitude)
+ draw_bezier_curve_add(spectrogram, dct_size, num_frames, frames1, freqs1, amps1, 2,
+ PROFILE_GAUSSIAN, 30.0f);
+
+ const float val_after_add = spectrogram[0 * dct_size + bin];
+
+ // Should be approximately doubled
+ assert(val_after_add > val_before_add * 1.8f); // Allow small error
+
+ printf("[PASS] test_draw_bezier_curve_add\n");
+}
+
+// Test: RNG determinism
+void test_rng_determinism() {
+ const uint32_t seed = 12345;
+
+ // Same seed should produce same value
+ const uint32_t val1 = spectral_brush_rand(seed);
+ const uint32_t val2 = spectral_brush_rand(seed);
+ assert(val1 == val2);
+
+ // Different seeds should produce different values
+ const uint32_t val3 = spectral_brush_rand(seed + 1);
+ assert(val1 != val3);
+
+ printf("[PASS] test_rng_determinism\n");
+}
+
+int main() {
+ printf("Running spectral brush tests...\n\n");
+
+ test_bezier_linear_2points();
+ test_bezier_linear_4points();
+ test_bezier_edge_cases();
+ test_profile_gaussian();
+ test_profile_decaying_sinusoid();
+ test_profile_noise();
+ test_draw_bezier_curve();
+ test_draw_bezier_curve_add();
+ test_rng_determinism();
+
+ printf("\nāœ“ All tests passed!\n");
+ return 0;
+}