diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-06 11:12:34 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-06 11:12:34 +0100 |
| commit | 5a1adde097e489c259bd052971546e95683c3596 (patch) | |
| tree | bf03cf8b803604638ad84ddd9cc26de64baea64f /src/audio/spectral_brush.cc | |
| parent | 83f34fb955524c09b7f3e124b97c3d4feef02a0c (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.cc | 171 |
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); +} |
