diff options
| -rw-r--r-- | CMakeLists.txt | 60 | ||||
| -rw-r--r-- | assets/test_demo.seq | 7 | ||||
| -rw-r--r-- | assets/test_demo.track | 32 | ||||
| -rw-r--r-- | src/generated/test_demo_music.cc | 77 | ||||
| -rw-r--r-- | src/generated/test_demo_timeline.cc | 15 | ||||
| -rw-r--r-- | src/test_demo.cc | 216 | ||||
| -rw-r--r-- | test_demo_README.md | 223 |
7 files changed, 630 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bf4d4a..4fc00b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -307,6 +307,66 @@ if (DEMO_SIZE_OPT) endif() endif() +# ============================================================================== +# Test Demo (Audio/Visual Sync Tool) +# ============================================================================== + +# Timeline generation +set(TEST_DEMO_SEQ_PATH ${CMAKE_CURRENT_SOURCE_DIR}/assets/test_demo.seq) +set(GENERATED_TEST_DEMO_TIMELINE_CC ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/test_demo_timeline.cc) +add_custom_command( + OUTPUT ${GENERATED_TEST_DEMO_TIMELINE_CC} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/src/generated + COMMAND ${SEQ_COMPILER_CMD} ${TEST_DEMO_SEQ_PATH} ${GENERATED_TEST_DEMO_TIMELINE_CC} + DEPENDS ${SEQ_COMPILER_DEPENDS} ${TEST_DEMO_SEQ_PATH} src/gpu/demo_effects.h + COMMENT "Compiling test_demo sequence..." +) +add_custom_target(generate_test_demo_timeline ALL DEPENDS ${GENERATED_TEST_DEMO_TIMELINE_CC}) + +# Music generation +set(TEST_DEMO_TRACK_PATH ${CMAKE_CURRENT_SOURCE_DIR}/assets/test_demo.track) +set(GENERATED_TEST_DEMO_MUSIC_CC ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/test_demo_music.cc) +add_custom_command( + OUTPUT ${GENERATED_TEST_DEMO_MUSIC_CC} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/src/generated + COMMAND ${TRACKER_COMPILER_FINAL_CMD} ${TEST_DEMO_TRACK_PATH} ${GENERATED_TEST_DEMO_MUSIC_CC} + DEPENDS ${TRACKER_COMPILER_FINAL_DEPENDS} ${TEST_DEMO_TRACK_PATH} tracker_compiler_host + COMMENT "Compiling test_demo music..." +) +add_custom_target(generate_test_demo_music ALL DEPENDS ${GENERATED_TEST_DEMO_MUSIC_CC}) + +# Build executable (uses main demo assets) +add_demo_executable( + test_demo + src/test_demo.cc + ${PLATFORM_SOURCES} + ${GEN_DEMO_CC} + ${GENERATED_TEST_DEMO_TIMELINE_CC} + ${GENERATED_TEST_DEMO_MUSIC_CC} +) + +add_dependencies(test_demo generate_demo_assets generate_test_demo_timeline generate_test_demo_music) + +if (APPLE) + target_link_libraries(test_demo PRIVATE 3d gpu audio procedural util ${DEMO_LIBS}) +else() + target_link_libraries(test_demo PRIVATE -Wl,--start-group 3d gpu audio procedural util -Wl,--end-group ${DEMO_LIBS}) +endif() + +# Size optimizations +if (DEMO_SIZE_OPT) + if (MSVC) + target_compile_options(test_demo PRIVATE /Os /GS-) + target_link_options(test_demo PRIVATE /OPT:REF /OPT:ICF /INCREMENTAL:NO) + elseif (APPLE) + target_compile_options(test_demo PRIVATE -Os) + target_link_options(test_demo PRIVATE -Wl,-dead_strip) + else () + target_compile_options(test_demo PRIVATE -Os -ffunction-sections -fdata-sections) + target_link_options(test_demo PRIVATE -Wl,--gc-sections -s) + endif() +endif() + #-- - Tests -- - enable_testing() if(DEMO_BUILD_TESTS) diff --git a/assets/test_demo.seq b/assets/test_demo.seq new file mode 100644 index 0000000..6dc26ca --- /dev/null +++ b/assets/test_demo.seq @@ -0,0 +1,7 @@ +# Minimal timeline for audio/visual sync testing +# BPM 120 (set in test_demo.track) + +SEQUENCE 0.0 0 "Main Loop" + EFFECT + FlashEffect 0.0 16.0 + +END_DEMO 32b diff --git a/assets/test_demo.track b/assets/test_demo.track new file mode 100644 index 0000000..8c06100 --- /dev/null +++ b/assets/test_demo.track @@ -0,0 +1,32 @@ +# Minimal drum beat for audio/visual sync testing +# Pattern: kick-snare-kick-snare, crash every 4th bar +# Includes NOTE_A4 (440 Hz) at start of each bar for testing + +SAMPLE ASSET_KICK_1 +SAMPLE ASSET_SNARE_1 +SAMPLE ASSET_CRASH_1 + +PATTERN drums_basic + 0.0, ASSET_KICK_1, 1.0, 0.0 + 0.0, NOTE_A4, 0.5, 0.0 + 1.0, ASSET_SNARE_1, 0.9, 0.0 + 2.0, ASSET_KICK_1, 1.0, 0.0 + 3.0, ASSET_SNARE_1, 0.9, 0.0 + +PATTERN drums_with_crash + 0.0, ASSET_KICK_1, 1.0, 0.0 + 0.0, ASSET_CRASH_1, 0.85, 0.0 + 0.0, NOTE_A4, 0.5, 0.0 + 1.0, ASSET_SNARE_1, 0.9, 0.0 + 2.0, ASSET_KICK_1, 1.0, 0.0 + 3.0, ASSET_SNARE_1, 0.9, 0.0 + +SCORE + 0.0, drums_basic + 2.0, drums_basic + 4.0, drums_with_crash + 6.0, drums_basic + 8.0, drums_basic + 10.0, drums_basic + 12.0, drums_with_crash + 14.0, drums_basic diff --git a/src/generated/test_demo_music.cc b/src/generated/test_demo_music.cc new file mode 100644 index 0000000..3fdd2a1 --- /dev/null +++ b/src/generated/test_demo_music.cc @@ -0,0 +1,77 @@ +// Generated by tracker_compiler. Do not edit. + +#include "audio/tracker.h" + +#include "generated/assets.h" + +const NoteParams g_tracker_samples[] = { + { 0 }, // ASSET_KICK_1 (ASSET) + { 0 }, // ASSET_SNARE_1 (ASSET) + { 0 }, // ASSET_CRASH_1 (ASSET) + { 440.0f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_A4 +}; +const uint32_t g_tracker_samples_count = 4; + +const AssetId g_tracker_sample_assets[] = { + AssetId::ASSET_KICK_1, + AssetId::ASSET_SNARE_1, + AssetId::ASSET_CRASH_1, + AssetId::ASSET_LAST_ID, +}; + +static const TrackerEvent PATTERN_EVENTS_drums_basic[] = { + { 0.0f, 0, 1.0f, 0.0f }, + { 0.0f, 3, 0.5f, 0.0f }, + { 1.0f, 1, 0.9f, 0.0f }, + { 2.0f, 0, 1.0f, 0.0f }, + { 3.0f, 1, 0.9f, 0.0f }, +}; +static const TrackerEvent PATTERN_EVENTS_drums_with_crash[] = { + { 0.0f, 0, 1.0f, 0.0f }, + { 0.0f, 2, 0.9f, 0.0f }, + { 0.0f, 3, 0.5f, 0.0f }, + { 1.0f, 1, 0.9f, 0.0f }, + { 2.0f, 0, 1.0f, 0.0f }, + { 3.0f, 1, 0.9f, 0.0f }, +}; + +const TrackerPattern g_tracker_patterns[] = { + { PATTERN_EVENTS_drums_basic, 5, 4.0f }, // drums_basic + { PATTERN_EVENTS_drums_with_crash, 6, 4.0f }, // drums_with_crash +}; +const uint32_t g_tracker_patterns_count = 2; + +static const TrackerPatternTrigger SCORE_TRIGGERS[] = { + { 0.0f, 0 }, + { 2.0f, 0 }, + { 4.0f, 1 }, + { 6.0f, 0 }, + { 8.0f, 0 }, + { 10.0f, 0 }, + { 12.0f, 1 }, + { 14.0f, 0 }, +}; + +const TrackerScore g_tracker_score = { + SCORE_TRIGGERS, 8, 120.0f +}; + +// ============================================================ +// RESOURCE USAGE ANALYSIS (for synth.h configuration) +// ============================================================ +// Total samples: 4 (3 assets + 1 generated notes) +// Max simultaneous pattern triggers: 1 +// Estimated max polyphony: 5 voices +// +// REQUIRED (minimum to avoid pool exhaustion): +// MAX_VOICES: 5 +// MAX_SPECTROGRAMS: 8 (no caching) +// +// RECOMMENDED (with 50% safety margin): +// MAX_VOICES: 10 +// MAX_SPECTROGRAMS: 12 (no caching) +// +// NOTE: With spectrogram caching by note parameters, +// MAX_SPECTROGRAMS could be reduced to ~4 +// ============================================================ + diff --git a/src/generated/test_demo_timeline.cc b/src/generated/test_demo_timeline.cc new file mode 100644 index 0000000..75c5fe6 --- /dev/null +++ b/src/generated/test_demo_timeline.cc @@ -0,0 +1,15 @@ +// Auto-generated by seq_compiler. Do not edit. +#include "gpu/demo_effects.h" +#include "gpu/effect.h" + +float GetDemoDuration() { + return 16.000000f; +} + +void LoadTimeline(MainSequence& main_seq, WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format) { + { + auto seq = std::make_shared<Sequence>(); + seq->add_effect(std::make_shared<FlashEffect>(device, queue, format), 0.0f, 16.0f, 0); + main_seq.add_sequence(seq, 0.0f, 0); + } +} diff --git a/src/test_demo.cc b/src/test_demo.cc new file mode 100644 index 0000000..1664f7c --- /dev/null +++ b/src/test_demo.cc @@ -0,0 +1,216 @@ +// Minimal audio/visual synchronization test tool +// Plays simple drum beat with synchronized screen flashes + +#include "audio/audio.h" +#include "audio/audio_engine.h" +#include "audio/synth.h" +#include "generated/assets.h" // Note: uses main demo asset bundle +#include "gpu/demo_effects.h" +#include "gpu/gpu.h" +#include "platform.h" +#include <cmath> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +// External declarations from generated files +extern float GetDemoDuration(); +extern void LoadTimeline(MainSequence& main_seq, WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format); + +#if !defined(STRIP_ALL) +static void print_usage(const char* prog_name) { + printf("Usage: %s [OPTIONS]\n", prog_name); + printf("\nMinimal audio/visual synchronization test tool.\n"); + printf("Plays a simple drum beat with synchronized screen flashes.\n"); + printf("\nOptions:\n"); + printf(" --help Show this help message and exit\n"); + printf(" --fullscreen Run in fullscreen mode\n"); + printf(" --resolution WxH Set window resolution (e.g., 1024x768)\n"); + printf(" --tempo Enable tempo variation test mode\n"); + printf(" (alternates between acceleration and deceleration)\n"); + printf(" --log-peaks FILE Log audio peaks to FILE for gnuplot visualization\n"); + printf("\nExamples:\n"); + printf(" %s --fullscreen\n", prog_name); + printf(" %s --resolution 1024x768 --tempo\n", prog_name); + printf(" %s --log-peaks peaks.txt\n", prog_name); + printf("\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; + +#if !defined(STRIP_ALL) + 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 && 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], "--log-peaks") == 0 && i + 1 < argc) { + log_peaks_file = argv[++i]; + } + } +#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); + audio_init(); + + static AudioEngine g_audio_engine; + g_audio_engine.init(); + + // Music time tracking with optional tempo variation + static float g_music_time = 0.0f; + static double g_last_physical_time = 0.0; + static float g_tempo_scale = 1.0f; + + auto fill_audio_buffer = [&](double t) { + const float dt = (float)(t - g_last_physical_time); + g_last_physical_time = t; + + // Calculate tempo scale if --tempo flag enabled + if (tempo_test_enabled) { + // Each bar = 2 seconds at 120 BPM (4 beats) + const float bar_duration = 2.0f; + const int bar_number = (int)(t / bar_duration); + const float bar_progress = fmodf((float)t, bar_duration) / bar_duration; // 0.0-1.0 within bar + + if (bar_number % 2 == 0) { + // Even bars: Ramp from 1.0x → 1.5x + g_tempo_scale = 1.0f + (0.5f * bar_progress); + } else { + // Odd bars: Ramp from 1.0x → 0.66x + g_tempo_scale = 1.0f - (0.34f * bar_progress); + } + } else { + g_tempo_scale = 1.0f; // No tempo variation + } + + g_music_time += dt * g_tempo_scale; + + g_audio_engine.update(g_music_time); + audio_render_ahead(g_music_time, dt * g_tempo_scale); + }; + + // Pre-fill audio buffer + g_audio_engine.update(g_music_time); + audio_render_ahead(g_music_time, 1.0f / 60.0f); + audio_start(); + + int last_width = platform_state.width; + int last_height = platform_state.height; + const float demo_duration = GetDemoDuration(); + +#if !defined(STRIP_ALL) + // Open peak log file if requested + FILE* peak_log = nullptr; + if (log_peaks_file) { + peak_log = fopen(log_peaks_file, "w"); + if (peak_log) { + fprintf(peak_log, "# Audio peak log from test_demo\n"); + fprintf(peak_log, "# To plot with gnuplot:\n"); + fprintf(peak_log, "# gnuplot -p -e \"set xlabel 'Time (s)'; set ylabel 'Peak'; plot '%s' using 1:3 with lines title 'Raw Peak'\"\n", log_peaks_file); + 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; +#endif + + // Main loop + while (!platform_should_close(&platform_state)) { + platform_poll(&platform_state); + + // Handle resize + if (platform_state.width != last_width || + platform_state.height != last_height) { + last_width = platform_state.width; + last_height = platform_state.height; + gpu_resize(last_width, last_height); + } + + const double current_time = platform_state.time; + + // Auto-exit at end + if (demo_duration > 0.0f && current_time >= demo_duration) { +#if !defined(STRIP_ALL) + printf("test_demo finished at %.2f seconds.\n", current_time); +#endif + break; + } + + fill_audio_buffer(current_time); + + // Audio/visual sync parameters + const float aspect_ratio = platform_state.aspect_ratio; + const float raw_peak = synth_get_output_peak(); + const float visual_peak = fminf(raw_peak * 8.0f, 1.0f); + + // Beat calculation (hardcoded BPM=120) + const float beat_time = (float)current_time * 120.0f / 60.0f; + const int beat_number = (int)beat_time; + const float beat = fmodf(beat_time, 1.0f); + +#if !defined(STRIP_ALL) + // Log peak at each beat boundary + if (peak_log && beat_number != last_beat_logged) { + fprintf(peak_log, "%d %.6f %.6f\n", beat_number, current_time, raw_peak); + last_beat_logged = beat_number; + } + + // Debug output every 0.5 seconds + static float last_print_time = -1.0f; + if (current_time - last_print_time >= 0.5f) { + if (tempo_test_enabled) { + printf("[T=%.2f, MusicT=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f, Tempo=%.2fx]\n", + (float)current_time, g_music_time, beat_number, beat, visual_peak, g_tempo_scale); + } else { + printf("[T=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f]\n", + (float)current_time, beat_number, beat, visual_peak); + } + last_print_time = (float)current_time; + } +#endif + + gpu_draw(visual_peak, aspect_ratio, (float)current_time, beat); + audio_update(); + } + + // Shutdown +#if !defined(STRIP_ALL) + if (peak_log) { + fclose(peak_log); + printf("Peak log written to '%s'\n", log_peaks_file); + } +#endif + + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); + return 0; +} diff --git a/test_demo_README.md b/test_demo_README.md new file mode 100644 index 0000000..f84f972 --- /dev/null +++ b/test_demo_README.md @@ -0,0 +1,223 @@ +# test_demo - Audio/Visual Synchronization Debug Tool + +## Overview + +A minimal standalone tool for debugging audio/visual synchronization in the demo without the complexity of the full demo64k timeline. + +## Features + +- **Simple Drum Beat**: Kick-snare pattern with crash every 4th bar +- **NOTE_A4 Reference Tone**: 440 Hz tone plays at start of each bar for testing +- **Screen Flash Effect**: Visual feedback synchronized to audio peaks +- **16 Second Duration**: 8 bars at 120 BPM +- **Variable Tempo Mode**: Tests tempo scaling (1.0x ↔ 1.5x and 1.0x ↔ 0.66x) +- **Peak Logging**: Export audio peaks to file for gnuplot visualization + +## Building + +```bash +cmake --build build --target test_demo +``` + +## Usage + +### Basic Usage + +```bash +build/test_demo +``` + +### Command-Line Options + +``` + --help Show help message and exit + --fullscreen Run in fullscreen mode + --resolution WxH Set window resolution (e.g., 1024x768) + --tempo Enable tempo variation test mode + --log-peaks FILE Log audio peaks to FILE for gnuplot visualization +``` + +### Examples + +#### Run in fullscreen +```bash +build/test_demo --fullscreen +``` + +#### Custom resolution with tempo testing +```bash +build/test_demo --resolution 1024x768 --tempo +``` + +#### Log audio peaks for analysis +```bash +build/test_demo --log-peaks peaks.txt +``` + +After running, visualize with gnuplot: +```bash +gnuplot -p -e "set xlabel 'Time (s)'; set ylabel 'Peak'; plot 'peaks.txt' using 1:3 with lines title 'Raw Peak'" +``` + +## Keyboard Controls + +- **ESC**: Exit the demo +- **F**: Toggle fullscreen + +## Audio Pattern + +The demo plays a repeating drum pattern: + +``` +Bar 1-2: Kick-Snare-Kick-Snare (with A4 note) +Bar 3: Kick+Crash-Snare-Kick-Snare (with A4 note) ← Crash landmark +Bar 4-6: Kick-Snare-Kick-Snare (with A4 note) +Bar 7: Kick+Crash-Snare-Kick-Snare (with A4 note) ← Crash landmark +Bar 8: Kick-Snare-Kick-Snare (with A4 note) +``` + +**Timing:** +- 120 BPM (2 beats per second) +- 1 bar = 4 beats = 2 seconds +- Total duration: 8 bars = 16 seconds + +**Crash Landmarks:** +- First crash at T=4.0s (bar 3, beat 8) +- Second crash at T=12.0s (bar 7, beat 24) + +## Tempo Test Mode (`--tempo`) + +When enabled with `--tempo` flag, the demo alternates tempo scaling every bar: + +- **Even bars (0, 2, 4, 6)**: Accelerate from 1.0x → 1.5x +- **Odd bars (1, 3, 5, 7)**: Decelerate from 1.0x → 0.66x + +This tests the variable tempo system where music time advances independently of physical time: +- `music_time += dt * tempo_scale` +- Pattern triggering respects tempo scaling +- Audio samples play at normal pitch (no pitch shifting) + +**Console Output with --tempo:** +``` +[T=0.50, MusicT=0.56, Beat=1, Frac=0.12, Peak=0.72, Tempo=1.25x] +``` + +**Expected Behavior:** +- Physical time always advances at 1:1 (16 seconds = 16 seconds) +- Music time advances faster during acceleration, slower during deceleration +- By end of demo: Music time > Physical time (due to net acceleration) + +## Peak Logging Format + +The `--log-peaks` option writes a text file with three columns: + +``` +# Audio peak log from test_demo +# To plot with gnuplot: +# gnuplot -p -e "set xlabel 'Time (s)'; set ylabel 'Peak'; plot 'peaks.txt' using 1:3 with lines title 'Raw Peak'" +# Columns: beat_number clock_time raw_peak +# +0 0.000000 0.850000 +1 0.500000 0.720000 +2 1.000000 0.800000 +... +``` + +**Columns:** +1. **beat_number**: Beat index (0, 1, 2, ...) +2. **clock_time**: Physical time in seconds +3. **raw_peak**: Audio peak value (0.0-1.0+) + +**Use Cases:** +- Verify audio/visual synchronization timing +- Detect clipping (peak > 1.0) +- Analyze tempo scaling effects +- Compare expected vs actual beat times + +## Files + +- **`src/test_demo.cc`**: Main executable (~220 lines) +- **`assets/test_demo.track`**: Drum pattern and NOTE_A4 definition +- **`assets/test_demo.seq`**: Visual timeline (FlashEffect) +- **`src/generated/test_demo_timeline.cc`**: Generated timeline (auto) +- **`src/generated/test_demo_music.cc`**: Generated music data (auto) + +## Verification Checklist + +### Normal Mode +- [ ] Visual flashes occur every ~0.5 seconds +- [ ] Audio plays (kick-snare drum pattern audible) +- [ ] A4 tone (440 Hz) audible at start of each bar +- [ ] Synchronization: Flash happens simultaneously with kick hit +- [ ] Crash landmark: Larger flash + cymbal crash at T=4.0s and T=12.0s +- [ ] Auto-exit: Demo stops cleanly at 16 seconds +- [ ] Console timing: Peak values spike when kicks hit (Peak > 0.7) +- [ ] No audio glitches: Smooth playback, no stuttering + +### Tempo Test Mode +- [ ] Bar 0 accelerates (1.0x → 1.5x) +- [ ] Bar 1 decelerates (1.0x → 0.66x) +- [ ] Music time drifts from physical time +- [ ] Audio still syncs with flashes +- [ ] Smooth transitions at bar boundaries +- [ ] Physical time = 16s at end +- [ ] Music time > 16s at end +- [ ] Console shows tempo value + +### Peak Logging +- [ ] File created when `--log-peaks` specified +- [ ] Contains beat_number, clock_time, raw_peak columns +- [ ] Gnuplot command in header comment +- [ ] One row per beat (32 rows for 16 seconds) +- [ ] Peak values match console output +- [ ] Gnuplot visualization works + +## Troubleshooting + +**Flash appears before audio:** +- Cause: Audio latency too high +- Fix: Reduce ring buffer size in `ring_buffer.h` + +**Flash appears after audio:** +- Cause: Ring buffer under-filled +- Fix: Increase pre-fill duration in `audio_render_ahead()` + +**No flash at all:** +- Cause: Peak threshold not reached +- Check: Console shows Peak > 0.7 +- Fix: Increase `visual_peak` multiplier in code (currently 8.0×) + +**A4 note not audible:** +- Cause: NOTE_A4 volume too low or procedural generation issue +- Check: Console shows correct sample count (4 samples) +- Fix: Increase volume in test_demo.track (currently 0.5) + +**Peak log file empty:** +- Cause: Demo exited before first beat +- Check: File has header comments +- Fix: Ensure demo runs for at least 0.5 seconds + +## Design Rationale + +**Why separate from demo64k?** +- Isolated testing environment +- No timeline complexity +- Faster iteration cycles +- Independent verification + +**Why use main demo assets?** +- Avoid asset system conflicts (AssetId enum collision) +- Reuse existing samples +- No size overhead +- Simpler CMake integration + +**Why 16 seconds?** +- Long enough for verification (8 bars) +- Short enough for quick tests +- Crash landmarks at 25% and 75% for easy reference + +**Why NOTE_A4?** +- Standard reference tone (440 Hz) +- Easily identifiable pitch +- Tests procedural note generation +- Minimal code impact |
