summaryrefslogtreecommitdiff
path: root/src/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests')
-rw-r--r--src/tests/test_assets.cc2
-rw-r--r--src/tests/test_fft.cc245
-rw-r--r--src/tests/test_mesh.cc36
-rw-r--r--src/tests/test_shader_compilation.cc232
-rw-r--r--src/tests/test_spectral_brush.cc236
5 files changed, 745 insertions, 6 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_fft.cc b/src/tests/test_fft.cc
new file mode 100644
index 0000000..948090a
--- /dev/null
+++ b/src/tests/test_fft.cc
@@ -0,0 +1,245 @@
+// Tests for FFT-based DCT/IDCT implementation
+// Verifies correctness against reference O(N²) implementation
+
+#include "audio/fft.h"
+
+#include <cassert>
+#include <cmath>
+#include <cstdio>
+#include <cstring>
+
+// Reference O(N²) DCT-II implementation (from original code)
+static void dct_reference(const float* input, float* output, size_t N) {
+ const float PI = 3.14159265358979323846f;
+
+ for (size_t k = 0; k < N; k++) {
+ float sum = 0.0f;
+ for (size_t n = 0; n < N; n++) {
+ sum += input[n] * cosf((PI / N) * k * (n + 0.5f));
+ }
+
+ // Apply DCT-II normalization
+ if (k == 0) {
+ output[k] = sum * sqrtf(1.0f / N);
+ } else {
+ output[k] = sum * sqrtf(2.0f / N);
+ }
+ }
+}
+
+// Reference O(N²) IDCT implementation (from original code)
+static void idct_reference(const float* input, float* output, size_t N) {
+ const float PI = 3.14159265358979323846f;
+
+ for (size_t n = 0; n < N; ++n) {
+ float sum = input[0] / 2.0f;
+ for (size_t k = 1; k < N; ++k) {
+ sum += input[k] * cosf((PI / N) * k * (n + 0.5f));
+ }
+ output[n] = sum * (2.0f / N);
+ }
+}
+
+// Compare two arrays with tolerance
+// Note: FFT-based DCT accumulates slightly more rounding error than O(N²) direct method
+// A tolerance of 1e-3 is still excellent for audio applications (< -60 dB error)
+static bool arrays_match(const float* a,
+ const float* b,
+ size_t N,
+ float tolerance = 1e-3f) {
+ for (size_t i = 0; i < N; i++) {
+ const float diff = fabsf(a[i] - b[i]);
+ if (diff > tolerance) {
+ fprintf(stderr,
+ "Mismatch at index %zu: %.6f vs %.6f (diff=%.6e)\n",
+ i,
+ a[i],
+ b[i],
+ diff);
+ return false;
+ }
+ }
+ return true;
+}
+
+// Test 1: DCT correctness (FFT-based vs reference)
+static void test_dct_correctness() {
+ printf("Test 1: DCT correctness (FFT vs reference O(N²))...\n");
+
+ const size_t N = 512;
+ float input[N];
+ float output_ref[N];
+ float output_fft[N];
+
+ // Test case 1: Impulse at index 0
+ memset(input, 0, N * sizeof(float));
+ input[0] = 1.0f;
+
+ dct_reference(input, output_ref, N);
+ dct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Impulse test passed\n");
+
+ // Test case 2: Impulse at middle
+ memset(input, 0, N * sizeof(float));
+ input[N / 2] = 1.0f;
+
+ dct_reference(input, output_ref, N);
+ dct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Middle impulse test passed\n");
+
+ // Test case 3: Sinusoidal input
+ for (size_t i = 0; i < N; i++) {
+ input[i] = sinf(2.0f * 3.14159265358979323846f * 5.0f * i / N);
+ }
+
+ dct_reference(input, output_ref, N);
+ dct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Sinusoidal input test passed\n");
+
+ // Test case 4: Random-ish input (deterministic)
+ for (size_t i = 0; i < N; i++) {
+ input[i] = sinf(i * 0.1f) * cosf(i * 0.05f);
+ }
+
+ dct_reference(input, output_ref, N);
+ dct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Complex input test passed\n");
+
+ printf("Test 1: PASSED ✓\n\n");
+}
+
+// Test 2: IDCT correctness (FFT-based vs reference)
+static void test_idct_correctness() {
+ printf("Test 2: IDCT correctness (FFT vs reference O(N²))...\n");
+
+ const size_t N = 512;
+ float input[N];
+ float output_ref[N];
+ float output_fft[N];
+
+ // Test case 1: DC component only
+ memset(input, 0, N * sizeof(float));
+ input[0] = 1.0f;
+
+ idct_reference(input, output_ref, N);
+ idct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ DC component test passed\n");
+
+ // Test case 2: Single frequency bin
+ memset(input, 0, N * sizeof(float));
+ input[10] = 1.0f;
+
+ idct_reference(input, output_ref, N);
+ idct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Single bin test passed\n");
+
+ // Test case 3: Mixed frequencies
+ for (size_t i = 0; i < N; i++) {
+ input[i] = (i % 10 == 0) ? 1.0f : 0.0f;
+ }
+
+ idct_reference(input, output_ref, N);
+ idct_fft(input, output_fft, N);
+
+ assert(arrays_match(output_ref, output_fft, N));
+ printf(" ✓ Mixed frequencies test passed\n");
+
+ printf("Test 2: PASSED ✓\n\n");
+}
+
+// Test 3: Round-trip (DCT → IDCT should recover original)
+static void test_roundtrip() {
+ printf("Test 3: Round-trip (DCT → IDCT = identity)...\n");
+
+ const size_t N = 512;
+ float input[N];
+ float dct_output[N];
+ float reconstructed[N];
+
+ // Test case 1: Sinusoidal input
+ for (size_t i = 0; i < N; i++) {
+ input[i] = sinf(2.0f * 3.14159265358979323846f * 3.0f * i / N);
+ }
+
+ dct_fft(input, dct_output, N);
+ idct_fft(dct_output, reconstructed, N);
+
+ assert(arrays_match(input, reconstructed, N));
+ printf(" ✓ Sinusoidal round-trip passed\n");
+
+ // Test case 2: Complex signal
+ for (size_t i = 0; i < N; i++) {
+ input[i] = sinf(i * 0.1f) * cosf(i * 0.05f) + cosf(i * 0.03f);
+ }
+
+ dct_fft(input, dct_output, N);
+ idct_fft(dct_output, reconstructed, N);
+
+ assert(arrays_match(input, reconstructed, N));
+ printf(" ✓ Complex signal round-trip passed\n");
+
+ printf("Test 3: PASSED ✓\n\n");
+}
+
+// Test 4: Output known values for JavaScript comparison
+static void test_known_values() {
+ printf("Test 4: Known values (for JavaScript verification)...\n");
+
+ const size_t N = 512;
+ float input[N];
+ float output[N];
+
+ // Simple test case: impulse at index 0
+ memset(input, 0, N * sizeof(float));
+ input[0] = 1.0f;
+
+ dct_fft(input, output, N);
+
+ printf(" DCT of impulse at 0:\n");
+ printf(" output[0] = %.8f (expected ~0.04419417)\n", output[0]);
+ printf(" output[1] = %.8f (expected ~0.04419417)\n", output[1]);
+ printf(" output[10] = %.8f (expected ~0.04419417)\n", output[10]);
+
+ // IDCT test
+ memset(input, 0, N * sizeof(float));
+ input[0] = 1.0f;
+
+ idct_fft(input, output, N);
+
+ printf(" IDCT of DC component:\n");
+ printf(" output[0] = %.8f (expected ~0.04419417)\n", output[0]);
+ printf(" output[100] = %.8f (expected ~0.04419417)\n", output[100]);
+ printf(" output[511] = %.8f (expected ~0.04419417)\n", output[511]);
+
+ printf("Test 4: PASSED ✓\n");
+ printf("(Copy these values to JavaScript test for verification)\n\n");
+}
+
+int main() {
+ printf("===========================================\n");
+ printf("FFT-based DCT/IDCT Test Suite\n");
+ printf("===========================================\n\n");
+
+ test_dct_correctness();
+ test_idct_correctness();
+ test_roundtrip();
+ test_known_values();
+
+ printf("===========================================\n");
+ printf("All tests PASSED ✓\n");
+ printf("===========================================\n");
+
+ return 0;
+}
diff --git a/src/tests/test_mesh.cc b/src/tests/test_mesh.cc
index 0294d9b..8c64d9b 100644
--- a/src/tests/test_mesh.cc
+++ b/src/tests/test_mesh.cc
@@ -169,7 +169,15 @@ bool load_obj_and_create_buffers(const char* path, Object3D& out_obj) {
std::string parts[3] = {s1, s2, s3};
RawFace face = {};
for (int i = 0; i < 3; ++i) {
- sscanf(parts[i].c_str(), "%d/%d/%d", &face.v[i], &face.vt[i], &face.vn[i]);
+ // Handle v//vn format
+ if (parts[i].find("//") != std::string::npos) {
+ sscanf(parts[i].c_str(), "%d//%d", &face.v[i], &face.vn[i]);
+ face.vt[i] = 0;
+ } else {
+ int res = sscanf(parts[i].c_str(), "%d/%d/%d", &face.v[i], &face.vt[i], &face.vn[i]);
+ if (res == 2) face.vn[i] = 0;
+ else if (res == 1) { face.vt[i] = 0; face.vn[i] = 0; }
+ }
}
raw_faces.push_back(face);
}
@@ -213,6 +221,21 @@ bool load_obj_and_create_buffers(const char* path, Object3D& out_obj) {
}
if (final_vertices.empty()) return false;
+
+ // Calculate AABB and center the mesh
+ float min_x = 1e10f, min_y = 1e10f, min_z = 1e10f;
+ float max_x = -1e10f, max_y = -1e10f, max_z = -1e10f;
+ for (const auto& v : final_vertices) {
+ min_x = std::min(min_x, v.p[0]); min_y = std::min(min_y, v.p[1]); min_z = std::min(min_z, v.p[2]);
+ max_x = std::max(max_x, v.p[0]); max_y = std::max(max_y, v.p[1]); max_z = std::max(max_z, v.p[2]);
+ }
+ float cx = (min_x + max_x) * 0.5f;
+ float cy = (min_y + max_y) * 0.5f;
+ float cz = (min_z + max_z) * 0.5f;
+ for (auto& v : final_vertices) {
+ v.p[0] -= cx; v.p[1] -= cy; v.p[2] -= cz;
+ }
+ out_obj.local_extent = vec3((max_x - min_x) * 0.5f, (max_y - min_y) * 0.5f, (max_z - min_z) * 0.5f);
g_mesh_gpu_data.num_indices = final_indices.size();
g_mesh_gpu_data.vertex_buffer = gpu_create_buffer(g_device, final_vertices.size() * sizeof(MeshVertex), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, final_vertices.data()).buffer;
@@ -260,8 +283,9 @@ int main(int argc, char** argv) {
g_renderer.set_noise_texture(g_textures.get_texture_view("noise"));
// --- Create Scene ---
- Object3D floor(ObjectType::PLANE);
- floor.scale = vec3(20.0f, 1.0f, 20.0f);
+ Object3D floor(ObjectType::BOX);
+ floor.position = vec3(0, -2.0f, 0);
+ floor.scale = vec3(25.0f, 0.2f, 25.0f);
floor.color = vec4(0.5f, 0.5f, 0.5f, 1.0f);
g_scene.add_object(floor);
@@ -271,11 +295,11 @@ int main(int argc, char** argv) {
return 1;
}
mesh_obj.color = vec4(1.0f, 0.7f, 0.2f, 1.0f);
- mesh_obj.position = {0, 1, 0};
+ mesh_obj.position = {0, 1.5, 0}; // Elevate a bit more
g_scene.add_object(mesh_obj);
g_camera.position = vec3(0, 3, 5);
- g_camera.target = vec3(0, 1, 0);
+ g_camera.target = vec3(0, 1.5, 0);
while (!platform_should_close(&platform_state)) {
platform_poll(&platform_state);
@@ -287,7 +311,7 @@ int main(int argc, char** argv) {
if (debug_mode) {
auto* vertices = (std::vector<MeshVertex>*)g_scene.objects[1].user_data;
- g_renderer.GetVisualDebug().add_mesh_normals(g_scene.objects[1].get_model_matrix(), vertices->size(), vertices->data());
+ g_renderer.GetVisualDebug().add_mesh_normals(g_scene.objects[1].get_model_matrix(), (uint32_t)vertices->size(), vertices->data());
}
WGPUSurfaceTexture surface_tex;
diff --git a/src/tests/test_shader_compilation.cc b/src/tests/test_shader_compilation.cc
new file mode 100644
index 0000000..8b3b5f5
--- /dev/null
+++ b/src/tests/test_shader_compilation.cc
@@ -0,0 +1,232 @@
+// This file is part of the 64k demo project.
+// It validates that all production shaders compile successfully with WebGPU.
+// This catches issues like:
+// - Invalid WGSL syntax (e.g., undefined functions like inverse())
+// - Missing binding declarations
+// - Type mismatches
+
+#include "generated/assets.h"
+#include "gpu/effects/shader_composer.h"
+#include "gpu/effects/shaders.h"
+#include "platform.h"
+#include <cassert>
+#include <cstdio>
+#include <cstring>
+#include <string>
+
+static WGPUDevice g_device = nullptr;
+
+// Initialize minimal WebGPU for shader compilation testing
+static bool init_wgpu() {
+ WGPUInstance instance = wgpuCreateInstance(nullptr);
+ if (!instance) {
+ fprintf(stderr, "Failed to create WGPU instance.\n");
+ return false;
+ }
+
+ WGPURequestAdapterOptions adapter_opts = {};
+ adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
+
+ WGPUAdapter adapter = nullptr;
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter a,
+ const char* message, void* userdata) {
+ if (status == WGPURequestAdapterStatus_Success) {
+ *(WGPUAdapter*)userdata = a;
+ }
+ };
+ wgpuInstanceRequestAdapter(instance, &adapter_opts, on_adapter, &adapter);
+#else
+ auto on_adapter = [](WGPURequestAdapterStatus status, WGPUAdapter a,
+ WGPUStringView message, void* userdata, void* user2) {
+ (void)user2;
+ (void)message;
+ if (status == WGPURequestAdapterStatus_Success) {
+ *(WGPUAdapter*)userdata = a;
+ }
+ };
+ WGPURequestAdapterCallbackInfo adapter_cb = {};
+ adapter_cb.mode = WGPUCallbackMode_WaitAnyOnly;
+ adapter_cb.callback = on_adapter;
+ adapter_cb.userdata1 = &adapter;
+ wgpuInstanceRequestAdapter(instance, &adapter_opts, adapter_cb);
+#endif
+
+ // Try to wait for adapter (may not work on all platforms)
+ for (int i = 0; i < 100 && !adapter; ++i) {
+ wgpuInstanceProcessEvents(instance);
+ }
+
+ if (!adapter) {
+ fprintf(stderr,
+ "Warning: Could not get WGPU adapter (GPU compilation tests "
+ "skipped)\n");
+ return false;
+ }
+
+ WGPUDeviceDescriptor device_desc = {};
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice d,
+ const char* message, void* userdata) {
+ if (status == WGPURequestDeviceStatus_Success) {
+ *(WGPUDevice*)userdata = d;
+ }
+ };
+ wgpuAdapterRequestDevice(adapter, &device_desc, on_device, &g_device);
+#else
+ auto on_device = [](WGPURequestDeviceStatus status, WGPUDevice d,
+ WGPUStringView message, void* userdata, void* user2) {
+ (void)user2;
+ (void)message;
+ if (status == WGPURequestDeviceStatus_Success) {
+ *(WGPUDevice*)userdata = d;
+ }
+ };
+ WGPURequestDeviceCallbackInfo device_cb = {};
+ device_cb.mode = WGPUCallbackMode_WaitAnyOnly;
+ device_cb.callback = on_device;
+ device_cb.userdata1 = &g_device;
+ wgpuAdapterRequestDevice(adapter, &device_desc, device_cb);
+#endif
+
+ // Try to wait for device (may not work on all platforms)
+ for (int i = 0; i < 100 && !g_device; ++i) {
+ wgpuInstanceProcessEvents(instance);
+ }
+
+ if (!g_device) {
+ fprintf(stderr,
+ "Warning: Could not get WGPU device (GPU compilation tests "
+ "skipped)\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Test shader compilation
+static bool test_shader_compilation(const char* name,
+ const char* shader_code) {
+ printf("Testing compilation: %s...\n", name);
+
+ if (!g_device) {
+ printf("SKIPPED: %s (no GPU device)\n", name);
+ return true; // Not a failure, just skipped
+ }
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ WGPUShaderModuleWGSLDescriptor wgsl_desc = {};
+ wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
+ wgsl_desc.code = shader_code;
+ WGPUShaderModuleDescriptor shader_desc = {};
+ shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain;
+#else
+ WGPUShaderSourceWGSL wgsl_desc = {};
+ wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
+ wgsl_desc.code = str_view(shader_code);
+ WGPUShaderModuleDescriptor shader_desc = {};
+ shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain;
+#endif
+
+ WGPUShaderModule shader_module =
+ wgpuDeviceCreateShaderModule(g_device, &shader_desc);
+
+ if (!shader_module) {
+ printf("FAILED: %s - shader compilation failed!\n", name);
+ return false;
+ }
+
+ wgpuShaderModuleRelease(shader_module);
+ printf("PASSED: %s\n", name);
+ return true;
+}
+
+// Test composed shader with different modes
+static bool test_composed_shader(const char* base_name, AssetId asset_id,
+ bool with_bvh) {
+ const char* mode_name = with_bvh ? "BVH" : "Linear";
+ char test_name[128];
+ snprintf(test_name, sizeof(test_name), "%s (%s mode)", base_name, mode_name);
+
+ const char* shader_asset = (const char*)GetAsset(asset_id);
+ std::string main_code = shader_asset;
+
+ ShaderComposer::CompositionMap composition_map;
+ if (with_bvh) {
+ composition_map["render/scene_query_mode"] = "render/scene_query_bvh";
+ } else {
+ composition_map["render/scene_query_mode"] = "render/scene_query_linear";
+ }
+
+ std::string composed_shader =
+ ShaderComposer::Get().Compose({}, main_code, composition_map);
+
+ return test_shader_compilation(test_name, composed_shader.c_str());
+}
+
+int main() {
+ printf("===========================================\n");
+ printf("Shader Compilation Test Suite\n");
+ printf("===========================================\n\n");
+
+ bool gpu_available = init_wgpu();
+ if (!gpu_available) {
+ printf(
+ "Note: GPU not available - running composition-only tests\n\n");
+ }
+
+ // Initialize shader composer
+ InitShaderComposer();
+
+ bool all_passed = true;
+
+ // Test 1: Simple shaders that don't need composition
+ printf("\n--- Test 1: Simple Shaders ---\n");
+ all_passed &= test_shader_compilation(
+ "Passthrough", (const char*)GetAsset(AssetId::ASSET_SHADER_PASSTHROUGH));
+ all_passed &= test_shader_compilation(
+ "Ellipse", (const char*)GetAsset(AssetId::ASSET_SHADER_ELLIPSE));
+ all_passed &= test_shader_compilation(
+ "Gaussian Blur",
+ (const char*)GetAsset(AssetId::ASSET_SHADER_GAUSSIAN_BLUR));
+ all_passed &= test_shader_compilation(
+ "Solarize", (const char*)GetAsset(AssetId::ASSET_SHADER_SOLARIZE));
+
+ // Test 2: Composed shaders (both BVH and Linear modes)
+ printf("\n--- Test 2: Composed Shaders (BVH Mode) ---\n");
+ all_passed &= test_composed_shader("Renderer 3D",
+ AssetId::ASSET_SHADER_RENDERER_3D, true);
+ all_passed &=
+ test_composed_shader("Mesh Render", AssetId::ASSET_SHADER_MESH, true);
+
+ printf("\n--- Test 3: Composed Shaders (Linear Mode) ---\n");
+ all_passed &= test_composed_shader("Renderer 3D",
+ AssetId::ASSET_SHADER_RENDERER_3D, false);
+ all_passed &=
+ test_composed_shader("Mesh Render", AssetId::ASSET_SHADER_MESH, false);
+
+ // Test 3: Compute shaders
+ printf("\n--- Test 4: Compute Shaders ---\n");
+ all_passed &= test_shader_compilation(
+ "Particle Compute",
+ (const char*)GetAsset(AssetId::ASSET_SHADER_PARTICLE_COMPUTE));
+ all_passed &= test_shader_compilation(
+ "Particle Spray Compute",
+ (const char*)GetAsset(AssetId::ASSET_SHADER_PARTICLE_SPRAY_COMPUTE));
+
+ printf("\n===========================================\n");
+ if (all_passed) {
+ printf("All shader compilation tests PASSED ✓\n");
+ } else {
+ printf("Some shader compilation tests FAILED ✗\n");
+ }
+ printf("===========================================\n");
+
+ if (g_device) {
+ wgpuDeviceRelease(g_device);
+ }
+
+ return all_passed ? 0 : 1;
+}
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;
+}