diff options
Diffstat (limited to 'src/app/main.cc')
| -rw-r--r-- | src/app/main.cc | 394 |
1 files changed, 394 insertions, 0 deletions
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 <vector> +#if defined(DEMO_HEADLESS) +#include <csignal> +#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 <cmath> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +#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<float> 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 |
