// 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 #include #include #include // 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; }