// Minimal audio/visual synchronization test tool // Plays simple drum beat with synchronized screen flashes #include "audio/audio.h" #include "audio/audio_engine.h" #include "audio/synth.h" #include "generated/assets.h" // Note: uses main demo asset bundle #include "gpu/demo_effects.h" #include "gpu/gpu.h" #include "platform/platform.h" #include #include #include #include // External declarations from generated files extern float GetDemoDuration(); extern void LoadTimeline(MainSequence& main_seq, const GpuContext& ctx); // Inline peak meter effect for debugging audio-visual sync #include "gpu/effects/post_process_helper.h" class PeakMeterEffect : public PostProcessEffect { public: PeakMeterEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { const char* shader_code = R"( struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, }; struct Uniforms { peak_value: f32, _pad0: f32, _pad1: f32, _pad2: f32, }; @group(0) @binding(0) var inputSampler: sampler; @group(0) @binding(1) var inputTexture: texture_2d; @group(0) @binding(2) var uniforms: Uniforms; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; var pos = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0) ); output.position = vec4(pos[vertexIndex], 0.0, 1.0); output.uv = pos[vertexIndex] * 0.5 + 0.5; return output; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { let color = textureSample(inputTexture, inputSampler, input.uv); // Draw red horizontal bar in middle of screen // Bar height: 5% of screen height // Bar width: proportional to peak_value (0.0 to 1.0) let bar_height = 0.05; let bar_center_y = 0.5; let bar_y_min = bar_center_y - bar_height * 0.5; let bar_y_max = bar_center_y + bar_height * 0.5; // Bar extends from left (0.0) to peak_value position let bar_x_max = uniforms.peak_value; // Check if current pixel is inside the bar let in_bar_y = input.uv.y >= bar_y_min && input.uv.y <= bar_y_max; let in_bar_x = input.uv.x <= bar_x_max; if (in_bar_y && in_bar_x) { // Red bar return vec4(1.0, 0.0, 0.0, 1.0); } else { // Original color return color; } } )"; pipeline_ = create_post_process_pipeline(device_, format_, shader_code); uniforms_ = gpu_create_buffer( device_, 16, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); } void update_bind_group(WGPUTextureView input_view) { pp_update_bind_group(device_, pipeline_, &bind_group_, input_view, uniforms_); } void render(WGPURenderPassEncoder pass, float time, float beat, float peak_value, float aspect_ratio) { (void)time; (void)beat; (void)aspect_ratio; float uniforms[4] = {peak_value, 0.0f, 0.0f, 0.0f}; wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, uniforms, sizeof(uniforms)); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); } }; #if !defined(STRIP_ALL) static void print_usage(const char* prog_name) { printf("Usage: %s [OPTIONS]\n", prog_name); printf("\nMinimal audio/visual synchronization test tool.\n"); printf("Plays a simple drum beat with synchronized screen flashes.\n"); printf("\nOptions:\n"); printf(" --help Show this help message and exit\n"); printf(" --fullscreen Run in fullscreen mode\n"); printf(" --resolution WxH Set window resolution (e.g., 1024x768)\n"); printf(" --tempo Enable tempo variation test mode\n"); printf(" (alternates between acceleration and deceleration)\n"); printf(" --log-peaks FILE Log audio peaks at each beat (32 samples for 16s)\n"); printf(" --log-peaks-fine Log at each frame for fine analysis (~960 samples)\n"); printf(" (use with --log-peaks for millisecond resolution)\n"); printf("\nExamples:\n"); printf(" %s --fullscreen\n", prog_name); printf(" %s --resolution 1024x768 --tempo\n", prog_name); printf(" %s --log-peaks peaks.txt\n", prog_name); printf(" %s --log-peaks peaks.txt --log-peaks-fine\n", prog_name); printf("\nControls:\n"); printf(" ESC Exit the demo\n"); printf(" F Toggle fullscreen\n"); } #endif int main(int argc, char** argv) { // Parse command-line PlatformState platform_state; bool fullscreen_enabled = false; bool tempo_test_enabled = false; int width = 1280; int height = 720; const char* log_peaks_file = nullptr; bool log_peaks_fine = false; #if !defined(STRIP_ALL) // Early exit for invalid options for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); return 0; } else if (strcmp(argv[i], "--fullscreen") == 0) { fullscreen_enabled = true; } else if (strcmp(argv[i], "--tempo") == 0) { tempo_test_enabled = true; } else if (strcmp(argv[i], "--resolution") == 0) { if (i + 1 < argc) { const char* res_str = argv[++i]; int w, h; if (sscanf(res_str, "%dx%d", &w, &h) == 2) { width = w; height = h; } else { fprintf(stderr, "Error: Invalid resolution format '%s' (expected WxH, e.g., 1024x768)\n\n", res_str); print_usage(argv[0]); return 1; } } else { fprintf(stderr, "Error: --resolution requires an argument (e.g., 1024x768)\n\n"); print_usage(argv[0]); return 1; } } else if (strcmp(argv[i], "--log-peaks") == 0) { if (i + 1 < argc) { log_peaks_file = argv[++i]; } else { fprintf(stderr, "Error: --log-peaks requires a filename argument\n\n"); print_usage(argv[0]); return 1; } } else if (strcmp(argv[i], "--log-peaks-fine") == 0) { log_peaks_fine = true; } else { fprintf(stderr, "Error: Unknown option '%s'\n\n", argv[i]); print_usage(argv[0]); return 1; } } #else (void)argc; (void)argv; fullscreen_enabled = true; #endif // Initialize platform, GPU, audio platform_state = platform_init(fullscreen_enabled, width, height); gpu_init(&platform_state); // Add peak meter visualization effect (renders as final post-process) #if !defined(STRIP_ALL) const GpuContext* gpu_ctx = gpu_get_context(); auto* peak_meter = new PeakMeterEffect(*gpu_ctx); gpu_add_custom_effect(peak_meter, 0.0f, 99999.0f, 999); // High priority = renders last #endif audio_init(); static AudioEngine g_audio_engine; g_audio_engine.init(); // Music time tracking with optional tempo variation static float g_music_time = 0.0f; static double g_last_physical_time = 0.0; static float g_tempo_scale = 1.0f; auto fill_audio_buffer = [&](double t) { const float dt = (float)(t - g_last_physical_time); g_last_physical_time = t; // Calculate tempo scale if --tempo flag enabled if (tempo_test_enabled) { // Each bar = 2 seconds at 120 BPM (4 beats) const float bar_duration = 2.0f; const int bar_number = (int)(t / bar_duration); const float bar_progress = fmodf((float)t, bar_duration) / bar_duration; // 0.0-1.0 within bar if (bar_number % 2 == 0) { // Even bars: Ramp from 1.0x → 1.5x g_tempo_scale = 1.0f + (0.5f * bar_progress); } else { // Odd bars: Ramp from 1.0x → 0.66x g_tempo_scale = 1.0f - (0.34f * bar_progress); } } else { g_tempo_scale = 1.0f; // No tempo variation } g_music_time += dt * g_tempo_scale; g_audio_engine.update(g_music_time); audio_render_ahead(g_music_time, dt * g_tempo_scale); }; // Pre-fill audio buffer g_audio_engine.update(g_music_time); audio_render_ahead(g_music_time, 1.0f / 60.0f); audio_start(); int last_width = platform_state.width; int last_height = platform_state.height; const float demo_duration = GetDemoDuration(); #if !defined(STRIP_ALL) // Open peak log file if requested FILE* peak_log = nullptr; if (log_peaks_file) { peak_log = fopen(log_peaks_file, "w"); if (peak_log) { fprintf(peak_log, "# Audio peak log from test_demo\n"); fprintf(peak_log, "# Mode: %s\n", log_peaks_fine ? "fine (per-frame)" : "beat-aligned"); fprintf(peak_log, "# To plot with gnuplot:\n"); fprintf(peak_log, "# gnuplot -p -e \"set xlabel 'Time (s)'; set ylabel 'Peak'; plot '%s' using 2:3 with lines title 'Raw Peak'\"\n", log_peaks_file); if (log_peaks_fine) { fprintf(peak_log, "# Columns: frame_number clock_time raw_peak beat_number\n"); } else { fprintf(peak_log, "# Columns: beat_number clock_time raw_peak\n"); } fprintf(peak_log, "#\n"); } else { fprintf(stderr, "Warning: Could not open log file '%s'\n", log_peaks_file); } } int last_beat_logged = -1; int frame_number = 0; #endif // Main loop while (!platform_should_close(&platform_state)) { platform_poll(&platform_state); // Handle resize if (platform_state.width != last_width || platform_state.height != last_height) { last_width = platform_state.width; last_height = platform_state.height; gpu_resize(last_width, last_height); } const double physical_time = platform_state.time; // Auto-exit at end (based on physical time for reliability) if (demo_duration > 0.0f && physical_time >= demo_duration) { #if !defined(STRIP_ALL) printf("test_demo finished at %.2f seconds.\n", physical_time); #endif break; } fill_audio_buffer(physical_time); // Audio-visual synchronization: Use audio playback time (not physical time!) // This accounts for ring buffer latency automatically (no hardcoded constants) const float audio_time = audio_get_playback_time(); // Audio/visual sync parameters const float aspect_ratio = platform_state.aspect_ratio; // Peak is measured at audio playback time, so it matches audio_time const float raw_peak = audio_get_realtime_peak(); const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); // Beat calculation uses AUDIO TIME (what's being heard), not physical time const float beat_time = audio_time * 120.0f / 60.0f; const int beat_number = (int)beat_time; const float beat = fmodf(beat_time, 1.0f); #if !defined(STRIP_ALL) // Log peak (either per-frame or per-beat) if (peak_log) { if (log_peaks_fine) { // Log every frame for fine-grained analysis fprintf(peak_log, "%d %.6f %.6f %d\n", frame_number, audio_time, raw_peak, beat_number); } else if (beat_number != last_beat_logged) { // Log only at beat boundaries fprintf(peak_log, "%d %.6f %.6f\n", beat_number, audio_time, raw_peak); last_beat_logged = beat_number; } } frame_number++; // Debug output every 0.5 seconds (based on audio time for consistency) static float last_print_time = -1.0f; if (audio_time - last_print_time >= 0.5f) { if (tempo_test_enabled) { printf("[AudioT=%.2f, PhysT=%.2f, MusicT=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f, Tempo=%.2fx]\n", audio_time, (float)physical_time, g_music_time, beat_number, beat, visual_peak, g_tempo_scale); } else { printf("[AudioT=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f]\n", audio_time, beat_number, beat, visual_peak); } last_print_time = audio_time; } #endif gpu_draw(visual_peak, aspect_ratio, audio_time, beat); audio_update(); } // Shutdown #if !defined(STRIP_ALL) if (peak_log) { fclose(peak_log); printf("Peak log written to '%s'\n", log_peaks_file); } #endif audio_shutdown(); gpu_shutdown(); platform_shutdown(&platform_state); return 0; }