summaryrefslogtreecommitdiff
path: root/src/app/main.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/main.cc')
-rw-r--r--src/app/main.cc394
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