summaryrefslogtreecommitdiff
path: root/src/app/test_demo.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/test_demo.cc')
-rw-r--r--src/app/test_demo.cc433
1 files changed, 433 insertions, 0 deletions
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 <cmath>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+// 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<f32>,
+ @location(0) uv: vec2<f32>,
+ };
+
+ struct EffectParams {
+ unused: f32,
+ };
+
+ @group(0) @binding(0) var inputSampler: sampler;
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
+ @group(0) @binding(2) var<uniform> uniforms: CommonUniforms;
+ @group(0) @binding(3) var<uniform> 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<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(3.0, -1.0),
+ vec2<f32>(-1.0, 3.0)
+ );
+ output.position = vec4<f32>(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<f32> {
+ // 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<f32>(0.0, 0.0, 0.0, 1.0), vec4<f32>(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;
+}