diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/audio/spectral_brush.cc | 171 | ||||
| -rw-r--r-- | src/audio/spectral_brush.h | 80 | ||||
| -rw-r--r-- | src/generated/assets_data.cc | 6 | ||||
| -rw-r--r-- | src/tests/test_assets.cc | 2 | ||||
| -rw-r--r-- | src/tests/test_spectral_brush.cc | 236 |
5 files changed, 491 insertions, 4 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); +} diff --git a/src/audio/spectral_brush.h b/src/audio/spectral_brush.h new file mode 100644 index 0000000..3125f35 --- /dev/null +++ b/src/audio/spectral_brush.h @@ -0,0 +1,80 @@ +// This file is part of the 64k demo project. +// It implements the "Spectral Brush" primitive for procedural audio generation. +// Spectral brushes trace Bezier curves through spectrograms with vertical profiles. + +#pragma once + +#include <cstdint> + +// Profile types for vertical distribution around central Bezier curve +enum ProfileType { + PROFILE_GAUSSIAN = 0, // Smooth harmonic falloff + PROFILE_DECAYING_SINUSOID = 1, // Resonant/metallic texture + PROFILE_NOISE = 2 // Random texture/grit +}; + +// Evaluate linear Bezier interpolation at given frame +// control_frames: Array of frame positions for control points +// control_values: Array of values at control points (freq_hz or amplitude) +// n_points: Number of control points +// frame: Frame number to evaluate at +// Returns: Interpolated value at frame (linearly interpolated between control points) +float evaluate_bezier_linear(const float* control_frames, + const float* control_values, + int n_points, + float frame); + +// Draw a spectral brush stroke onto a spectrogram +// Traces a Bezier curve through time-frequency space with a vertical profile +// spectrogram: Output buffer (dct_size Ć num_frames), modified in-place +// dct_size: Number of frequency bins (e.g., 512) +// num_frames: Number of time frames +// control_frames: Frame positions of Bezier control points +// control_freqs_hz: Frequency values (Hz) at control points +// control_amps: Amplitude values at control points (0.0-1.0 typical) +// n_control_points: Number of control points (minimum 2 for a curve) +// profile_type: Type of vertical profile to apply +// profile_param1: First parameter (sigma for Gaussian, decay for sinusoid, amplitude for noise) +// profile_param2: Second parameter (unused for Gaussian, frequency for sinusoid, seed for noise) +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 = 0.0f); + +// Additive variant of draw_bezier_curve (adds to existing spectrogram content) +// Use for compositing multiple profiles (e.g., Gaussian + Noise) +// Parameters same as draw_bezier_curve() +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 = 0.0f); + +// Evaluate vertical profile at given distance from central curve +// type: Profile type (Gaussian, Decaying Sinusoid, Noise) +// distance: Distance in frequency bins from curve center +// param1: First profile parameter +// param2: Second profile parameter +// Returns: Profile amplitude at given distance (0.0-1.0 range typically) +float evaluate_profile(ProfileType type, + float distance, + float param1, + float param2); + +// Home-brew deterministic RNG for noise profile +// Simple linear congruential generator (LCG) for small code size +// seed: Input seed value +// Returns: Pseudo-random uint32_t value +uint32_t spectral_brush_rand(uint32_t seed); + diff --git a/src/generated/assets_data.cc b/src/generated/assets_data.cc index a29680f..0b2daba 100644 --- a/src/generated/assets_data.cc +++ b/src/generated/assets_data.cc @@ -369584,7 +369584,7 @@ static const float ASSET_PROC_PARAMS_NOISE_TEX[] = {1234.000000, 16.000000}; static const char* ASSET_PROC_FUNC_STR_NOISE_TEX = "gen_noise"; -const size_t ASSET_SIZE_SHADER_RENDERER_3D = 9468; +const size_t ASSET_SIZE_SHADER_RENDERER_3D = 9449; alignas(16) static const uint8_t ASSET_DATA_SHADER_RENDERER_3D[] = { 0x23, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x20, 0x22, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, @@ -370373,9 +370373,7 @@ alignas(16) static const uint8_t ASSET_DATA_SHADER_RENDERER_3D[] = { 0x6f, 0x73, 0x2e, 0x7a, 0x20, 0x2f, 0x20, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x70, 0x6f, 0x73, 0x2e, 0x77, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, - 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x2f, 0x2f, 0x20, 0x64, 0x65, 0x70, 0x65, - 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, - 0x00 + 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x00 }; const size_t ASSET_SIZE_SHADER_COMMON_UNIFORMS = 346; alignas(16) static const uint8_t ASSET_DATA_SHADER_COMMON_UNIFORMS[] = { 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; +} |
