From 10673f00dfece584ba81d581b69c9ba706a5ea5a Mon Sep 17 00:00:00 2001 From: skal Date: Fri, 13 Feb 2026 08:14:07 +0100 Subject: Refactor: Move application entry points to src/app/ Moved main.cc, stub_main.cc, and test_demo.cc from src/ to src/app/ for better organization. Updated cmake/DemoExecutables.cmake paths. handoff(Claude): App files reorganized into src/app/ directory --- src/app/main.cc | 394 ++++++++++++++++++++++++++++++++++++++++++++++ src/app/stub_main.cc | 13 ++ src/app/test_demo.cc | 433 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.cc | 394 ---------------------------------------------- src/stub_main.cc | 13 -- src/test_demo.cc | 433 --------------------------------------------------- 6 files changed, 840 insertions(+), 840 deletions(-) create mode 100644 src/app/main.cc create mode 100644 src/app/stub_main.cc create mode 100644 src/app/test_demo.cc delete mode 100644 src/main.cc delete mode 100644 src/stub_main.cc delete mode 100644 src/test_demo.cc (limited to 'src') diff --git a/src/app/main.cc b/src/app/main.cc new file mode 100644 index 0000000..45a642a --- /dev/null +++ b/src/app/main.cc @@ -0,0 +1,394 @@ +// This file is part of the 64k demo project. +// It serves as the application entry point. +// Orchestrates platform initialization, main loop, and subsystem coordination. + +#include "3d/renderer.h" +#include "audio/audio.h" +#include "audio/audio_engine.h" +#include "audio/gen.h" +#include "audio/ring_buffer.h" +#include "audio/synth.h" +#include "audio/tracker.h" +#if !defined(STRIP_ALL) +#include "audio/backend/wav_dump_backend.h" +#include "util/file_watcher.h" +#include +#if defined(DEMO_HEADLESS) +#include +#endif +#endif +#include "generated/assets.h" // Include generated asset header +#include "gpu/demo_effects.h" // For GetDemoDuration() +#include "gpu/gpu.h" +#include "platform/platform.h" +#include "util/math.h" +#include +#include +#include +#include + +#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) +static WavDumpBackend* g_wav_backend_ptr = nullptr; +static void signal_handler(int sig) { + if (g_wav_backend_ptr != nullptr) { + g_wav_backend_ptr->shutdown(); + g_wav_backend_ptr = nullptr; + } + exit(sig); +} +#endif + +int main(int argc, char** argv) { + PlatformState platform_state; + bool fullscreen_enabled = false; + float seek_time = 0.0f; + int width = 1280; + int height = 720; + bool dump_wav = false; + bool tempo_test_enabled = false; + bool headless_mode = false; + float headless_duration = 30.0f; + const char* wav_output_file = "audio_dump.wav"; + bool hot_reload_enabled = false; + +#if !defined(STRIP_ALL) + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--fullscreen") == 0) { + fullscreen_enabled = true; + } else if (strcmp(argv[i], "--seek") == 0 && i + 1 < argc) { + seek_time = atof(argv[i + 1]); + ++i; + } else if (strcmp(argv[i], "--resolution") == 0 && 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 if (strcmp(argv[i], "--debug") == 0) { + Renderer3D::SetDebugEnabled(true); + } else if (strcmp(argv[i], "--dump_wav") == 0) { + dump_wav = true; + // Optional: allow specifying output filename + if (i + 1 < argc && argv[i + 1][0] != '-') { + wav_output_file = argv[++i]; + } + } else if (strcmp(argv[i], "--tempo") == 0) { + tempo_test_enabled = true; +#if defined(DEMO_HEADLESS) + } else if (strcmp(argv[i], "--headless") == 0) { + headless_mode = true; + } else if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc) { + headless_duration = atof(argv[i + 1]); + ++i; +#endif + } else if (strcmp(argv[i], "--hot-reload") == 0) { + hot_reload_enabled = true; + printf("Hot-reload enabled (watching config files)\n"); + } + } +#else + (void)argc; + (void)argv; + fullscreen_enabled = true; +#endif /* STRIP_ALL */ + + platform_state = platform_init(fullscreen_enabled, width, height); + gpu_init(&platform_state); + + // Load timeline data (visual effects layering) +#if !defined(DEMO_HEADLESS) + LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); +#endif + +#if !defined(STRIP_ALL) + // Set WAV dump backend if requested + WavDumpBackend wav_backend; + if (dump_wav) { + wav_backend.set_output_file(wav_output_file); + audio_set_backend(&wav_backend); +#if defined(DEMO_HEADLESS) + g_wav_backend_ptr = &wav_backend; + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); +#endif + printf("WAV dump mode enabled: %s\n", wav_output_file); + } +#endif + + // Initialize audio backend (miniaudio device) + audio_init(); + + // Initialize audio engine (synth + tracker unified management) + static AudioEngine g_audio_engine; + g_audio_engine.init(); + + // Generate and play melody (replaced by tracker) + // int melody_id = generate_melody(); + // synth_trigger_voice(melody_id, 0.6f, 0.0f); + + // Music time state for variable tempo + static float g_music_time = 0.0f; + static float g_tempo_scale = 1.0f; // 1.0 = normal speed + static float g_last_audio_time = 0.0f; + + auto fill_audio_buffer = [&](float audio_dt, double physical_time) { + // Calculate tempo scale if --tempo flag enabled + if (tempo_test_enabled) { + const float t = (float)physical_time; + if (t >= 2.0f && t < 4.0f) { + // [2s->4s]: Accelerate from 1.0x to 1.5x + const float progress = (t - 2.0f) / 2.0f; + g_tempo_scale = 1.0f + (0.5f * progress); + } else if (t >= 6.0f && t < 8.0f) { + // [6s->8s]: Decelerate from 1.0x to 0.66x + const float progress = (t - 6.0f) / 2.0f; + g_tempo_scale = 1.0f - (0.34f * progress); + } else { + // All other times: Normal tempo + g_tempo_scale = 1.0f; + } + } else { + g_tempo_scale = 1.0f; // No tempo variation + } + + const float prev_tempo = g_tempo_scale; + +#if !defined(STRIP_ALL) + // Debug output when tempo changes significantly + if (fabsf(g_tempo_scale - prev_tempo) > 0.05f) { + printf("[Tempo] t=%.2fs, tempo=%.3fx, music_time=%.3fs\n", + (float)physical_time, g_tempo_scale, g_music_time); + } +#endif + + // CRITICAL: Update tracker BEFORE advancing music_time + // This ensures events trigger in the correct frame, not one frame early + // Pass current music_time (not future time) to tracker + g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale); + + // Fill ring buffer with upcoming audio (look-ahead rendering) + // CRITICAL: Scale dt by tempo to render enough audio during + // acceleration/deceleration At 2.0x tempo, we consume 2x audio per physical + // second, so we must render 2x per frame + audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); + + // Advance music time AFTER rendering audio for this frame + // This prevents events from triggering one frame early + g_music_time += audio_dt * g_tempo_scale; + }; + +#if !defined(STRIP_ALL) + if (seek_time > 0.0) { + printf("Seeking to %.2f seconds...\n", seek_time); + + // Simulate audio/game logic + // We step at ~60hz + const double step = 1.0 / 60.0; + for (double t = 0.0; t < seek_time; t += step) { + fill_audio_buffer(step, t); + audio_render_silent((float)step); + } + + // Simulate Visuals + gpu_simulate_until((float)seek_time, g_tracker_score.bpm); + } +#endif /* !defined(STRIP_ALL) */ + + // Pre-fill using same pattern as main loop (100ms) + fill_audio_buffer(0.1f, 0.0); + + audio_start(); + g_last_audio_time = audio_get_playback_time(); // Initialize after start + +#if !defined(STRIP_ALL) + // Hot-reload setup + FileWatcher file_watcher; + if (hot_reload_enabled) { + file_watcher.add_file("assets/final/demo_assets.txt"); + file_watcher.add_file("assets/demo.seq"); + file_watcher.add_file("assets/music.track"); + } +#endif + +#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) + // In headless mode, run simulation without rendering + if (headless_mode) { + printf("Running headless simulation (%.1fs)...\n", headless_duration); + + const float update_dt = 1.0f / 60.0f; + double physical_time = 0.0; + while (physical_time < headless_duration) { + fill_audio_buffer(update_dt, physical_time); + gpu_simulate_until(g_music_time); + physical_time += update_dt; + + if ((int)physical_time % 5 == 0 && + physical_time - update_dt < (int)physical_time) { + printf(" Progress: %.1fs / %.1fs (music: %.1fs)\r", + physical_time, headless_duration, g_music_time); + fflush(stdout); + } + } + + printf("\nHeadless simulation complete: %.2fs\n", physical_time); + + g_audio_engine.shutdown(); + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); + return 0; + } + + // In WAV dump mode, run headless simulation and write audio to file + if (dump_wav) { + printf("Running WAV dump simulation...\n"); + + const float demo_duration = GetDemoDuration(); + const float max_duration = (demo_duration > 0.0f) ? demo_duration : 60.0f; + const float update_dt = 1.0f / 60.0f; // 60Hz update rate + const int frames_per_update = (int)(32000 * update_dt); // ~533 frames + const int samples_per_update = frames_per_update * 2; // Stereo + + AudioRingBuffer* ring_buffer = audio_get_ring_buffer(); + std::vector chunk_buffer(samples_per_update); + + double physical_time = 0.0; + while (physical_time < max_duration) { + // Update music time and tracker (using tempo logic from + // fill_audio_buffer) + fill_audio_buffer(update_dt, physical_time); + + // Read rendered audio from ring buffer + if (ring_buffer != nullptr) { + ring_buffer->read(chunk_buffer.data(), samples_per_update); + } + + // Write to WAV file + wav_backend.write_audio(chunk_buffer.data(), samples_per_update); + + physical_time += update_dt; + + // Progress indicator every second + if ((int)physical_time % 1 == 0 && + physical_time - update_dt < (int)physical_time) { + printf(" Rendering: %.1fs / %.1fs (music: %.1fs, tempo: %.2fx)\r", + physical_time, max_duration, g_music_time, g_tempo_scale); + fflush(stdout); + } + } + + printf("\nWAV dump complete: %.2fs physical, %.2fs music time\n", + physical_time, g_music_time); + +#if defined(DEMO_HEADLESS) + g_wav_backend_ptr = nullptr; +#endif + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); + return 0; + } +#endif + +#if !defined(DEMO_HEADLESS) + int last_width = platform_state.width; + int last_height = platform_state.height; + + // Get demo duration from sequence file (or -1 if not specified) + const float demo_duration = GetDemoDuration(); + + static double last_frame_time = 0.0; + while (!platform_should_close(&platform_state)) { + platform_poll(&platform_state); + + 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); + } + + // Graphics frame time - derived from platform's clock + const double current_physical_time = platform_state.time + seek_time; + const double graphics_frame_time = current_physical_time - last_frame_time; + // Audio playback time - master clock for audio events + const float current_audio_time = audio_get_playback_time(); + // Delta time for audio processing, based on audio clock + const float audio_dt = current_audio_time - g_last_audio_time; + g_last_audio_time = current_audio_time; + + // Auto-exit when demo finishes (if duration is specified) + if (demo_duration > 0.0f && current_physical_time >= demo_duration) { +#if !defined(STRIP_ALL) + printf("Demo finished at %.2f seconds. Exiting...\n", + current_physical_time); +#endif + break; + } + + // This fill_audio_buffer call is crucial for audio system to process + // events based on the *current audio time* and *graphics physical time* + // context + fill_audio_buffer(audio_dt, current_physical_time); + +#if !defined(STRIP_ALL) + // Hot-reload: Check for file changes + if (hot_reload_enabled && file_watcher.check_changes()) { + printf("\n[Hot-Reload] Config files changed - rebuild required\n"); + printf("[Hot-Reload] Run: cmake --build build -j4 && ./build/demo64k\n"); + file_watcher.reset(); + } +#endif + + // --- Graphics Update --- + const float aspect_ratio = platform_state.aspect_ratio; + + // Peak value derived from audio, but used for visual effect intensity + const float raw_peak = audio_get_realtime_peak(); + const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); + + // Beat calculation: convert audio time to musical beats + const float absolute_beat_time = current_audio_time * g_tracker_score.bpm / 60.0f; + const int beat_number = (int)absolute_beat_time; + const float beat_phase = fmodf(absolute_beat_time, 1.0f); // Fractional part (0.0 to 1.0) + + // Print beat/time info periodically for identifying sync points + // Use graphics time for the print interval to avoid excessive output if + // audio clock is slow + static float last_graphics_print_time = -1.0f; + if (current_physical_time - last_graphics_print_time >= + 0.5f) { // Print every 0.5 seconds + if (tempo_test_enabled) { + printf( + "[GraphicsT=%.2f, AudioT=%.2f, MusicT=%.2f, Beat=%d, Phase=%.2f, " + "Peak=%.2f, Tempo=%.2fx]\n", + current_physical_time, current_audio_time, g_music_time, + beat_number, beat_phase, visual_peak, g_tempo_scale); + } else { + printf("[GraphicsT=%.2f, AudioT=%.2f, Beat=%d, Phase=%.2f, Peak=%.2f]\n", + current_physical_time, current_audio_time, beat_number, beat_phase, + visual_peak); + } + last_graphics_print_time = current_physical_time; + } + + // Draw graphics using physical time and musical beat time + gpu_draw(visual_peak, aspect_ratio, (float)current_physical_time, + absolute_beat_time, beat_phase); + last_frame_time = current_physical_time; + + // Update audio systems (tracker, synth, etc.) based on audio time + // progression + audio_update(); + } + +#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) + g_wav_backend_ptr = nullptr; +#endif + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); +#endif /* !defined(DEMO_HEADLESS) */ + return 0; +} \ No newline at end of file diff --git a/src/app/stub_main.cc b/src/app/stub_main.cc new file mode 100644 index 0000000..8540fcd --- /dev/null +++ b/src/app/stub_main.cc @@ -0,0 +1,13 @@ +// Stub main for size measurement builds. +// Binary compiles but does NOT run. +// This file is only compiled when STRIP_EXTERNAL_LIBS is defined. + +#if defined(STRIP_EXTERNAL_LIBS) + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + return 0; +} + +#endif // STRIP_EXTERNAL_LIBS diff --git a/src/app/test_demo.cc b/src/app/test_demo.cc new file mode 100644 index 0000000..7f10c3b --- /dev/null +++ b/src/app/test_demo.cc @@ -0,0 +1,433 @@ +// 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 "audio/tracker.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 "util/check_return.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" +#include "gpu/effects/shader_composer.h" +#include "gpu/effects/cnn_effect.h" +#include "gpu/effects/cnn_v2_effect.h" + +class PeakMeterEffect : public PostProcessEffect { + public: + PeakMeterEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { + // Use ShaderComposer to include CommonUniforms from common_uniforms.wgsl + const char* shader_main = R"( + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + }; + + struct EffectParams { + unused: f32, + }; + + @group(0) @binding(0) var inputSampler: sampler; + @group(0) @binding(1) var inputTexture: texture_2d; + @group(0) @binding(2) var uniforms: CommonUniforms; + @group(0) @binding(3) var params: EffectParams; + + @vertex + fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { + var output: VertexOutput; + // Full-screen triangle (required for post-process pass-through) + 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 { + // Bar dimensions + let bar_y_min = 0.005; + let bar_y_max = 0.015; + let bar_x_min = 0.015; + let bar_x_max = 0.250; + 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_min && input.uv.x <= bar_x_max; + + // Optimization: Return bar color early (avoids texture sampling for ~5% of pixels) + if (in_bar_y && in_bar_x) { + let uv_x = (input.uv.x - bar_x_min) / (bar_x_max - bar_x_min); + let factor = step(uv_x, uniforms.audio_intensity); + return mix(vec4(0.0, 0.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0,1.0), factor); + } + + // Pass through input texture for rest of screen + return textureSample(inputTexture, inputSampler, input.uv); + } + )"; + + // Compose shader with common_uniforms to get CommonUniforms definition + std::string shader_code = ShaderComposer::Get().Compose( + {"common_uniforms"}, shader_main); + + pipeline_ = + create_post_process_pipeline(ctx_.device, ctx_.format, shader_code.c_str()); + } + + void update_bind_group(WGPUTextureView input_view) override { + pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, + uniforms_.get(), {}); + } + + void render(WGPURenderPassEncoder pass, + const CommonPostProcessUniforms& uniforms) override { + uniforms_.update(ctx_.queue, uniforms); + PostProcessEffect::render(pass, uniforms); + } +}; + +static int g_cnn_version = 2; // Default to v2 + +#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(" --cnn-version <1|2> Select CNN version (1=v1, 2=v2, default=2)\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 --cnn-version 1\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; + // Seek time needs to be declared here to be accessible globally for the loop. + float seek_time = 0.0f; + +#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) { + CHECK_RETURN_BEGIN(i + 1 >= argc, 1) + print_usage(argv[0]); + ERROR_MSG("--log-peaks requires a filename argument\n"); + return 1; + CHECK_RETURN_END + log_peaks_file = argv[++i]; + } else if (strcmp(argv[i], "--log-peaks-fine") == 0) { + log_peaks_fine = true; + } else if (strcmp(argv[i], "--cnn-version") == 0) { + if (i + 1 < argc) { + int version = atoi(argv[++i]); + if (version == 1 || version == 2) { + g_cnn_version = version; + } else { + fprintf(stderr, "Error: --cnn-version must be 1 or 2\n\n"); + print_usage(argv[0]); + return 1; + } + } else { + fprintf(stderr, "Error: --cnn-version requires argument\n\n"); + print_usage(argv[0]); + return 1; + } + } else { + CHECK_RETURN_BEGIN(true, 1) + print_usage(argv[0]); + ERROR_MSG("Unknown option '%s'\n", argv[i]); + return 1; + CHECK_RETURN_END + } + } +#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); + + // Load timeline from test_demo.seq + LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); + +#if !defined(STRIP_ALL) + const GpuContext* gpu_ctx = gpu_get_context(); + + // Add CNN post-processing effect based on version flag + if (g_cnn_version == 1) { + CNNEffectParams params; + params.blend_amount = 1.0f; + auto* cnn = new CNNEffect(*gpu_ctx, params); + cnn->set_beat_modulation(true, 1.0f); + gpu_add_custom_effect(cnn, 0.0f, 99999.0f, 10); + } else if (g_cnn_version == 2) { + CNNv2EffectParams params; + params.blend_amount = 1.0f; + auto* cnn = new CNNv2Effect(*gpu_ctx, params); + cnn->set_beat_modulation(true, 1.0f); + gpu_add_custom_effect(cnn, 0.0f, 99999.0f, 10); + } + + // Add peak meter visualization effect (renders as final post-process) + 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 float g_last_audio_time = 0.0f; + static float g_tempo_scale = 1.0f; + + auto fill_audio_buffer = [&](float audio_dt, double physical_time) { + // Calculate tempo scale if --tempo flag enabled + if (tempo_test_enabled) { + const float t = (float)physical_time; + if (t >= 2.0f && t < 4.0f) { + // [2s->4s]: Accelerate from 1.0x to 1.5x + const float progress = (t - 2.0f) / 2.0f; + g_tempo_scale = 1.0f + (0.5f * progress); + } else if (t >= 6.0f && t < 8.0f) { + // [6s->8s]: Decelerate from 1.0x to 0.66x + const float progress = (t - 6.0f) / 2.0f; + g_tempo_scale = 1.0f - (0.34f * progress); + } else { + // All other times: Normal tempo + g_tempo_scale = 1.0f; + } + } else { + g_tempo_scale = 1.0f; // No tempo variation + } + + g_music_time += audio_dt * g_tempo_scale; + + g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale); + audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); + }; + + // Pre-fill using same pattern as main loop (100ms) + fill_audio_buffer(0.1f, 0.0); + + audio_start(); + g_last_audio_time = audio_get_playback_time(); + + 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); + } + + // Graphics frame time - derived from platform's clock + const double current_physical_time = platform_state.time + seek_time; + // Audio playback time - master clock for audio events + const float current_audio_time = audio_get_playback_time(); + // Delta time for audio processing, based on audio clock + const float audio_dt = current_audio_time - g_last_audio_time; + g_last_audio_time = current_audio_time; + + // Auto-exit at end (based on physical time for graphics loop consistency) + if (demo_duration > 0.0f && current_physical_time >= demo_duration) { +#if !defined(STRIP_ALL) + printf("test_demo finished at %.2f seconds. Exiting...\n", + current_physical_time); +#endif + break; + } + + // This fill_audio_buffer call is crucial for audio system to process + // events based on the *current audio time* and *graphics physical time* + // context + fill_audio_buffer(audio_dt, current_physical_time); + + // --- Graphics Update --- + const float aspect_ratio = platform_state.aspect_ratio; + + // Peak value derived from audio, but used for visual effect intensity + const float raw_peak = audio_get_realtime_peak(); + const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); + + // Beat calculation: convert audio time to musical beats + const float absolute_beat_time = current_audio_time * g_tracker_score.bpm / 60.0f; + const int beat_number = (int)absolute_beat_time; + const float beat_phase = fmodf(absolute_beat_time, 1.0f); // Fractional part (0.0 to 1.0) + +#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 + // Use platform_get_time() for high-resolution timestamps (not + // audio_time which advances in chunks) + const double frame_time = platform_get_time(); + fprintf(peak_log, "%d %.6f %.6f %d\n", frame_number, frame_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, current_audio_time, + raw_peak); + last_beat_logged = beat_number; + } + } + frame_number++; + + // Debug output every 0.5 seconds (based on graphics time for consistency) + static float last_graphics_print_time = -1.0f; + if (current_physical_time - last_graphics_print_time >= 0.5f) { + if (tempo_test_enabled) { + printf( + "[GraphicsT=%.2f, AudioT=%.2f, MusicT=%.2f, Beat=%d, Phase=%.2f, " + "Peak=%.2f, Tempo=%.2fx]\n", + current_physical_time, current_audio_time, g_music_time, + beat_number, beat_phase, visual_peak, g_tempo_scale); + } else { + printf("[GraphicsT=%.2f, AudioT=%.2f, Beat=%d, Phase=%.2f, Peak=%.2f]\n", + current_physical_time, current_audio_time, beat_number, beat_phase, + visual_peak); + } + last_graphics_print_time = current_physical_time; + } +#endif + + // Draw graphics using physical time and musical beat time + const float graphics_frame_time = (float)current_physical_time; + gpu_draw(visual_peak, aspect_ratio, graphics_frame_time, + absolute_beat_time, beat_phase); + + // Update audio systems (tracker, synth, etc.) based on audio time + // progression + 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; +} diff --git a/src/main.cc b/src/main.cc deleted file mode 100644 index 45a642a..0000000 --- a/src/main.cc +++ /dev/null @@ -1,394 +0,0 @@ -// This file is part of the 64k demo project. -// It serves as the application entry point. -// Orchestrates platform initialization, main loop, and subsystem coordination. - -#include "3d/renderer.h" -#include "audio/audio.h" -#include "audio/audio_engine.h" -#include "audio/gen.h" -#include "audio/ring_buffer.h" -#include "audio/synth.h" -#include "audio/tracker.h" -#if !defined(STRIP_ALL) -#include "audio/backend/wav_dump_backend.h" -#include "util/file_watcher.h" -#include -#if defined(DEMO_HEADLESS) -#include -#endif -#endif -#include "generated/assets.h" // Include generated asset header -#include "gpu/demo_effects.h" // For GetDemoDuration() -#include "gpu/gpu.h" -#include "platform/platform.h" -#include "util/math.h" -#include -#include -#include -#include - -#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) -static WavDumpBackend* g_wav_backend_ptr = nullptr; -static void signal_handler(int sig) { - if (g_wav_backend_ptr != nullptr) { - g_wav_backend_ptr->shutdown(); - g_wav_backend_ptr = nullptr; - } - exit(sig); -} -#endif - -int main(int argc, char** argv) { - PlatformState platform_state; - bool fullscreen_enabled = false; - float seek_time = 0.0f; - int width = 1280; - int height = 720; - bool dump_wav = false; - bool tempo_test_enabled = false; - bool headless_mode = false; - float headless_duration = 30.0f; - const char* wav_output_file = "audio_dump.wav"; - bool hot_reload_enabled = false; - -#if !defined(STRIP_ALL) - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--fullscreen") == 0) { - fullscreen_enabled = true; - } else if (strcmp(argv[i], "--seek") == 0 && i + 1 < argc) { - seek_time = atof(argv[i + 1]); - ++i; - } else if (strcmp(argv[i], "--resolution") == 0 && 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 if (strcmp(argv[i], "--debug") == 0) { - Renderer3D::SetDebugEnabled(true); - } else if (strcmp(argv[i], "--dump_wav") == 0) { - dump_wav = true; - // Optional: allow specifying output filename - if (i + 1 < argc && argv[i + 1][0] != '-') { - wav_output_file = argv[++i]; - } - } else if (strcmp(argv[i], "--tempo") == 0) { - tempo_test_enabled = true; -#if defined(DEMO_HEADLESS) - } else if (strcmp(argv[i], "--headless") == 0) { - headless_mode = true; - } else if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc) { - headless_duration = atof(argv[i + 1]); - ++i; -#endif - } else if (strcmp(argv[i], "--hot-reload") == 0) { - hot_reload_enabled = true; - printf("Hot-reload enabled (watching config files)\n"); - } - } -#else - (void)argc; - (void)argv; - fullscreen_enabled = true; -#endif /* STRIP_ALL */ - - platform_state = platform_init(fullscreen_enabled, width, height); - gpu_init(&platform_state); - - // Load timeline data (visual effects layering) -#if !defined(DEMO_HEADLESS) - LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); -#endif - -#if !defined(STRIP_ALL) - // Set WAV dump backend if requested - WavDumpBackend wav_backend; - if (dump_wav) { - wav_backend.set_output_file(wav_output_file); - audio_set_backend(&wav_backend); -#if defined(DEMO_HEADLESS) - g_wav_backend_ptr = &wav_backend; - signal(SIGINT, signal_handler); - signal(SIGTERM, signal_handler); -#endif - printf("WAV dump mode enabled: %s\n", wav_output_file); - } -#endif - - // Initialize audio backend (miniaudio device) - audio_init(); - - // Initialize audio engine (synth + tracker unified management) - static AudioEngine g_audio_engine; - g_audio_engine.init(); - - // Generate and play melody (replaced by tracker) - // int melody_id = generate_melody(); - // synth_trigger_voice(melody_id, 0.6f, 0.0f); - - // Music time state for variable tempo - static float g_music_time = 0.0f; - static float g_tempo_scale = 1.0f; // 1.0 = normal speed - static float g_last_audio_time = 0.0f; - - auto fill_audio_buffer = [&](float audio_dt, double physical_time) { - // Calculate tempo scale if --tempo flag enabled - if (tempo_test_enabled) { - const float t = (float)physical_time; - if (t >= 2.0f && t < 4.0f) { - // [2s->4s]: Accelerate from 1.0x to 1.5x - const float progress = (t - 2.0f) / 2.0f; - g_tempo_scale = 1.0f + (0.5f * progress); - } else if (t >= 6.0f && t < 8.0f) { - // [6s->8s]: Decelerate from 1.0x to 0.66x - const float progress = (t - 6.0f) / 2.0f; - g_tempo_scale = 1.0f - (0.34f * progress); - } else { - // All other times: Normal tempo - g_tempo_scale = 1.0f; - } - } else { - g_tempo_scale = 1.0f; // No tempo variation - } - - const float prev_tempo = g_tempo_scale; - -#if !defined(STRIP_ALL) - // Debug output when tempo changes significantly - if (fabsf(g_tempo_scale - prev_tempo) > 0.05f) { - printf("[Tempo] t=%.2fs, tempo=%.3fx, music_time=%.3fs\n", - (float)physical_time, g_tempo_scale, g_music_time); - } -#endif - - // CRITICAL: Update tracker BEFORE advancing music_time - // This ensures events trigger in the correct frame, not one frame early - // Pass current music_time (not future time) to tracker - g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale); - - // Fill ring buffer with upcoming audio (look-ahead rendering) - // CRITICAL: Scale dt by tempo to render enough audio during - // acceleration/deceleration At 2.0x tempo, we consume 2x audio per physical - // second, so we must render 2x per frame - audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); - - // Advance music time AFTER rendering audio for this frame - // This prevents events from triggering one frame early - g_music_time += audio_dt * g_tempo_scale; - }; - -#if !defined(STRIP_ALL) - if (seek_time > 0.0) { - printf("Seeking to %.2f seconds...\n", seek_time); - - // Simulate audio/game logic - // We step at ~60hz - const double step = 1.0 / 60.0; - for (double t = 0.0; t < seek_time; t += step) { - fill_audio_buffer(step, t); - audio_render_silent((float)step); - } - - // Simulate Visuals - gpu_simulate_until((float)seek_time, g_tracker_score.bpm); - } -#endif /* !defined(STRIP_ALL) */ - - // Pre-fill using same pattern as main loop (100ms) - fill_audio_buffer(0.1f, 0.0); - - audio_start(); - g_last_audio_time = audio_get_playback_time(); // Initialize after start - -#if !defined(STRIP_ALL) - // Hot-reload setup - FileWatcher file_watcher; - if (hot_reload_enabled) { - file_watcher.add_file("assets/final/demo_assets.txt"); - file_watcher.add_file("assets/demo.seq"); - file_watcher.add_file("assets/music.track"); - } -#endif - -#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) - // In headless mode, run simulation without rendering - if (headless_mode) { - printf("Running headless simulation (%.1fs)...\n", headless_duration); - - const float update_dt = 1.0f / 60.0f; - double physical_time = 0.0; - while (physical_time < headless_duration) { - fill_audio_buffer(update_dt, physical_time); - gpu_simulate_until(g_music_time); - physical_time += update_dt; - - if ((int)physical_time % 5 == 0 && - physical_time - update_dt < (int)physical_time) { - printf(" Progress: %.1fs / %.1fs (music: %.1fs)\r", - physical_time, headless_duration, g_music_time); - fflush(stdout); - } - } - - printf("\nHeadless simulation complete: %.2fs\n", physical_time); - - g_audio_engine.shutdown(); - audio_shutdown(); - gpu_shutdown(); - platform_shutdown(&platform_state); - return 0; - } - - // In WAV dump mode, run headless simulation and write audio to file - if (dump_wav) { - printf("Running WAV dump simulation...\n"); - - const float demo_duration = GetDemoDuration(); - const float max_duration = (demo_duration > 0.0f) ? demo_duration : 60.0f; - const float update_dt = 1.0f / 60.0f; // 60Hz update rate - const int frames_per_update = (int)(32000 * update_dt); // ~533 frames - const int samples_per_update = frames_per_update * 2; // Stereo - - AudioRingBuffer* ring_buffer = audio_get_ring_buffer(); - std::vector chunk_buffer(samples_per_update); - - double physical_time = 0.0; - while (physical_time < max_duration) { - // Update music time and tracker (using tempo logic from - // fill_audio_buffer) - fill_audio_buffer(update_dt, physical_time); - - // Read rendered audio from ring buffer - if (ring_buffer != nullptr) { - ring_buffer->read(chunk_buffer.data(), samples_per_update); - } - - // Write to WAV file - wav_backend.write_audio(chunk_buffer.data(), samples_per_update); - - physical_time += update_dt; - - // Progress indicator every second - if ((int)physical_time % 1 == 0 && - physical_time - update_dt < (int)physical_time) { - printf(" Rendering: %.1fs / %.1fs (music: %.1fs, tempo: %.2fx)\r", - physical_time, max_duration, g_music_time, g_tempo_scale); - fflush(stdout); - } - } - - printf("\nWAV dump complete: %.2fs physical, %.2fs music time\n", - physical_time, g_music_time); - -#if defined(DEMO_HEADLESS) - g_wav_backend_ptr = nullptr; -#endif - audio_shutdown(); - gpu_shutdown(); - platform_shutdown(&platform_state); - return 0; - } -#endif - -#if !defined(DEMO_HEADLESS) - int last_width = platform_state.width; - int last_height = platform_state.height; - - // Get demo duration from sequence file (or -1 if not specified) - const float demo_duration = GetDemoDuration(); - - static double last_frame_time = 0.0; - while (!platform_should_close(&platform_state)) { - platform_poll(&platform_state); - - 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); - } - - // Graphics frame time - derived from platform's clock - const double current_physical_time = platform_state.time + seek_time; - const double graphics_frame_time = current_physical_time - last_frame_time; - // Audio playback time - master clock for audio events - const float current_audio_time = audio_get_playback_time(); - // Delta time for audio processing, based on audio clock - const float audio_dt = current_audio_time - g_last_audio_time; - g_last_audio_time = current_audio_time; - - // Auto-exit when demo finishes (if duration is specified) - if (demo_duration > 0.0f && current_physical_time >= demo_duration) { -#if !defined(STRIP_ALL) - printf("Demo finished at %.2f seconds. Exiting...\n", - current_physical_time); -#endif - break; - } - - // This fill_audio_buffer call is crucial for audio system to process - // events based on the *current audio time* and *graphics physical time* - // context - fill_audio_buffer(audio_dt, current_physical_time); - -#if !defined(STRIP_ALL) - // Hot-reload: Check for file changes - if (hot_reload_enabled && file_watcher.check_changes()) { - printf("\n[Hot-Reload] Config files changed - rebuild required\n"); - printf("[Hot-Reload] Run: cmake --build build -j4 && ./build/demo64k\n"); - file_watcher.reset(); - } -#endif - - // --- Graphics Update --- - const float aspect_ratio = platform_state.aspect_ratio; - - // Peak value derived from audio, but used for visual effect intensity - const float raw_peak = audio_get_realtime_peak(); - const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); - - // Beat calculation: convert audio time to musical beats - const float absolute_beat_time = current_audio_time * g_tracker_score.bpm / 60.0f; - const int beat_number = (int)absolute_beat_time; - const float beat_phase = fmodf(absolute_beat_time, 1.0f); // Fractional part (0.0 to 1.0) - - // Print beat/time info periodically for identifying sync points - // Use graphics time for the print interval to avoid excessive output if - // audio clock is slow - static float last_graphics_print_time = -1.0f; - if (current_physical_time - last_graphics_print_time >= - 0.5f) { // Print every 0.5 seconds - if (tempo_test_enabled) { - printf( - "[GraphicsT=%.2f, AudioT=%.2f, MusicT=%.2f, Beat=%d, Phase=%.2f, " - "Peak=%.2f, Tempo=%.2fx]\n", - current_physical_time, current_audio_time, g_music_time, - beat_number, beat_phase, visual_peak, g_tempo_scale); - } else { - printf("[GraphicsT=%.2f, AudioT=%.2f, Beat=%d, Phase=%.2f, Peak=%.2f]\n", - current_physical_time, current_audio_time, beat_number, beat_phase, - visual_peak); - } - last_graphics_print_time = current_physical_time; - } - - // Draw graphics using physical time and musical beat time - gpu_draw(visual_peak, aspect_ratio, (float)current_physical_time, - absolute_beat_time, beat_phase); - last_frame_time = current_physical_time; - - // Update audio systems (tracker, synth, etc.) based on audio time - // progression - audio_update(); - } - -#if !defined(STRIP_ALL) && defined(DEMO_HEADLESS) - g_wav_backend_ptr = nullptr; -#endif - audio_shutdown(); - gpu_shutdown(); - platform_shutdown(&platform_state); -#endif /* !defined(DEMO_HEADLESS) */ - return 0; -} \ No newline at end of file diff --git a/src/stub_main.cc b/src/stub_main.cc deleted file mode 100644 index 8540fcd..0000000 --- a/src/stub_main.cc +++ /dev/null @@ -1,13 +0,0 @@ -// Stub main for size measurement builds. -// Binary compiles but does NOT run. -// This file is only compiled when STRIP_EXTERNAL_LIBS is defined. - -#if defined(STRIP_EXTERNAL_LIBS) - -int main(int argc, char** argv) { - (void)argc; - (void)argv; - return 0; -} - -#endif // STRIP_EXTERNAL_LIBS diff --git a/src/test_demo.cc b/src/test_demo.cc deleted file mode 100644 index 7f10c3b..0000000 --- a/src/test_demo.cc +++ /dev/null @@ -1,433 +0,0 @@ -// 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 "audio/tracker.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 "util/check_return.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" -#include "gpu/effects/shader_composer.h" -#include "gpu/effects/cnn_effect.h" -#include "gpu/effects/cnn_v2_effect.h" - -class PeakMeterEffect : public PostProcessEffect { - public: - PeakMeterEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { - // Use ShaderComposer to include CommonUniforms from common_uniforms.wgsl - const char* shader_main = R"( - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, - }; - - struct EffectParams { - unused: f32, - }; - - @group(0) @binding(0) var inputSampler: sampler; - @group(0) @binding(1) var inputTexture: texture_2d; - @group(0) @binding(2) var uniforms: CommonUniforms; - @group(0) @binding(3) var params: EffectParams; - - @vertex - fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var output: VertexOutput; - // Full-screen triangle (required for post-process pass-through) - 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 { - // Bar dimensions - let bar_y_min = 0.005; - let bar_y_max = 0.015; - let bar_x_min = 0.015; - let bar_x_max = 0.250; - 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_min && input.uv.x <= bar_x_max; - - // Optimization: Return bar color early (avoids texture sampling for ~5% of pixels) - if (in_bar_y && in_bar_x) { - let uv_x = (input.uv.x - bar_x_min) / (bar_x_max - bar_x_min); - let factor = step(uv_x, uniforms.audio_intensity); - return mix(vec4(0.0, 0.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0,1.0), factor); - } - - // Pass through input texture for rest of screen - return textureSample(inputTexture, inputSampler, input.uv); - } - )"; - - // Compose shader with common_uniforms to get CommonUniforms definition - std::string shader_code = ShaderComposer::Get().Compose( - {"common_uniforms"}, shader_main); - - pipeline_ = - create_post_process_pipeline(ctx_.device, ctx_.format, shader_code.c_str()); - } - - void update_bind_group(WGPUTextureView input_view) override { - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, - uniforms_.get(), {}); - } - - void render(WGPURenderPassEncoder pass, - const CommonPostProcessUniforms& uniforms) override { - uniforms_.update(ctx_.queue, uniforms); - PostProcessEffect::render(pass, uniforms); - } -}; - -static int g_cnn_version = 2; // Default to v2 - -#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(" --cnn-version <1|2> Select CNN version (1=v1, 2=v2, default=2)\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 --cnn-version 1\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; - // Seek time needs to be declared here to be accessible globally for the loop. - float seek_time = 0.0f; - -#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) { - CHECK_RETURN_BEGIN(i + 1 >= argc, 1) - print_usage(argv[0]); - ERROR_MSG("--log-peaks requires a filename argument\n"); - return 1; - CHECK_RETURN_END - log_peaks_file = argv[++i]; - } else if (strcmp(argv[i], "--log-peaks-fine") == 0) { - log_peaks_fine = true; - } else if (strcmp(argv[i], "--cnn-version") == 0) { - if (i + 1 < argc) { - int version = atoi(argv[++i]); - if (version == 1 || version == 2) { - g_cnn_version = version; - } else { - fprintf(stderr, "Error: --cnn-version must be 1 or 2\n\n"); - print_usage(argv[0]); - return 1; - } - } else { - fprintf(stderr, "Error: --cnn-version requires argument\n\n"); - print_usage(argv[0]); - return 1; - } - } else { - CHECK_RETURN_BEGIN(true, 1) - print_usage(argv[0]); - ERROR_MSG("Unknown option '%s'\n", argv[i]); - return 1; - CHECK_RETURN_END - } - } -#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); - - // Load timeline from test_demo.seq - LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); - -#if !defined(STRIP_ALL) - const GpuContext* gpu_ctx = gpu_get_context(); - - // Add CNN post-processing effect based on version flag - if (g_cnn_version == 1) { - CNNEffectParams params; - params.blend_amount = 1.0f; - auto* cnn = new CNNEffect(*gpu_ctx, params); - cnn->set_beat_modulation(true, 1.0f); - gpu_add_custom_effect(cnn, 0.0f, 99999.0f, 10); - } else if (g_cnn_version == 2) { - CNNv2EffectParams params; - params.blend_amount = 1.0f; - auto* cnn = new CNNv2Effect(*gpu_ctx, params); - cnn->set_beat_modulation(true, 1.0f); - gpu_add_custom_effect(cnn, 0.0f, 99999.0f, 10); - } - - // Add peak meter visualization effect (renders as final post-process) - 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 float g_last_audio_time = 0.0f; - static float g_tempo_scale = 1.0f; - - auto fill_audio_buffer = [&](float audio_dt, double physical_time) { - // Calculate tempo scale if --tempo flag enabled - if (tempo_test_enabled) { - const float t = (float)physical_time; - if (t >= 2.0f && t < 4.0f) { - // [2s->4s]: Accelerate from 1.0x to 1.5x - const float progress = (t - 2.0f) / 2.0f; - g_tempo_scale = 1.0f + (0.5f * progress); - } else if (t >= 6.0f && t < 8.0f) { - // [6s->8s]: Decelerate from 1.0x to 0.66x - const float progress = (t - 6.0f) / 2.0f; - g_tempo_scale = 1.0f - (0.34f * progress); - } else { - // All other times: Normal tempo - g_tempo_scale = 1.0f; - } - } else { - g_tempo_scale = 1.0f; // No tempo variation - } - - g_music_time += audio_dt * g_tempo_scale; - - g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale); - audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); - }; - - // Pre-fill using same pattern as main loop (100ms) - fill_audio_buffer(0.1f, 0.0); - - audio_start(); - g_last_audio_time = audio_get_playback_time(); - - 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); - } - - // Graphics frame time - derived from platform's clock - const double current_physical_time = platform_state.time + seek_time; - // Audio playback time - master clock for audio events - const float current_audio_time = audio_get_playback_time(); - // Delta time for audio processing, based on audio clock - const float audio_dt = current_audio_time - g_last_audio_time; - g_last_audio_time = current_audio_time; - - // Auto-exit at end (based on physical time for graphics loop consistency) - if (demo_duration > 0.0f && current_physical_time >= demo_duration) { -#if !defined(STRIP_ALL) - printf("test_demo finished at %.2f seconds. Exiting...\n", - current_physical_time); -#endif - break; - } - - // This fill_audio_buffer call is crucial for audio system to process - // events based on the *current audio time* and *graphics physical time* - // context - fill_audio_buffer(audio_dt, current_physical_time); - - // --- Graphics Update --- - const float aspect_ratio = platform_state.aspect_ratio; - - // Peak value derived from audio, but used for visual effect intensity - const float raw_peak = audio_get_realtime_peak(); - const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); - - // Beat calculation: convert audio time to musical beats - const float absolute_beat_time = current_audio_time * g_tracker_score.bpm / 60.0f; - const int beat_number = (int)absolute_beat_time; - const float beat_phase = fmodf(absolute_beat_time, 1.0f); // Fractional part (0.0 to 1.0) - -#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 - // Use platform_get_time() for high-resolution timestamps (not - // audio_time which advances in chunks) - const double frame_time = platform_get_time(); - fprintf(peak_log, "%d %.6f %.6f %d\n", frame_number, frame_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, current_audio_time, - raw_peak); - last_beat_logged = beat_number; - } - } - frame_number++; - - // Debug output every 0.5 seconds (based on graphics time for consistency) - static float last_graphics_print_time = -1.0f; - if (current_physical_time - last_graphics_print_time >= 0.5f) { - if (tempo_test_enabled) { - printf( - "[GraphicsT=%.2f, AudioT=%.2f, MusicT=%.2f, Beat=%d, Phase=%.2f, " - "Peak=%.2f, Tempo=%.2fx]\n", - current_physical_time, current_audio_time, g_music_time, - beat_number, beat_phase, visual_peak, g_tempo_scale); - } else { - printf("[GraphicsT=%.2f, AudioT=%.2f, Beat=%d, Phase=%.2f, Peak=%.2f]\n", - current_physical_time, current_audio_time, beat_number, beat_phase, - visual_peak); - } - last_graphics_print_time = current_physical_time; - } -#endif - - // Draw graphics using physical time and musical beat time - const float graphics_frame_time = (float)current_physical_time; - gpu_draw(visual_peak, aspect_ratio, graphics_frame_time, - absolute_beat_time, beat_phase); - - // Update audio systems (tracker, synth, etc.) based on audio time - // progression - 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; -} -- cgit v1.2.3