diff options
| -rw-r--r-- | CMakeLists.txt | 18 | ||||
| -rw-r--r-- | HANDOFF_2026-02-07_AssetRegeneration.md | 112 | ||||
| -rw-r--r-- | HANDOFF_2026-02-07_Final.md | 55 | ||||
| -rw-r--r-- | assets/music.track | 392 | ||||
| -rw-r--r-- | assets/test_demo.track | 44 | ||||
| -rw-r--r-- | assets/test_gantt.seq | 11 | ||||
| -rw-r--r-- | convert_track.py | 77 | ||||
| -rw-r--r-- | doc/TRACKER.md | 81 | ||||
| -rwxr-xr-x | scripts/test_gantt_html.sh | 102 | ||||
| -rwxr-xr-x | scripts/test_gantt_output.sh | 70 | ||||
| -rw-r--r-- | src/audio/tracker.cc | 23 | ||||
| -rw-r--r-- | src/audio/tracker.h | 8 | ||||
| -rw-r--r-- | src/generated/music_data.cc | 383 | ||||
| -rw-r--r-- | src/generated/test_demo_music.cc | 40 | ||||
| -rw-r--r-- | src/gpu/effects/flash_effect.cc | 10 | ||||
| -rw-r--r-- | src/gpu/effects/post_process_helper.cc | 15 | ||||
| -rw-r--r-- | src/gpu/effects/post_process_helper.h | 6 | ||||
| -rw-r--r-- | src/main.cc | 1 | ||||
| -rw-r--r-- | src/test_demo.cc | 42 | ||||
| -rw-r--r-- | tools/track_visualizer/README.md | 82 | ||||
| -rw-r--r-- | tools/track_visualizer/index.html | 537 | ||||
| -rw-r--r-- | tools/tracker_compiler.cc | 25 |
22 files changed, 1650 insertions, 484 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 53d0285..f2ab936 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -577,6 +577,24 @@ if(DEMO_BUILD_TESTS) ${GEN_DEMO_CC}) target_link_libraries(test_texture_manager PRIVATE 3d gpu audio procedural util ${DEMO_LIBS}) add_dependencies(test_texture_manager generate_demo_assets) + + # Gantt chart output test (bash script) + add_test( + NAME GanttOutputTest + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_gantt_output.sh + $<TARGET_FILE:seq_compiler> + ${CMAKE_CURRENT_SOURCE_DIR}/assets/test_gantt.seq + ${CMAKE_CURRENT_BINARY_DIR}/test_gantt_output.txt + ) + + # HTML Gantt chart output test + add_test( + NAME GanttHtmlOutputTest + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_gantt_html.sh + $<TARGET_FILE:seq_compiler> + ${CMAKE_CURRENT_SOURCE_DIR}/assets/test_gantt.seq + ${CMAKE_CURRENT_BINARY_DIR}/test_gantt_output.html + ) endif() #-- - Extra Tools -- - diff --git a/HANDOFF_2026-02-07_AssetRegeneration.md b/HANDOFF_2026-02-07_AssetRegeneration.md new file mode 100644 index 0000000..dd41b31 --- /dev/null +++ b/HANDOFF_2026-02-07_AssetRegeneration.md @@ -0,0 +1,112 @@ +# Handoff: Asset Regeneration Fix (February 7, 2026) + +## Session Summary +Fixed build system issue where generated files weren't automatically regenerated after `make clean`, causing "fatal error: 'generated/assets.h' file not found" errors. + +## Problem +After running `make clean` in the build directory, the build would fail because: +1. Generated asset files (`generated/assets.h`, etc.) were removed +2. CMake didn't know to regenerate them before compiling libraries +3. Libraries (`audio`, `3d`, `gpu`) tried to compile before assets existed + +## Solution + +### 1. Mark Generated Files with GENERATED Property +Added `set_source_files_properties()` calls to tell CMake these files are generated and should be checked for rebuild: + +```cmake +# Mark generated files so CMake always checks if they need rebuilding +set_source_files_properties(${GEN_DEMO_H} ${GEN_DEMO_CC} PROPERTIES GENERATED TRUE) +set_source_files_properties(${GEN_TEST_H} ${GEN_TEST_CC} PROPERTIES GENERATED TRUE) +set_source_files_properties(${GENERATED_TIMELINE_CC} PROPERTIES GENERATED TRUE) +set_source_files_properties(${GENERATED_MUSIC_DATA_CC} PROPERTIES GENERATED TRUE) +set_source_files_properties(${GENERATED_TEST_DEMO_TIMELINE_CC} PROPERTIES GENERATED TRUE) +set_source_files_properties(${GENERATED_TEST_DEMO_MUSIC_CC} PROPERTIES GENERATED TRUE) +``` + +### 2. Add Explicit Library Dependencies +Added `add_dependencies()` to ensure libraries wait for asset generation: + +```cmake +# Libraries must wait for asset generation (they include generated/assets.h) +add_dependencies(audio generate_demo_assets) +add_dependencies(3d generate_demo_assets) +add_dependencies(gpu generate_demo_assets) +``` + +### 3. Update seq_compiler for GpuContext +Updated code generator to match the GpuContext refactor from earlier session: + +**Before:** +```cpp +"void LoadTimeline(MainSequence& main_seq, WGPUDevice device, " + "WGPUQueue queue, WGPUTextureFormat format) {\n"; + +">(device, queue, format" << eff.extra_args << "), " +``` + +**After:** +```cpp +"void LoadTimeline(MainSequence& main_seq, const GpuContext& ctx) {\n"; + +">(ctx" << eff.extra_args << "), " +``` + +### 4. Cleanup Stale Files +Removed old generated test assets that were incorrectly placed in `src/generated/`: +- `test_assets.h` / `test_assets_data.cc` (now in `build/src/generated_test/`) +- `test_demo_assets.h` / `test_demo_assets_data.cc` (now in `build/src/generated_test/`) + +## Commit Made + +**Commit:** fb2aa8b +**Title:** `fix: Auto-regenerate assets after clean build` + +**Changes:** +- CMakeLists.txt: +15 lines (GENERATED properties + dependencies) +- tools/seq_compiler.cc: Updated signatures +- Removed 4 stale generated files (~41K lines) + +## Verification + +**Clean Build Test:** +```bash +cd build +make clean +rm -f ../src/generated/*.{h,cc} +cmake --build . --target demo64k +``` + +**Result:** ✅ Build succeeds, assets regenerated automatically + +**Tests:** ✅ All 28 tests pass + +## Technical Details + +**Build Order (After Fix):** +1. Build tools: `seq_compiler`, `tracker_compiler`, `asset_packer` +2. Generate files: `generate_timeline`, `generate_tracker_music`, `generate_demo_assets` +3. **Wait for generation to complete** (new dependency) +4. Build libraries: `audio`, `3d`, `gpu` (now safe to compile) +5. Build executables: `demo64k`, `test_demo` + +**Key Insight:** +The `GENERATED` property alone isn't enough - you also need explicit `add_dependencies()` for libraries that consume generated headers. Without it, CMake may start compiling library sources before the generation targets complete. + +## Files Modified +- CMakeLists.txt (15 insertions) +- tools/seq_compiler.cc (5 changes) +- Removed 4 stale files + +## Current State +- ✅ All targets build successfully after clean +- ✅ All 28 tests passing +- ✅ Working tree clean +- ✅ 4 commits ahead of origin/main + +## Ready For +- Push to origin (`git push`) +- Continue with next priorities + +--- +**handoff(Claude):** Asset regeneration fixed. Clean builds now work correctly. All dependencies tracked properly. diff --git a/HANDOFF_2026-02-07_Final.md b/HANDOFF_2026-02-07_Final.md new file mode 100644 index 0000000..36b53c3 --- /dev/null +++ b/HANDOFF_2026-02-07_Final.md @@ -0,0 +1,55 @@ +# Session Summary - February 7, 2026 + +## Work Completed + +### 1. GpuContext Refactor (2 commits) +- **bd939ac** - Bundle GPU context into GpuContext struct +- **8c9815a** - Store const GpuContext& in Effect base class +- Simplified Effect API, eliminated triplet parameters +- All 28 tests passing + +### 2. Asset Regeneration Fix (1 commit) +- **fb2aa8b** - Fix auto-regenerate assets after clean build +- Added GENERATED properties to generated files +- Added explicit library dependencies on generation targets +- Updated seq_compiler to use GpuContext +- Removed stale test asset files + +### 3. Gantt Chart Tests (2 commits) +- **9583dcc** - Add ASCII Gantt chart output test +- **28a12a7** - Add HTML Gantt chart output test +- Created test_gantt.seq (minimal test timeline) +- Created bash test scripts for both output formats +- All 30 tests passing (was 28) + +## Current State +- **Branch:** main +- **Commits ahead:** 6 (unpushed) +- **Tests:** 30/30 passing (100%) +- **Build:** All targets clean +- **Working tree:** Clean + +## Commits Ready to Push +``` +28a12a7 test: Add HTML Gantt chart output test for seq_compiler +9583dcc test: Add Gantt chart output test for seq_compiler +652f3db docs: Add handoff for asset regeneration fix +fb2aa8b fix: Auto-regenerate assets after clean build +8c9815a refactor: Store const GpuContext& in Effect base class +bd939ac refactor: Bundle GPU context into GpuContext struct +``` + +## Files Modified (Session Total) +- CMakeLists.txt (+33 lines) +- tools/seq_compiler.cc (GpuContext signature) +- src/gpu/effect.h (const GpuContext& ctx_) +- 16 effect files (ctx_.device/queue/format) +- 3 new test files (test_gantt.seq, 2 bash scripts) +- 3 handoff documents + +## Next Steps +- `git push` to sync with remote +- Continue with Task #5 (Spectral Brush Editor) or other priorities + +--- +**handoff(Claude):** Session complete. GpuContext refactor, asset regeneration fix, Gantt tests all done. 30/30 tests passing. diff --git a/assets/music.track b/assets/music.track index d1d1ee7..252fb1d 100644 --- a/assets/music.track +++ b/assets/music.track @@ -1,7 +1,9 @@ # Enhanced Demo Track - Progressive buildup with varied percussion # Features acceleration/deceleration with diverse samples and melodic progression - -# Import expanded drum kit +# +# TIMING: Unit-less (1 unit = 4 beats at 120 BPM = 2 seconds) +# Pattern events use unit-less time (0.0-1.0 for 4-beat pattern) +# Score triggers use unit-less time SAMPLE ASSET_KICK_1 SAMPLE ASSET_KICK_2 SAMPLE ASSET_KICK_2 @@ -20,264 +22,264 @@ SAMPLE ASSET_BASS_1 # === KICK PATTERNS === # Varied kicks for different sections -PATTERN kick_basic - 0.0, ASSET_KICK_1, 1.0, 0.0 - 2.0, ASSET_KICK_1, 1.0, 0.0 - 2.5, ASSET_KICK_2, 0.7, -0.2 +PATTERN kick_basic LENGTH 1.0 + 0.00, ASSET_KICK_1, 1.0, 0.0 + 0.50, ASSET_KICK_1, 1.0, 0.0 +# 2.5, ASSET_KICK_2, 0.7, -0.2 -PATTERN kick_varied - 0.0, ASSET_KICK_2, 1.0, 0.0 - 2.0, ASSET_KICK_3, 0.95, 0.0 - 2.5, ASSET_KICK_1, 0.7, 0.2 +PATTERN kick_varied LENGTH 1.0 + 0.00, ASSET_KICK_2, 1.0, 0.0 + 0.50, ASSET_KICK_3, 0.95, 0.0 +# 2.5, ASSET_KICK_1, 0.7, 0.2 -PATTERN kick_dense - 0.0, ASSET_KICK_1, 1.0, 0.0 - 0.5, ASSET_KICK_2, 0.6, -0.2 - 1.0, ASSET_KICK_3, 0.95, 0.0 - 1.5, ASSET_KICK_2, 0.6, 0.2 - 2.0, ASSET_KICK_1, 1.0, 0.0 - 2.5, ASSET_KICK_2, 0.6, -0.2 - 3.0, ASSET_KICK_3, 0.95, 0.0 - 3.5, ASSET_KICK_2, 0.6, 0.2 +PATTERN kick_dense LENGTH 1.0 + 0.00, ASSET_KICK_1, 1.0, 0.0 + 0.12, ASSET_KICK_2, 0.6, -0.2 + 0.25, ASSET_KICK_3, 0.95, 0.0 + 0.38, ASSET_KICK_2, 0.6, 0.2 + 0.50, ASSET_KICK_1, 1.0, 0.0 + 0.62, ASSET_KICK_2, 0.6, -0.2 + 0.75, ASSET_KICK_3, 0.95, 0.0 + 0.88, ASSET_KICK_2, 0.6, 0.2 # === SNARE PATTERNS === # Louder snare for more punch -PATTERN snare_basic - 1.0, ASSET_SNARE_1, 1.1, 0.1 - 3.0, ASSET_SNARE_1, 1.1, 0.1 +PATTERN snare_basic LENGTH 1.0 + 0.25, ASSET_SNARE_1, 1.1, 0.1 + 0.75, ASSET_SNARE_1, 1.1, 0.1 -PATTERN snare_varied - 1.0, ASSET_SNARE_2, 1.05, -0.1 - 3.0, ASSET_SNARE_4, 1.1, 0.1 +PATTERN snare_varied LENGTH 1.0 + 0.25, ASSET_SNARE_2, 1.05, -0.1 + 0.75, ASSET_SNARE_4, 1.1, 0.1 -PATTERN snare_dense - 0.5, ASSET_SNARE_3, 0.9, 0.0 - 1.0, ASSET_SNARE_1, 1.1, 0.1 - 1.5, ASSET_SNARE_4, 0.85, 0.0 - 2.5, ASSET_SNARE_3, 0.9, 0.0 - 3.0, ASSET_SNARE_2, 1.05, 0.1 - 3.5, ASSET_SNARE_4, 0.85, 0.0 +PATTERN snare_dense LENGTH 1.0 +# 0.5, ASSET_SNARE_3, 0.9, 0.0 + 0.25, ASSET_SNARE_1, 1.1, 0.1 +# 1.5, ASSET_SNARE_4, 0.85, 0.0 + 0.62, ASSET_SNARE_3, 0.9, 0.0 +# 3.0, ASSET_SNARE_2, 1.05, 0.1 + 0.88, ASSET_SNARE_4, 0.85, 0.0 # === HIHAT PATTERNS === -PATTERN hihat_basic - 0.0, ASSET_HIHAT_2, 0.7, -0.3 - 0.5, ASSET_HIHAT_1, 0.35, 0.3 - 1.0, ASSET_HIHAT_2, 0.7, -0.3 - 1.5, ASSET_HIHAT_1, 0.35, 0.3 - 2.0, ASSET_HIHAT_2, 0.7, -0.3 - 2.5, ASSET_HIHAT_1, 0.35, 0.3 - 3.0, ASSET_HIHAT_2, 0.7, -0.3 - 3.5, ASSET_HIHAT_1, 0.35, 0.3 +PATTERN hihat_basic LENGTH 1.0 + 0.00, ASSET_HIHAT_2, 0.7, -0.3 + 0.12, ASSET_HIHAT_1, 0.35, 0.3 + 0.25, ASSET_HIHAT_2, 0.7, -0.3 + 0.38, ASSET_HIHAT_1, 0.35, 0.3 + 0.50, ASSET_HIHAT_2, 0.7, -0.3 + 0.62, ASSET_HIHAT_1, 0.35, 0.3 + 0.75, ASSET_HIHAT_2, 0.7, -0.3 + 0.88, ASSET_HIHAT_1, 0.35, 0.3 -PATTERN hihat_varied - 0.0, ASSET_HIHAT_3, 0.7, -0.3 - 0.5, ASSET_HIHAT_1, 0.35, 0.3 - 1.0, ASSET_HIHAT_4, 0.65, -0.2 - 1.5, ASSET_HIHAT_1, 0.35, 0.3 - 2.0, ASSET_HIHAT_3, 0.7, -0.3 - 2.5, ASSET_HIHAT_1, 0.35, 0.3 - 3.0, ASSET_HIHAT_4, 0.65, -0.2 - 3.5, ASSET_HIHAT_1, 0.35, 0.3 +PATTERN hihat_varied LENGTH 1.0 + 0.00, ASSET_HIHAT_3, 0.7, -0.3 + 0.12, ASSET_HIHAT_1, 0.35, 0.3 + 0.25, ASSET_HIHAT_4, 0.65, -0.2 + 0.38, ASSET_HIHAT_1, 0.35, 0.3 + 0.50, ASSET_HIHAT_3, 0.7, -0.3 + 0.62, ASSET_HIHAT_1, 0.35, 0.3 + 0.75, ASSET_HIHAT_4, 0.65, -0.2 + 0.88, ASSET_HIHAT_1, 0.35, 0.3 # === CYMBAL PATTERNS === # Crash for major transitions only -PATTERN crash - 0.0, ASSET_CRASH_1, 0.85, 0.0 +PATTERN crash LENGTH 1.0 + 0.00, ASSET_CRASH_1, 0.85, 0.0 # Ride for driving the beat (replaces most crashes) -PATTERN ride - 0.0, ASSET_RIDE_1, 0.75, 0.2 +PATTERN ride LENGTH 1.0 + 0.00, ASSET_RIDE_1, 0.75, 0.2 # Faster ride beat for intensity -PATTERN ride_fast - 0.0, ASSET_RIDE_1, 0.75, 0.2 - 0.5, ASSET_RIDE_1, 0.6, 0.2 - 1.0, ASSET_RIDE_1, 0.75, 0.2 - 1.5, ASSET_RIDE_1, 0.6, 0.2 - 2.0, ASSET_RIDE_1, 0.75, 0.2 - 2.5, ASSET_RIDE_1, 0.6, 0.2 - 3.0, ASSET_RIDE_1, 0.75, 0.2 - 3.5, ASSET_RIDE_1, 0.6, 0.2 +PATTERN ride_fast LENGTH 1.0 + 0.00, ASSET_RIDE_1, 0.75, 0.2 + 0.12, ASSET_RIDE_1, 0.6, 0.2 + 0.25, ASSET_RIDE_1, 0.75, 0.2 + 0.38, ASSET_RIDE_1, 0.6, 0.2 + 0.50, ASSET_RIDE_1, 0.75, 0.2 + 0.62, ASSET_RIDE_1, 0.6, 0.2 + 0.75, ASSET_RIDE_1, 0.75, 0.2 + 0.88, ASSET_RIDE_1, 0.6, 0.2 # Splash for accent/variation -PATTERN splash - 0.0, ASSET_SPLASH_1, 0.7, -0.2 +PATTERN splash LENGTH 1.0 + 0.00, ASSET_SPLASH_1, 0.7, -0.2 # === BASS PATTERNS === # Progressive bass introduction with reduced volumes -PATTERN bass_e_soft - 0.0, NOTE_E2, 0.4, 0.0 - 2.0, NOTE_E2, 0.35, 0.0 +PATTERN bass_e_soft LENGTH 1.0 + 0.00, NOTE_E3, 0.4, 0.0 + 0.50, NOTE_E3, 0.35, 0.0 -PATTERN bass_e - 0.0, NOTE_E2, 0.5, 0.0 - 1.0, NOTE_E2, 0.4, 0.0 - 2.0, NOTE_E2, 0.5, 0.0 - 2.5, NOTE_E2, 0.35, 0.0 - 3.0, NOTE_E2, 0.4, 0.0 +PATTERN bass_e LENGTH 1.0 + 0.00, NOTE_E3, 0.5, 0.0 + 0.25, NOTE_E3, 0.4, 0.0 + 0.50, NOTE_E3, 0.5, 0.0 + 0.62, NOTE_E3, 0.35, 0.0 + 0.75, NOTE_E3, 0.4, 0.0 -PATTERN bass_eg - 0.0, NOTE_E2, 0.5, 0.0 - 1.0, NOTE_E2, 0.4, 0.0 - 2.0, NOTE_G2, 0.5, 0.0 - 3.0, NOTE_G2, 0.4, 0.0 +PATTERN bass_eg LENGTH 1.0 + 0.00, NOTE_E3, 0.5, 0.0 + 0.25, NOTE_E3, 0.4, 0.0 + 0.50, NOTE_G3, 0.5, 0.0 + 0.75, NOTE_G3, 0.4, 0.0 -PATTERN bass_progression - 0.0, NOTE_E2, 0.5, 0.0 - 1.0, NOTE_D2, 0.45, 0.0 - 2.0, NOTE_C2, 0.5, 0.0 - 3.0, NOTE_G2, 0.4, 0.0 +PATTERN bass_progression LENGTH 1.0 + 0.00, NOTE_E3, 0.5, 0.0 + 0.25, NOTE_D3, 0.45, 0.0 + 0.50, NOTE_C2, 0.5, 0.0 + 0.75, NOTE_G3, 0.4, 0.0 # === SYNCOPATED BASS PATTERNS === # Punchy, syncopated bass with short notes for final section -PATTERN bass_synco_1 - 0.0, NOTE_E2, 0.6, 0.0 - 0.25, NOTE_E2, 0.5, 0.1 - 0.75, NOTE_E2, 0.55, -0.1 - 1.5, NOTE_E2, 0.5, 0.0 - 2.0, NOTE_E2, 0.6, 0.0 - 2.75, NOTE_G2, 0.55, 0.1 - 3.25, NOTE_E2, 0.5, 0.0 +PATTERN bass_synco_1 LENGTH 1.0 + 0.00, NOTE_E3, 0.6, 0.0 + 0.06, NOTE_E3, 0.5, 0.1 + 0.19, NOTE_E3, 0.55, -0.1 + 0.38, NOTE_E3, 0.5, 0.0 + 0.50, NOTE_E3, 0.6, 0.0 + 0.69, NOTE_G3, 0.55, 0.1 + 0.81, NOTE_E3, 0.5, 0.0 -PATTERN bass_synco_2 - 0.0, NOTE_E2, 0.6, 0.0 - 0.5, NOTE_D2, 0.55, -0.1 - 1.25, NOTE_E2, 0.5, 0.1 - 1.75, NOTE_D2, 0.5, 0.0 - 2.0, NOTE_C2, 0.6, 0.0 - 2.5, NOTE_E2, 0.5, 0.1 - 3.0, NOTE_G2, 0.6, 0.0 - 3.5, NOTE_E2, 0.5, -0.1 +PATTERN bass_synco_2 LENGTH 1.0 + 0.00, NOTE_E3, 0.6, 0.0 + 0.12, NOTE_D3, 0.55, -0.1 + 0.31, NOTE_E3, 0.5, 0.1 + 0.44, NOTE_D3, 0.5, 0.0 + 0.50, NOTE_C2, 0.6, 0.0 + 0.62, NOTE_E3, 0.5, 0.1 + 0.75, NOTE_G3, 0.6, 0.0 + 0.88, NOTE_E3, 0.5, -0.1 -PATTERN bass_synco_3 - 0.0, NOTE_E2, 0.65, 0.0 - 0.25, NOTE_E2, 0.5, 0.0 - 0.5, NOTE_E2, 0.55, 0.1 - 1.0, NOTE_G2, 0.6, 0.0 - 1.5, NOTE_E2, 0.5, -0.1 - 2.25, NOTE_D2, 0.55, 0.0 - 2.75, NOTE_E2, 0.5, 0.1 - 3.5, NOTE_E2, 0.55, 0.0 +PATTERN bass_synco_3 LENGTH 1.0 + 0.00, NOTE_E3, 0.65, 0.0 + 0.06, NOTE_E3, 0.5, 0.0 + 0.12, NOTE_E3, 0.55, 0.1 + 0.25, NOTE_G3, 0.6, 0.0 + 0.38, NOTE_E3, 0.5, -0.1 + 0.56, NOTE_D2, 0.55, 0.0 + 0.69, NOTE_E3, 0.5, 0.1 + 0.88, NOTE_E3, 0.55, 0.0 # === SCORE === SCORE # Phase 1: Intro - Minimal setup (0-4s) - 0.0, crash - 0.0, kick_basic - 0.0, hihat_basic + 0.00, crash + 0.00, kick_basic + 0.00, hihat_basic - 2.0, kick_basic - 2.0, snare_basic - 2.0, hihat_basic + 0.50, kick_basic + 0.50, snare_basic + 0.50, hihat_basic # Phase 2: Build - Add variety (4-8s) - 4.0, ride - 4.0, kick_varied - 4.0, snare_basic - 4.0, hihat_varied + 1.00, ride + 1.00, kick_varied + 1.00, snare_basic + 1.00, hihat_varied - 6.0, kick_varied - 6.0, snare_varied - 6.0, hihat_varied + 1.50, kick_varied + 1.50, snare_varied + 1.50, hihat_varied # Phase 3: Introduce bass softly (8-12s) - 8.0, splash - 8.0, kick_basic - 8.0, snare_basic - 8.0, hihat_basic - 8.0, bass_e_soft + 2.00, splash + 2.00, kick_basic + 2.00, snare_basic + 2.00, hihat_basic + 2.00, bass_e_soft - 10.0, kick_varied - 10.0, snare_varied - 10.0, hihat_varied - 10.0, bass_e_soft + 2.50, kick_varied + 2.50, snare_varied + 2.50, hihat_varied + 2.50, bass_e_soft # Phase 4: Acceleration section (12-16s music time) # tempo_scale accelerates from 1.0 to 2.0 - 12.0, ride - 12.0, kick_basic - 12.0, snare_basic - 12.0, hihat_basic - 12.0, bass_e + 3.00, ride + 3.00, kick_basic + 3.00, snare_basic + 3.00, hihat_basic + 3.00, bass_e - 14.0, kick_varied - 14.0, snare_varied - 14.0, hihat_varied - 14.0, bass_eg + 3.50, kick_varied + 3.50, snare_varied + 3.50, hihat_varied + 3.50, bass_eg # Phase 5: After acceleration reset - denser patterns (16-20s) # tempo_scale = 1.0 with 2x denser patterns - 16.0, crash - 16.0, kick_dense - 16.0, snare_dense - 16.0, hihat_varied - 16.0, bass_e + 4.00, crash + 4.00, kick_dense + 4.00, snare_dense + 4.00, hihat_varied + 4.00, bass_e - 18.0, kick_dense - 18.0, snare_dense - 18.0, hihat_basic - 18.0, bass_progression + 4.50, kick_dense + 4.50, snare_dense + 4.50, hihat_basic + 4.50, bass_progression # Phase 6: Continue buildup (20-24s) - 20.0, ride - 20.0, kick_dense - 20.0, snare_dense - 20.0, hihat_varied - 20.0, bass_e + 5.00, ride + 5.00, kick_dense + 5.00, snare_dense + 5.00, hihat_varied + 5.00, bass_e - 22.0, kick_dense - 22.0, snare_dense - 22.0, hihat_basic - 22.0, bass_eg + 5.50, kick_dense + 5.50, snare_dense + 5.50, hihat_basic + 5.50, bass_eg # Phase 7: Slow-down section (24-28s music time) # tempo_scale decelerates from 1.0 to 0.5 - 24.0, splash - 24.0, kick_dense - 24.0, snare_dense - 24.0, hihat_varied - 24.0, bass_progression + 6.00, splash + 6.00, kick_dense + 6.00, snare_dense + 6.00, hihat_varied + 6.00, bass_progression - 26.0, kick_dense - 26.0, snare_dense - 26.0, hihat_basic - 26.0, bass_e + 6.50, kick_dense + 6.50, snare_dense + 6.50, hihat_basic + 6.50, bass_e # Phase 8: Build to break (28-31s) - 28.0, ride_fast - 28.0, kick_basic - 28.0, snare_varied - 28.0, hihat_varied - 28.0, bass_eg + 7.00, ride_fast + 7.00, kick_basic + 7.00, snare_varied + 7.00, hihat_varied + 7.00, bass_eg - 30.0, kick_varied - 30.0, snare_basic - 30.0, hihat_basic - 30.0, bass_progression + 7.50, kick_varied + 7.50, snare_basic + 7.50, hihat_basic + 7.50, bass_progression # DRAMATIC BREAK: 1 beat of silence before climax (31-32s) - 31.0, hihat_basic + 7.75, hihat_basic # Phase 9: CLIMAX - Punchy syncopated bass with fast ride (32-36s) - 32.0, crash - 32.0, ride_fast - 32.0, kick_dense - 32.0, snare_dense - 32.0, hihat_varied - 32.0, bass_synco_1 + 8.00, crash + 8.00, ride_fast + 8.00, kick_dense + 8.00, snare_dense + 8.00, hihat_varied + 8.00, bass_synco_1 - 34.0, ride_fast - 34.0, kick_dense - 34.0, snare_dense - 34.0, hihat_basic - 34.0, bass_synco_2 + 8.50, ride_fast + 8.50, kick_dense + 8.50, snare_dense + 8.50, hihat_basic + 8.50, bass_synco_2 # Phase 10: Final push with syncopation (36-38s) - 36.0, ride_fast - 36.0, kick_dense - 36.0, snare_dense - 36.0, hihat_varied - 36.0, bass_synco_3 + 9.00, ride_fast + 9.00, kick_dense + 9.00, snare_dense + 9.00, hihat_varied + 9.00, bass_synco_3 # Ending - 38.0, crash + 9.50, crash diff --git a/assets/test_demo.track b/assets/test_demo.track index 8c06100..6ae5c67 100644 --- a/assets/test_demo.track +++ b/assets/test_demo.track @@ -1,32 +1,36 @@ # 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 +# +# TIMING: Unit-less (1 unit = 4 beats at 120 BPM = 2 seconds) +# Pattern events use unit-less time (0.0-1.0 for 4-beat pattern) +# Score triggers use unit-less time 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_basic LENGTH 1.0 + 0.00, ASSET_KICK_1, 1.0, 0.0 + 0.00, NOTE_A4, 0.5, 0.0 + 0.25, ASSET_SNARE_1, 0.9, 0.0 + 0.50, ASSET_KICK_1, 1.0, 0.0 + 0.75, 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 +PATTERN drums_with_crash LENGTH 1.0 + 0.00, ASSET_KICK_1, 1.0, 0.0 + 0.00, ASSET_CRASH_1, 0.85, 0.0 + 0.00, NOTE_A4, 0.5, 0.0 + 0.25, ASSET_SNARE_1, 0.9, 0.0 + 0.50, ASSET_KICK_1, 1.0, 0.0 + 0.75, 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 + 1.0, drums_basic + 2.0, drums_with_crash + 3.0, drums_basic + 4.0, drums_basic + 5.0, drums_basic + 6.0, drums_with_crash + 7.0, drums_basic diff --git a/assets/test_gantt.seq b/assets/test_gantt.seq new file mode 100644 index 0000000..92f9cfc --- /dev/null +++ b/assets/test_gantt.seq @@ -0,0 +1,11 @@ +# Test sequence file for Gantt chart testing +# BPM 120 + +# Simple timeline with two sequences +SEQUENCE 0.0 0 "Sequence A" + EFFECT + FlashEffect 0.0 2.0 + +SEQUENCE 1.0 0 "Sequence B" + EFFECT + PassthroughEffect 0.0 2.0 + +END_DEMO 4.0 diff --git a/convert_track.py b/convert_track.py new file mode 100644 index 0000000..ec9d62c --- /dev/null +++ b/convert_track.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Convert .track files from beat-based to unit-less timing.""" + +import re +import sys + +def convert_beat_to_unit(beat_str): + """Convert beat value to unit-less (beat / 4).""" + beat = float(beat_str) + unit = beat / 4.0 + return f"{unit:.2f}" + +def process_line(line): + """Process a single line, converting beat values.""" + line = line.rstrip('\n') + + # Skip comments and empty lines + if not line.strip() or line.strip().startswith('#'): + return line + + # PATTERN line - add LENGTH 1.0 + if line.strip().startswith('PATTERN '): + # Check if LENGTH already exists + if ' LENGTH ' in line: + return line + parts = line.split() + if len(parts) >= 2: + return f"PATTERN {parts[1]} LENGTH 1.0" + return line + + # Event line (starts with a number) + match = re.match(r'^(\s*)([0-9.]+),\s*(.+)$', line) + if match: + indent, beat_str, rest = match.groups() + unit_str = convert_beat_to_unit(beat_str) + return f"{indent}{unit_str}, {rest}" + + return line + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} <input.track> <output.track>") + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + + with open(input_file, 'r') as f: + lines = f.readlines() + + # Add header comment about timing + output_lines = [] + output_lines.append("# Enhanced Demo Track - Progressive buildup with varied percussion\n") + output_lines.append("# Features acceleration/deceleration with diverse samples and melodic progression\n") + output_lines.append("#\n") + output_lines.append("# TIMING: Unit-less (1 unit = 4 beats at 120 BPM = 2 seconds)\n") + output_lines.append("# Pattern events use unit-less time (0.0-1.0 for 4-beat pattern)\n") + output_lines.append("# Score triggers use unit-less time\n") + + # Skip old header lines + start_idx = 0 + for i, line in enumerate(lines): + if line.strip() and not line.strip().startswith('#'): + start_idx = i + break + + # Process rest of file + for line in lines[start_idx:]: + output_lines.append(process_line(line) + '\n') + + with open(output_file, 'w') as f: + f.writelines(output_lines) + + print(f"Converted {input_file} -> {output_file}") + +if __name__ == '__main__': + main() diff --git a/doc/TRACKER.md b/doc/TRACKER.md index cb14755..f3a34a3 100644 --- a/doc/TRACKER.md +++ b/doc/TRACKER.md @@ -40,4 +40,85 @@ This generated code can be mixed with fixed code from the demo codebase itself (explosion predefined at a given time ,etc.) The baking is done at compile time, and the code will go in src/generated/ +## .track File Format + +### Timing System + +**Unit-less Timing Convention:** +- All time values are **unit-less** (not beats or seconds) +- Convention: **1 unit = 4 beats** +- Conversion to seconds: `seconds = units * (4 / BPM) * 60` +- At 120 BPM: 1 unit = 2 seconds + +This makes patterns independent of BPM - changing BPM only affects playback speed, not pattern structure. + +### File Structure + +``` +# Comments start with # + +BPM <tempo> # Optional, defaults to 120 BPM + +SAMPLE <name> # Define sample (asset or generated note) + +PATTERN <name> LENGTH <duration> # Define pattern with unit-less duration + <unit_time>, <sample>, <volume>, <pan> # Pattern events + +SCORE # Score section (pattern triggers) + <unit_time>, <pattern_name> +``` + +### Examples + +#### Simple 4-beat pattern (1 unit): +``` +PATTERN kick_snare LENGTH 1.0 + 0.00, ASSET_KICK_1, 1.0, 0.0 # Start of pattern (beat 0) + 0.25, ASSET_SNARE_1, 0.9, 0.0 # 1/4 through (beat 1) + 0.50, ASSET_KICK_1, 1.0, 0.0 # 1/2 through (beat 2) + 0.75, ASSET_SNARE_1, 0.9, 0.0 # 3/4 through (beat 3) +``` + +#### Score triggers: +``` +SCORE + 0.0, kick_snare # Trigger at 0 seconds (120 BPM) + 1.0, kick_snare # Trigger at 2 seconds (1 unit = 2s at 120 BPM) + 2.0, kick_snare # Trigger at 4 seconds +``` + +#### Generated note: +``` +SAMPLE NOTE_C4 # Automatically generates C4 note (261.63 Hz) +PATTERN melody LENGTH 1.0 + 0.00, NOTE_C4, 0.8, 0.0 + 0.25, NOTE_E4, 0.7, 0.0 + 0.50, NOTE_G4, 0.8, 0.0 +``` + +### Conversion Reference + +At 120 BPM (1 unit = 4 beats = 2 seconds): + +| Units | Beats | Seconds | Description | +|-------|-------|---------|-------------| +| 0.00 | 0 | 0.0 | Start | +| 0.25 | 1 | 0.5 | Quarter | +| 0.50 | 2 | 1.0 | Half | +| 0.75 | 3 | 1.5 | Three-quarter | +| 1.00 | 4 | 2.0 | Full pattern | + +### Pattern Length + +- `LENGTH` parameter is optional, defaults to 1.0 +- Can be any value (0.5 for half-length, 2.0 for double-length, etc.) +- Events must be within range `[0.0, LENGTH]` + +Example of half-length pattern: +``` +PATTERN short_fill LENGTH 0.5 # 2 beats = 1 second at 120 BPM + 0.00, ASSET_HIHAT, 0.7, 0.0 + 0.50, ASSET_HIHAT, 0.6, 0.0 # 0.50 * 0.5 = 1 beat into the pattern +``` + diff --git a/scripts/test_gantt_html.sh b/scripts/test_gantt_html.sh new file mode 100755 index 0000000..d7a5777 --- /dev/null +++ b/scripts/test_gantt_html.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Test script for seq_compiler HTML Gantt chart output + +set -e # Exit on error + +# Arguments +SEQ_COMPILER=$1 +INPUT_SEQ=$2 +OUTPUT_HTML=$3 + +if [ -z "$SEQ_COMPILER" ] || [ -z "$INPUT_SEQ" ] || [ -z "$OUTPUT_HTML" ]; then + echo "Usage: $0 <seq_compiler> <input.seq> <output.html>" + exit 1 +fi + +# Clean up any existing output +rm -f "$OUTPUT_HTML" + +# Run seq_compiler with HTML Gantt output +"$SEQ_COMPILER" "$INPUT_SEQ" "--gantt-html=$OUTPUT_HTML" > /dev/null 2>&1 + +# Check output file exists +if [ ! -f "$OUTPUT_HTML" ]; then + echo "ERROR: HTML output file not created" + exit 1 +fi + +# Verify key content exists +ERRORS=0 + +# Check for HTML structure +if ! grep -q "<!DOCTYPE html>" "$OUTPUT_HTML"; then + echo "ERROR: Missing HTML doctype" + ERRORS=$((ERRORS + 1)) +fi + +if ! grep -q "<html>" "$OUTPUT_HTML"; then + echo "ERROR: Missing <html> tag" + ERRORS=$((ERRORS + 1)) +fi + +# Check for title (matches actual format: "Demo Timeline - BPM <bpm>") +if ! grep -q "<title>Demo Timeline" "$OUTPUT_HTML"; then + echo "ERROR: Missing page title" + ERRORS=$((ERRORS + 1)) +fi + +# Check for main heading +if ! grep -q "<h1>Demo Timeline Gantt Chart</h1>" "$OUTPUT_HTML"; then + echo "ERROR: Missing main heading" + ERRORS=$((ERRORS + 1)) +fi + +# Check for SVG content +if ! grep -q "<svg" "$OUTPUT_HTML"; then + echo "ERROR: Missing SVG element" + ERRORS=$((ERRORS + 1)) +fi + +# Check for timeline visualization (rectangles for sequences) +if ! grep -q "<rect" "$OUTPUT_HTML"; then + echo "ERROR: Missing SVG rectangles (sequence bars)" + ERRORS=$((ERRORS + 1)) +fi + +# Check for text labels +if ! grep -q "<text" "$OUTPUT_HTML"; then + echo "ERROR: Missing SVG text labels" + ERRORS=$((ERRORS + 1)) +fi + +# Check for time axis elements +if ! grep -q "Time axis" "$OUTPUT_HTML"; then + echo "ERROR: Missing time axis comment" + ERRORS=$((ERRORS + 1)) +fi + +# Check file is not empty (HTML should be larger than ASCII) +FILE_SIZE=$(wc -c < "$OUTPUT_HTML") +if [ "$FILE_SIZE" -lt 500 ]; then + echo "ERROR: HTML output is too small ($FILE_SIZE bytes)" + ERRORS=$((ERRORS + 1)) +fi + +# Verify it's valid HTML (basic check - no unclosed tags) +OPEN_TAGS=$(grep -o "<[^/][^>]*>" "$OUTPUT_HTML" | wc -l) +CLOSE_TAGS=$(grep -o "</[^>]*>" "$OUTPUT_HTML" | wc -l) +if [ "$OPEN_TAGS" -ne "$CLOSE_TAGS" ]; then + echo "WARNING: HTML tag mismatch (open=$OPEN_TAGS, close=$CLOSE_TAGS)" + # Don't fail on this - some self-closing tags might not match +fi + +if [ $ERRORS -eq 0 ]; then + echo "✓ HTML Gantt chart output test passed" + exit 0 +else + echo "✗ HTML Gantt chart output test failed ($ERRORS errors)" + echo "--- Output file size: $FILE_SIZE bytes ---" + echo "--- First 50 lines ---" + head -50 "$OUTPUT_HTML" + exit 1 +fi diff --git a/scripts/test_gantt_output.sh b/scripts/test_gantt_output.sh new file mode 100755 index 0000000..3cfb9c3 --- /dev/null +++ b/scripts/test_gantt_output.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Test script for seq_compiler Gantt chart output + +set -e # Exit on error + +# Arguments +SEQ_COMPILER=$1 +INPUT_SEQ=$2 +OUTPUT_GANTT=$3 + +if [ -z "$SEQ_COMPILER" ] || [ -z "$INPUT_SEQ" ] || [ -z "$OUTPUT_GANTT" ]; then + echo "Usage: $0 <seq_compiler> <input.seq> <output_gantt.txt>" + exit 1 +fi + +# Clean up any existing output +rm -f "$OUTPUT_GANTT" + +# Run seq_compiler with Gantt output +"$SEQ_COMPILER" "$INPUT_SEQ" "--gantt=$OUTPUT_GANTT" > /dev/null 2>&1 + +# Check output file exists +if [ ! -f "$OUTPUT_GANTT" ]; then + echo "ERROR: Gantt output file not created" + exit 1 +fi + +# Verify key content exists +ERRORS=0 + +# Check for timeline header +if ! grep -q "Demo Timeline Gantt Chart" "$OUTPUT_GANTT"; then + echo "ERROR: Missing 'Demo Timeline Gantt Chart' header" + ERRORS=$((ERRORS + 1)) +fi + +# Check for BPM info +if ! grep -q "BPM:" "$OUTPUT_GANTT"; then + echo "ERROR: Missing 'BPM:' information" + ERRORS=$((ERRORS + 1)) +fi + +# Check for time axis +if ! grep -q "Time (s):" "$OUTPUT_GANTT"; then + echo "ERROR: Missing 'Time (s):' axis" + ERRORS=$((ERRORS + 1)) +fi + +# Check for sequence bars (should have '█' characters) +if ! grep -q "█" "$OUTPUT_GANTT"; then + echo "ERROR: Missing sequence visualization bars" + ERRORS=$((ERRORS + 1)) +fi + +# Check file is not empty +FILE_SIZE=$(wc -c < "$OUTPUT_GANTT") +if [ "$FILE_SIZE" -lt 100 ]; then + echo "ERROR: Gantt output is too small ($FILE_SIZE bytes)" + ERRORS=$((ERRORS + 1)) +fi + +if [ $ERRORS -eq 0 ]; then + echo "✓ Gantt chart output test passed" + exit 0 +else + echo "✗ Gantt chart output test failed ($ERRORS errors)" + echo "--- Output file contents ---" + cat "$OUTPUT_GANTT" + exit 1 +fi diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 7ad5a67..9ae772e 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -212,19 +212,24 @@ static void trigger_note_event(const TrackerEvent& event) { } void tracker_update(float music_time_sec) { + // Unit-less timing: 1 unit = 4 beats (by convention) + const float BEATS_PER_UNIT = 4.0f; + const float unit_duration_sec = (BEATS_PER_UNIT / g_tracker_score.bpm) * 60.0f; + // Step 1: Process new pattern triggers while (g_last_trigger_idx < g_tracker_score.num_triggers) { const TrackerPatternTrigger& trigger = g_tracker_score.triggers[g_last_trigger_idx]; - if (trigger.time_sec > music_time_sec) + const float trigger_time_sec = trigger.unit_time * unit_duration_sec; + if (trigger_time_sec > music_time_sec) break; // Add this pattern to active patterns list const int slot = get_free_pattern_slot(); if (slot != -1) { g_active_patterns[slot].pattern_id = trigger.pattern_id; - g_active_patterns[slot].start_music_time = trigger.time_sec; + g_active_patterns[slot].start_music_time = trigger_time_sec; g_active_patterns[slot].next_event_idx = 0; g_active_patterns[slot].active = true; } @@ -233,8 +238,6 @@ void tracker_update(float music_time_sec) { } // Step 2: Update all active patterns and trigger individual events - const float beat_duration = 60.0f / g_tracker_score.bpm; - for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { if (!g_active_patterns[i].active) continue; @@ -242,15 +245,15 @@ void tracker_update(float music_time_sec) { ActivePattern& active = g_active_patterns[i]; const TrackerPattern& pattern = g_tracker_patterns[active.pattern_id]; - // Calculate elapsed beats since pattern started + // Calculate elapsed unit-less time since pattern started const float elapsed_music_time = music_time_sec - active.start_music_time; - const float elapsed_beats = elapsed_music_time / beat_duration; + const float elapsed_units = elapsed_music_time / unit_duration_sec; - // Trigger all events that have passed their beat time + // Trigger all events that have passed their unit time while (active.next_event_idx < pattern.num_events) { const TrackerEvent& event = pattern.events[active.next_event_idx]; - if (event.beat > elapsed_beats) + if (event.unit_time > elapsed_units) break; // This event hasn't reached its time yet // Trigger this event as an individual voice @@ -259,8 +262,8 @@ void tracker_update(float music_time_sec) { active.next_event_idx++; } - // If all events have been triggered, mark pattern as complete - if (active.next_event_idx >= pattern.num_events) { + // Pattern remains active until full duration elapses + if (elapsed_units >= pattern.unit_length) { active.active = false; } } diff --git a/src/audio/tracker.h b/src/audio/tracker.h index 336f77f..4cd011b 100644 --- a/src/audio/tracker.h +++ b/src/audio/tracker.h @@ -8,7 +8,7 @@ #include <cstdint> struct TrackerEvent { - float beat; + float unit_time; // Unit-less time within pattern (0.0 to pattern.unit_length) uint16_t sample_id; float volume; float pan; @@ -17,11 +17,11 @@ struct TrackerEvent { struct TrackerPattern { const TrackerEvent* events; uint32_t num_events; - float num_beats; + float unit_length; // Pattern duration in units (typically 1.0 for 4-beat patterns) }; struct TrackerPatternTrigger { - float time_sec; + float unit_time; // Unit-less time when pattern triggers uint16_t pattern_id; // Modifiers could be added here }; @@ -29,7 +29,7 @@ struct TrackerPatternTrigger { struct TrackerScore { const TrackerPatternTrigger* triggers; uint32_t num_triggers; - float bpm; + float bpm; // BPM is used only for playback scaling (1 unit = 4 beats) }; // Global music data generated by tracker_compiler diff --git a/src/generated/music_data.cc b/src/generated/music_data.cc index 0852e93..7db925a 100644 --- a/src/generated/music_data.cc +++ b/src/generated/music_data.cc @@ -20,12 +20,13 @@ const NoteParams g_tracker_samples[] = { { 0 }, // ASSET_RIDE_1 (ASSET) { 0 }, // ASSET_SPLASH_1 (ASSET) { 0 }, // ASSET_BASS_1 (ASSET) - { 82.4f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_E2 - { 98.0f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_G2 - { 73.4f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_D2 + { 164.8f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_E3 + { 196.0f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_G3 + { 146.8f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_D3 { 65.4f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_C2 + { 73.4f, 0.50f, 1.0f, 0.01f, 0.0f, 0.0f, 0.0f, 3, 0.6f, 0.0f, 0.0f }, // NOTE_D2 }; -const uint32_t g_tracker_samples_count = 19; +const uint32_t g_tracker_samples_count = 20; const AssetId g_tracker_sample_assets[] = { AssetId::ASSET_KICK_1, @@ -47,156 +48,152 @@ const AssetId g_tracker_sample_assets[] = { AssetId::ASSET_LAST_ID, AssetId::ASSET_LAST_ID, AssetId::ASSET_LAST_ID, + AssetId::ASSET_LAST_ID, }; static const TrackerEvent PATTERN_EVENTS_kick_basic[] = { - { 0.0f, 0, 1.0f, 0.0f }, - { 2.0f, 0, 1.0f, 0.0f }, - { 2.5f, 2, 0.7f, -0.2f }, + { 0.00f, 0, 1.0f, 0.0f }, + { 0.50f, 0, 1.0f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_kick_varied[] = { - { 0.0f, 2, 1.0f, 0.0f }, - { 2.0f, 0, 0.9f, 0.0f }, - { 2.5f, 0, 0.7f, 0.2f }, + { 0.00f, 2, 1.0f, 0.0f }, + { 0.50f, 0, 0.9f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_kick_dense[] = { - { 0.0f, 0, 1.0f, 0.0f }, - { 0.5f, 2, 0.6f, -0.2f }, - { 1.0f, 0, 0.9f, 0.0f }, - { 1.5f, 2, 0.6f, 0.2f }, - { 2.0f, 0, 1.0f, 0.0f }, - { 2.5f, 2, 0.6f, -0.2f }, - { 3.0f, 0, 0.9f, 0.0f }, - { 3.5f, 2, 0.6f, 0.2f }, + { 0.00f, 0, 1.0f, 0.0f }, + { 0.12f, 2, 0.6f, -0.2f }, + { 0.25f, 0, 0.9f, 0.0f }, + { 0.38f, 2, 0.6f, 0.2f }, + { 0.50f, 0, 1.0f, 0.0f }, + { 0.62f, 2, 0.6f, -0.2f }, + { 0.75f, 0, 0.9f, 0.0f }, + { 0.88f, 2, 0.6f, 0.2f }, }; static const TrackerEvent PATTERN_EVENTS_snare_basic[] = { - { 1.0f, 3, 1.1f, 0.1f }, - { 3.0f, 3, 1.1f, 0.1f }, + { 0.25f, 3, 1.1f, 0.1f }, + { 0.75f, 3, 1.1f, 0.1f }, }; static const TrackerEvent PATTERN_EVENTS_snare_varied[] = { - { 1.0f, 4, 1.0f, -0.1f }, - { 3.0f, 0, 1.1f, 0.1f }, + { 0.25f, 4, 1.0f, -0.1f }, + { 0.75f, 0, 1.1f, 0.1f }, }; static const TrackerEvent PATTERN_EVENTS_snare_dense[] = { - { 0.5f, 6, 0.9f, 0.0f }, - { 1.0f, 3, 1.1f, 0.1f }, - { 1.5f, 0, 0.9f, 0.0f }, - { 2.5f, 6, 0.9f, 0.0f }, - { 3.0f, 4, 1.0f, 0.1f }, - { 3.5f, 0, 0.9f, 0.0f }, + { 0.25f, 3, 1.1f, 0.1f }, + { 0.62f, 6, 0.9f, 0.0f }, + { 0.88f, 0, 0.9f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_hihat_basic[] = { - { 0.0f, 8, 0.7f, -0.3f }, - { 0.5f, 7, 0.3f, 0.3f }, - { 1.0f, 8, 0.7f, -0.3f }, - { 1.5f, 7, 0.3f, 0.3f }, - { 2.0f, 8, 0.7f, -0.3f }, - { 2.5f, 7, 0.3f, 0.3f }, - { 3.0f, 8, 0.7f, -0.3f }, - { 3.5f, 7, 0.3f, 0.3f }, + { 0.00f, 8, 0.7f, -0.3f }, + { 0.12f, 7, 0.3f, 0.3f }, + { 0.25f, 8, 0.7f, -0.3f }, + { 0.38f, 7, 0.3f, 0.3f }, + { 0.50f, 8, 0.7f, -0.3f }, + { 0.62f, 7, 0.3f, 0.3f }, + { 0.75f, 8, 0.7f, -0.3f }, + { 0.88f, 7, 0.3f, 0.3f }, }; static const TrackerEvent PATTERN_EVENTS_hihat_varied[] = { - { 0.0f, 10, 0.7f, -0.3f }, - { 0.5f, 7, 0.3f, 0.3f }, - { 1.0f, 0, 0.6f, -0.2f }, - { 1.5f, 7, 0.3f, 0.3f }, - { 2.0f, 10, 0.7f, -0.3f }, - { 2.5f, 7, 0.3f, 0.3f }, - { 3.0f, 0, 0.6f, -0.2f }, - { 3.5f, 7, 0.3f, 0.3f }, + { 0.00f, 10, 0.7f, -0.3f }, + { 0.12f, 7, 0.3f, 0.3f }, + { 0.25f, 0, 0.6f, -0.2f }, + { 0.38f, 7, 0.3f, 0.3f }, + { 0.50f, 10, 0.7f, -0.3f }, + { 0.62f, 7, 0.3f, 0.3f }, + { 0.75f, 0, 0.6f, -0.2f }, + { 0.88f, 7, 0.3f, 0.3f }, }; static const TrackerEvent PATTERN_EVENTS_crash[] = { - { 0.0f, 11, 0.9f, 0.0f }, + { 0.00f, 11, 0.9f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_ride[] = { - { 0.0f, 12, 0.8f, 0.2f }, + { 0.00f, 12, 0.8f, 0.2f }, }; static const TrackerEvent PATTERN_EVENTS_ride_fast[] = { - { 0.0f, 12, 0.8f, 0.2f }, - { 0.5f, 12, 0.6f, 0.2f }, - { 1.0f, 12, 0.8f, 0.2f }, - { 1.5f, 12, 0.6f, 0.2f }, - { 2.0f, 12, 0.8f, 0.2f }, - { 2.5f, 12, 0.6f, 0.2f }, - { 3.0f, 12, 0.8f, 0.2f }, - { 3.5f, 12, 0.6f, 0.2f }, + { 0.00f, 12, 0.8f, 0.2f }, + { 0.12f, 12, 0.6f, 0.2f }, + { 0.25f, 12, 0.8f, 0.2f }, + { 0.38f, 12, 0.6f, 0.2f }, + { 0.50f, 12, 0.8f, 0.2f }, + { 0.62f, 12, 0.6f, 0.2f }, + { 0.75f, 12, 0.8f, 0.2f }, + { 0.88f, 12, 0.6f, 0.2f }, }; static const TrackerEvent PATTERN_EVENTS_splash[] = { - { 0.0f, 13, 0.7f, -0.2f }, + { 0.00f, 13, 0.7f, -0.2f }, }; static const TrackerEvent PATTERN_EVENTS_bass_e_soft[] = { - { 0.0f, 15, 0.4f, 0.0f }, - { 2.0f, 15, 0.3f, 0.0f }, + { 0.00f, 15, 0.4f, 0.0f }, + { 0.50f, 15, 0.3f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_bass_e[] = { - { 0.0f, 15, 0.5f, 0.0f }, - { 1.0f, 15, 0.4f, 0.0f }, - { 2.0f, 15, 0.5f, 0.0f }, - { 2.5f, 15, 0.3f, 0.0f }, - { 3.0f, 15, 0.4f, 0.0f }, + { 0.00f, 15, 0.5f, 0.0f }, + { 0.25f, 15, 0.4f, 0.0f }, + { 0.50f, 15, 0.5f, 0.0f }, + { 0.62f, 15, 0.3f, 0.0f }, + { 0.75f, 15, 0.4f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_bass_eg[] = { - { 0.0f, 15, 0.5f, 0.0f }, - { 1.0f, 15, 0.4f, 0.0f }, - { 2.0f, 16, 0.5f, 0.0f }, - { 3.0f, 16, 0.4f, 0.0f }, + { 0.00f, 15, 0.5f, 0.0f }, + { 0.25f, 15, 0.4f, 0.0f }, + { 0.50f, 16, 0.5f, 0.0f }, + { 0.75f, 16, 0.4f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_bass_progression[] = { - { 0.0f, 15, 0.5f, 0.0f }, - { 1.0f, 17, 0.4f, 0.0f }, - { 2.0f, 18, 0.5f, 0.0f }, - { 3.0f, 16, 0.4f, 0.0f }, + { 0.00f, 15, 0.5f, 0.0f }, + { 0.25f, 17, 0.4f, 0.0f }, + { 0.50f, 18, 0.5f, 0.0f }, + { 0.75f, 16, 0.4f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_bass_synco_1[] = { - { 0.0f, 15, 0.6f, 0.0f }, - { 0.2f, 15, 0.5f, 0.1f }, - { 0.8f, 15, 0.6f, -0.1f }, - { 1.5f, 15, 0.5f, 0.0f }, - { 2.0f, 15, 0.6f, 0.0f }, - { 2.8f, 16, 0.6f, 0.1f }, - { 3.2f, 15, 0.5f, 0.0f }, + { 0.00f, 15, 0.6f, 0.0f }, + { 0.06f, 15, 0.5f, 0.1f }, + { 0.19f, 15, 0.6f, -0.1f }, + { 0.38f, 15, 0.5f, 0.0f }, + { 0.50f, 15, 0.6f, 0.0f }, + { 0.69f, 16, 0.6f, 0.1f }, + { 0.81f, 15, 0.5f, 0.0f }, }; static const TrackerEvent PATTERN_EVENTS_bass_synco_2[] = { - { 0.0f, 15, 0.6f, 0.0f }, - { 0.5f, 17, 0.6f, -0.1f }, - { 1.2f, 15, 0.5f, 0.1f }, - { 1.8f, 17, 0.5f, 0.0f }, - { 2.0f, 18, 0.6f, 0.0f }, - { 2.5f, 15, 0.5f, 0.1f }, - { 3.0f, 16, 0.6f, 0.0f }, - { 3.5f, 15, 0.5f, -0.1f }, + { 0.00f, 15, 0.6f, 0.0f }, + { 0.12f, 17, 0.6f, -0.1f }, + { 0.31f, 15, 0.5f, 0.1f }, + { 0.44f, 17, 0.5f, 0.0f }, + { 0.50f, 18, 0.6f, 0.0f }, + { 0.62f, 15, 0.5f, 0.1f }, + { 0.75f, 16, 0.6f, 0.0f }, + { 0.88f, 15, 0.5f, -0.1f }, }; static const TrackerEvent PATTERN_EVENTS_bass_synco_3[] = { - { 0.0f, 15, 0.6f, 0.0f }, - { 0.2f, 15, 0.5f, 0.0f }, - { 0.5f, 15, 0.6f, 0.1f }, - { 1.0f, 16, 0.6f, 0.0f }, - { 1.5f, 15, 0.5f, -0.1f }, - { 2.2f, 17, 0.6f, 0.0f }, - { 2.8f, 15, 0.5f, 0.1f }, - { 3.5f, 15, 0.6f, 0.0f }, + { 0.00f, 15, 0.6f, 0.0f }, + { 0.06f, 15, 0.5f, 0.0f }, + { 0.12f, 15, 0.6f, 0.1f }, + { 0.25f, 16, 0.6f, 0.0f }, + { 0.38f, 15, 0.5f, -0.1f }, + { 0.56f, 19, 0.6f, 0.0f }, + { 0.69f, 15, 0.5f, 0.1f }, + { 0.88f, 15, 0.6f, 0.0f }, }; const TrackerPattern g_tracker_patterns[] = { - { PATTERN_EVENTS_kick_basic, 3, 4.0f }, // kick_basic - { PATTERN_EVENTS_kick_varied, 3, 4.0f }, // kick_varied - { PATTERN_EVENTS_kick_dense, 8, 4.0f }, // kick_dense - { PATTERN_EVENTS_snare_basic, 2, 4.0f }, // snare_basic - { PATTERN_EVENTS_snare_varied, 2, 4.0f }, // snare_varied - { PATTERN_EVENTS_snare_dense, 6, 4.0f }, // snare_dense - { PATTERN_EVENTS_hihat_basic, 8, 4.0f }, // hihat_basic - { PATTERN_EVENTS_hihat_varied, 8, 4.0f }, // hihat_varied - { PATTERN_EVENTS_crash, 1, 4.0f }, // crash - { PATTERN_EVENTS_ride, 1, 4.0f }, // ride - { PATTERN_EVENTS_ride_fast, 8, 4.0f }, // ride_fast - { PATTERN_EVENTS_splash, 1, 4.0f }, // splash - { PATTERN_EVENTS_bass_e_soft, 2, 4.0f }, // bass_e_soft - { PATTERN_EVENTS_bass_e, 5, 4.0f }, // bass_e - { PATTERN_EVENTS_bass_eg, 4, 4.0f }, // bass_eg - { PATTERN_EVENTS_bass_progression, 4, 4.0f }, // bass_progression - { PATTERN_EVENTS_bass_synco_1, 7, 4.0f }, // bass_synco_1 - { PATTERN_EVENTS_bass_synco_2, 8, 4.0f }, // bass_synco_2 - { PATTERN_EVENTS_bass_synco_3, 8, 4.0f }, // bass_synco_3 + { PATTERN_EVENTS_kick_basic, 2, 1.00f }, // kick_basic + { PATTERN_EVENTS_kick_varied, 2, 1.00f }, // kick_varied + { PATTERN_EVENTS_kick_dense, 8, 1.00f }, // kick_dense + { PATTERN_EVENTS_snare_basic, 2, 1.00f }, // snare_basic + { PATTERN_EVENTS_snare_varied, 2, 1.00f }, // snare_varied + { PATTERN_EVENTS_snare_dense, 3, 1.00f }, // snare_dense + { PATTERN_EVENTS_hihat_basic, 8, 1.00f }, // hihat_basic + { PATTERN_EVENTS_hihat_varied, 8, 1.00f }, // hihat_varied + { PATTERN_EVENTS_crash, 1, 1.00f }, // crash + { PATTERN_EVENTS_ride, 1, 1.00f }, // ride + { PATTERN_EVENTS_ride_fast, 8, 1.00f }, // ride_fast + { PATTERN_EVENTS_splash, 1, 1.00f }, // splash + { PATTERN_EVENTS_bass_e_soft, 2, 1.00f }, // bass_e_soft + { PATTERN_EVENTS_bass_e, 5, 1.00f }, // bass_e + { PATTERN_EVENTS_bass_eg, 4, 1.00f }, // bass_eg + { PATTERN_EVENTS_bass_progression, 4, 1.00f }, // bass_progression + { PATTERN_EVENTS_bass_synco_1, 7, 1.00f }, // bass_synco_1 + { PATTERN_EVENTS_bass_synco_2, 8, 1.00f }, // bass_synco_2 + { PATTERN_EVENTS_bass_synco_3, 8, 1.00f }, // bass_synco_3 }; const uint32_t g_tracker_patterns_count = 19; @@ -204,88 +201,88 @@ static const TrackerPatternTrigger SCORE_TRIGGERS[] = { { 0.0f, 8 }, { 0.0f, 0 }, { 0.0f, 6 }, + { 0.5f, 0 }, + { 0.5f, 3 }, + { 0.5f, 6 }, + { 1.0f, 9 }, + { 1.0f, 1 }, + { 1.0f, 3 }, + { 1.0f, 7 }, + { 1.5f, 1 }, + { 1.5f, 4 }, + { 1.5f, 7 }, + { 2.0f, 11 }, { 2.0f, 0 }, { 2.0f, 3 }, { 2.0f, 6 }, - { 4.0f, 9 }, - { 4.0f, 1 }, - { 4.0f, 3 }, + { 2.0f, 12 }, + { 2.5f, 1 }, + { 2.5f, 4 }, + { 2.5f, 7 }, + { 2.5f, 12 }, + { 3.0f, 9 }, + { 3.0f, 0 }, + { 3.0f, 3 }, + { 3.0f, 6 }, + { 3.0f, 13 }, + { 3.5f, 1 }, + { 3.5f, 4 }, + { 3.5f, 7 }, + { 3.5f, 14 }, + { 4.0f, 8 }, + { 4.0f, 2 }, + { 4.0f, 5 }, { 4.0f, 7 }, - { 6.0f, 1 }, - { 6.0f, 4 }, + { 4.0f, 13 }, + { 4.5f, 2 }, + { 4.5f, 5 }, + { 4.5f, 6 }, + { 4.5f, 15 }, + { 5.0f, 9 }, + { 5.0f, 2 }, + { 5.0f, 5 }, + { 5.0f, 7 }, + { 5.0f, 13 }, + { 5.5f, 2 }, + { 5.5f, 5 }, + { 5.5f, 6 }, + { 5.5f, 14 }, + { 6.0f, 11 }, + { 6.0f, 2 }, + { 6.0f, 5 }, { 6.0f, 7 }, - { 8.0f, 11 }, - { 8.0f, 0 }, - { 8.0f, 3 }, - { 8.0f, 6 }, - { 8.0f, 12 }, - { 10.0f, 1 }, - { 10.0f, 4 }, - { 10.0f, 7 }, - { 10.0f, 12 }, - { 12.0f, 9 }, - { 12.0f, 0 }, - { 12.0f, 3 }, - { 12.0f, 6 }, - { 12.0f, 13 }, - { 14.0f, 1 }, - { 14.0f, 4 }, - { 14.0f, 7 }, - { 14.0f, 14 }, - { 16.0f, 8 }, - { 16.0f, 2 }, - { 16.0f, 5 }, - { 16.0f, 7 }, - { 16.0f, 13 }, - { 18.0f, 2 }, - { 18.0f, 5 }, - { 18.0f, 6 }, - { 18.0f, 15 }, - { 20.0f, 9 }, - { 20.0f, 2 }, - { 20.0f, 5 }, - { 20.0f, 7 }, - { 20.0f, 13 }, - { 22.0f, 2 }, - { 22.0f, 5 }, - { 22.0f, 6 }, - { 22.0f, 14 }, - { 24.0f, 11 }, - { 24.0f, 2 }, - { 24.0f, 5 }, - { 24.0f, 7 }, - { 24.0f, 15 }, - { 26.0f, 2 }, - { 26.0f, 5 }, - { 26.0f, 6 }, - { 26.0f, 13 }, - { 28.0f, 10 }, - { 28.0f, 0 }, - { 28.0f, 4 }, - { 28.0f, 7 }, - { 28.0f, 14 }, - { 30.0f, 1 }, - { 30.0f, 3 }, - { 30.0f, 6 }, - { 30.0f, 15 }, - { 31.0f, 6 }, - { 32.0f, 8 }, - { 32.0f, 10 }, - { 32.0f, 2 }, - { 32.0f, 5 }, - { 32.0f, 7 }, - { 32.0f, 16 }, - { 34.0f, 10 }, - { 34.0f, 2 }, - { 34.0f, 5 }, - { 34.0f, 6 }, - { 34.0f, 17 }, - { 36.0f, 10 }, - { 36.0f, 2 }, - { 36.0f, 5 }, - { 36.0f, 7 }, - { 36.0f, 18 }, - { 38.0f, 8 }, + { 6.0f, 15 }, + { 6.5f, 2 }, + { 6.5f, 5 }, + { 6.5f, 6 }, + { 6.5f, 13 }, + { 7.0f, 10 }, + { 7.0f, 0 }, + { 7.0f, 4 }, + { 7.0f, 7 }, + { 7.0f, 14 }, + { 7.5f, 1 }, + { 7.5f, 3 }, + { 7.5f, 6 }, + { 7.5f, 15 }, + { 7.8f, 6 }, + { 8.0f, 8 }, + { 8.0f, 10 }, + { 8.0f, 2 }, + { 8.0f, 5 }, + { 8.0f, 7 }, + { 8.0f, 16 }, + { 8.5f, 10 }, + { 8.5f, 2 }, + { 8.5f, 5 }, + { 8.5f, 6 }, + { 8.5f, 17 }, + { 9.0f, 10 }, + { 9.0f, 2 }, + { 9.0f, 5 }, + { 9.0f, 7 }, + { 9.0f, 18 }, + { 9.5f, 8 }, }; const TrackerScore g_tracker_score = { @@ -295,19 +292,19 @@ const TrackerScore g_tracker_score = { // ============================================================ // RESOURCE USAGE ANALYSIS (for synth.h configuration) // ============================================================ -// Total samples: 19 (15 assets + 4 generated notes) +// Total samples: 20 (15 assets + 5 generated notes) // Max simultaneous pattern triggers: 6 // Estimated max polyphony: 24 voices // // REQUIRED (minimum to avoid pool exhaustion): // MAX_VOICES: 24 -// MAX_SPECTROGRAMS: 111 (no caching) +// MAX_SPECTROGRAMS: 135 (no caching) // // RECOMMENDED (with 50% safety margin): // MAX_VOICES: 48 -// MAX_SPECTROGRAMS: 166 (no caching) +// MAX_SPECTROGRAMS: 202 (no caching) // // NOTE: With spectrogram caching by note parameters, -// MAX_SPECTROGRAMS could be reduced to ~19 +// MAX_SPECTROGRAMS could be reduced to ~20 // ============================================================ diff --git a/src/generated/test_demo_music.cc b/src/generated/test_demo_music.cc index 3fdd2a1..f77984e 100644 --- a/src/generated/test_demo_music.cc +++ b/src/generated/test_demo_music.cc @@ -20,36 +20,36 @@ const AssetId g_tracker_sample_assets[] = { }; 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 }, + { 0.00f, 0, 1.0f, 0.0f }, + { 0.00f, 3, 0.5f, 0.0f }, + { 0.25f, 1, 0.9f, 0.0f }, + { 0.50f, 0, 1.0f, 0.0f }, + { 0.75f, 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 }, + { 0.00f, 0, 1.0f, 0.0f }, + { 0.00f, 2, 0.9f, 0.0f }, + { 0.00f, 3, 0.5f, 0.0f }, + { 0.25f, 1, 0.9f, 0.0f }, + { 0.50f, 0, 1.0f, 0.0f }, + { 0.75f, 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 + { PATTERN_EVENTS_drums_basic, 5, 1.00f }, // drums_basic + { PATTERN_EVENTS_drums_with_crash, 6, 1.00f }, // 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 }, + { 1.0f, 0 }, + { 2.0f, 1 }, + { 3.0f, 0 }, + { 4.0f, 0 }, + { 5.0f, 0 }, + { 6.0f, 1 }, + { 7.0f, 0 }, }; const TrackerScore g_tracker_score = { diff --git a/src/gpu/effects/flash_effect.cc b/src/gpu/effects/flash_effect.cc index 217a7bb..5aebe2d 100644 --- a/src/gpu/effects/flash_effect.cc +++ b/src/gpu/effects/flash_effect.cc @@ -15,7 +15,7 @@ FlashEffect::FlashEffect(const GpuContext& ctx) struct Uniforms { flash_intensity: f32, - _pad0: f32, + intensity: f32, _pad1: f32, _pad2: f32, }; @@ -42,7 +42,9 @@ FlashEffect::FlashEffect(const GpuContext& ctx) let color = textureSample(inputTexture, inputSampler, input.uv); // Add white flash: blend towards white based on flash intensity let white = vec3<f32>(1.0, 1.0, 1.0); - let flashed = mix(color.rgb, white, uniforms.flash_intensity); + let green = vec3<f32>(0.0, 1.0, 0.0); + var flashed = mix(color.rgb, green, uniforms.intensity); + if (input.uv.y > .5) { flashed = mix(color.rgb, white, uniforms.flash_intensity); } return vec4<f32>(flashed, color.a); } )"; @@ -68,9 +70,9 @@ void FlashEffect::render(WGPURenderPassEncoder pass, float time, float beat, } // Exponential decay - flash_intensity_ *= 0.85f; + flash_intensity_ *= 0.98f; - float uniforms[4] = {flash_intensity_, 0.0f, 0.0f, 0.0f}; + float uniforms[4] = {flash_intensity_, intensity, 0.0f, 0.0f}; wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, uniforms, sizeof(uniforms)); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); diff --git a/src/gpu/effects/post_process_helper.cc b/src/gpu/effects/post_process_helper.cc index db89d77..0a2ac22 100644 --- a/src/gpu/effects/post_process_helper.cc +++ b/src/gpu/effects/post_process_helper.cc @@ -1,6 +1,7 @@ // This file is part of the 64k demo project. // It implements helper functions for post-processing effects. +#include "post_process_helper.h" #include "../demo_effects.h" #include "gpu/gpu.h" #include <cstring> @@ -18,15 +19,15 @@ WGPURenderPipeline create_post_process_pipeline(WGPUDevice device, wgpuDeviceCreateShaderModule(device, &shader_desc); WGPUBindGroupLayoutEntry bgl_entries[3] = {}; - bgl_entries[0].binding = 0; + bgl_entries[0].binding = PP_BINDING_SAMPLER; bgl_entries[0].visibility = WGPUShaderStage_Fragment; bgl_entries[0].sampler.type = WGPUSamplerBindingType_Filtering; - bgl_entries[1].binding = 1; + bgl_entries[1].binding = PP_BINDING_TEXTURE; bgl_entries[1].visibility = WGPUShaderStage_Fragment; bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; - bgl_entries[2].binding = 2; - bgl_entries[2].visibility = WGPUShaderStage_Fragment; + bgl_entries[2].binding = PP_BINDING_UNIFORMS; + bgl_entries[2].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; bgl_entries[2].buffer.type = WGPUBufferBindingType_Uniform; WGPUBindGroupLayoutDescriptor bgl_desc = {}; @@ -74,11 +75,11 @@ void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline, sd.maxAnisotropy = 1; WGPUSampler sampler = wgpuDeviceCreateSampler(device, &sd); WGPUBindGroupEntry bge[3] = {}; - bge[0].binding = 0; + bge[0].binding = PP_BINDING_SAMPLER; bge[0].sampler = sampler; - bge[1].binding = 1; + bge[1].binding = PP_BINDING_TEXTURE; bge[1].textureView = input_view; - bge[2].binding = 2; + bge[2].binding = PP_BINDING_UNIFORMS; bge[2].buffer = uniforms.buffer; bge[2].size = uniforms.size; WGPUBindGroupDescriptor bgd = { diff --git a/src/gpu/effects/post_process_helper.h b/src/gpu/effects/post_process_helper.h index 1986ff3..45757cf 100644 --- a/src/gpu/effects/post_process_helper.h +++ b/src/gpu/effects/post_process_helper.h @@ -5,7 +5,13 @@ #include "gpu/gpu.h" +// Standard post-process bind group layout (group 0): +#define PP_BINDING_SAMPLER 0 // Sampler for input texture +#define PP_BINDING_TEXTURE 1 // Input texture (previous render pass) +#define PP_BINDING_UNIFORMS 2 // Custom uniforms buffer + // Helper to create a standard post-processing pipeline +// Uniforms are accessible to both vertex and fragment shaders WGPURenderPipeline create_post_process_pipeline(WGPUDevice device, WGPUTextureFormat format, const char* shader_code); diff --git a/src/main.cc b/src/main.cc index 0e6fd71..89e21f1 100644 --- a/src/main.cc +++ b/src/main.cc @@ -115,6 +115,7 @@ int main(int argc, char** argv) { } else { g_tempo_scale = 1.0f; // Reset to normal } + g_tempo_scale = 1.0f; #if !defined(STRIP_ALL) // Debug output when tempo changes significantly diff --git a/src/test_demo.cc b/src/test_demo.cc index 9635f88..491968c 100644 --- a/src/test_demo.cc +++ b/src/test_demo.cc @@ -23,6 +23,7 @@ class PeakMeterEffect : public PostProcessEffect { public: PeakMeterEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { + // Use standard post-process binding macros const char* shader_code = R"( struct VertexOutput { @builtin(position) position: vec4<f32>, @@ -43,6 +44,7 @@ class PeakMeterEffect : public PostProcessEffect { @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), @@ -55,30 +57,23 @@ class PeakMeterEffect : public PostProcessEffect { @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - let color = textureSample(inputTexture, inputSampler, input.uv); - - // Draw red horizontal bar in middle of screen - // Bar height: 5% of screen height - // Bar width: proportional to peak_value (0.0 to 1.0) - let bar_height = 0.05; - let bar_center_y = 0.5; - let bar_y_min = bar_center_y - bar_height * 0.5; - let bar_y_max = bar_center_y + bar_height * 0.5; - - // Bar extends from left (0.0) to peak_value position - let bar_x_max = uniforms.peak_value; - - // Check if current pixel is inside the bar + // 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_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) { - // Red bar - return vec4<f32>(1.0, 0.0, 0.0, 1.0); - } else { - // Original color - return color; + let uv_x = (input.uv.x - bar_x_min) / (bar_x_max - bar_x_min); + let factor = step(uv_x, uniforms.peak_value); + 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); } )"; @@ -102,7 +97,7 @@ class PeakMeterEffect : public PostProcessEffect { wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Full-screen triangle } }; @@ -316,7 +311,9 @@ int main(int argc, char** argv) { if (peak_log) { if (log_peaks_fine) { // Log every frame for fine-grained analysis - fprintf(peak_log, "%d %.6f %.6f %d\n", frame_number, audio_time, raw_peak, beat_number); + // 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, audio_time, raw_peak); @@ -350,7 +347,6 @@ int main(int argc, char** argv) { printf("Peak log written to '%s'\n", log_peaks_file); } #endif - audio_shutdown(); gpu_shutdown(); platform_shutdown(&platform_state); diff --git a/tools/track_visualizer/README.md b/tools/track_visualizer/README.md new file mode 100644 index 0000000..a036dbd --- /dev/null +++ b/tools/track_visualizer/README.md @@ -0,0 +1,82 @@ +# Music Track Visualizer + +A simple HTML-based visualizer for `.track` music files. + +## Features + +- **Load .track files** via file input button +- **Zoomable timeline** with horizontal and vertical zoom controls +- **Pan/scroll** by clicking and dragging on the canvas +- **Color-coded patterns** with deterministic colors based on pattern names +- **Sample ticks** displayed within each pattern box (vertical marks showing when samples trigger) +- **Stack-based layout** prevents overlapping patterns automatically +- **Time ruler** at the top with second markers +- **Pattern duration** calculated from max beat time in each pattern + +## Usage + +1. Open `index.html` in a web browser +2. Click "Choose File" and select a `.track` file (e.g., `assets/music.track`) +3. Use the zoom controls to adjust horizontal and vertical scale +4. Click and drag on the canvas to pan around +5. Use mouse wheel to zoom (Shift+wheel for vertical zoom) + +## Controls + +| Action | Method | +|--------|--------| +| **Load file** | Click "Choose File" button | +| **Zoom in/out** | Use + / − buttons or mouse wheel | +| **Vertical zoom** | Use vertical zoom buttons or Shift+wheel | +| **Pan** | Click and drag on canvas | +| **Reset zoom** | Click "Reset" buttons | + +## Track File Format + +The visualizer parses `.track` files with the following structure: + +``` +# Comments start with # + +SAMPLE ASSET_KICK_1 +SAMPLE ASSET_SNARE_1 + +PATTERN pattern_name + beat, sample_id, volume, pan + 0.0, ASSET_KICK_1, 1.0, 0.0 + 2.0, ASSET_SNARE_1, 0.9, 0.1 + +SCORE + time, pattern_name + 0.0, pattern_name + 4.0, pattern_name +``` + +### Elements Visualized + +- **Pattern boxes**: Color-coded rectangles representing each pattern trigger +- **Sample ticks**: Vertical marks inside boxes showing when samples play +- **Volume indicators**: Tick height varies with sample volume +- **Time labels**: Start time displayed in each pattern box +- **Stack levels**: Patterns automatically stack to avoid visual overlap + +## Implementation Details + +- **Stack-based layout**: Patterns are automatically arranged in vertical stacks to prevent overlaps +- **Deterministic colors**: Pattern colors are generated from pattern name hashes for consistency +- **Responsive canvas**: Canvas size adapts to track duration and pattern count +- **Efficient rendering**: Full redraw on zoom/pan changes + +## Keyboard Shortcuts + +None currently - all controls via mouse/buttons. + +## Future Enhancements + +Potential additions: +- Beat grid overlay +- Pattern highlighting on hover +- Sample name tooltips +- Export to image +- Playback cursor +- Edit mode (move/resize patterns) diff --git a/tools/track_visualizer/index.html b/tools/track_visualizer/index.html new file mode 100644 index 0000000..4a613ec --- /dev/null +++ b/tools/track_visualizer/index.html @@ -0,0 +1,537 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Music Track Visualizer</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #1e1e1e; + color: #d4d4d4; + overflow: hidden; + } + #controls { + padding: 15px; + background: #2d2d2d; + border-bottom: 1px solid #3e3e3e; + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; + } + button, input[type="file"] { + padding: 8px 16px; + background: #0e639c; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + } + button:hover { + background: #1177bb; + } + input[type="file"] { + padding: 6px 12px; + } + .zoom-controls { + display: flex; + gap: 8px; + align-items: center; + } + .zoom-controls button { + padding: 6px 12px; + min-width: 40px; + } + .zoom-label { + font-size: 14px; + color: #9cdcfe; + } + #canvas-container { + position: relative; + width: 100%; + height: calc(100vh - 70px); + overflow: auto; + background: #1e1e1e; + } + #timeline-canvas { + display: block; + cursor: grab; + } + #timeline-canvas:active { + cursor: grabbing; + } + #info { + position: fixed; + bottom: 10px; + right: 10px; + background: rgba(45, 45, 45, 0.95); + padding: 10px 15px; + border-radius: 4px; + font-size: 12px; + border: 1px solid #3e3e3e; + max-width: 300px; + } + #info div { + margin: 3px 0; + } + .info-label { + color: #9cdcfe; + font-weight: bold; + } + #filename { + color: #4ec9b0; + margin-left: 10px; + font-weight: bold; + } + </style> +</head> +<body> + <div id="controls"> + <input type="file" id="file-input" accept=".track" /> + <span id="filename"></span> + <div class="zoom-controls"> + <span class="zoom-label">Zoom:</span> + <button id="zoom-out">−</button> + <button id="zoom-reset">Reset</button> + <button id="zoom-in">+</button> + </div> + <div class="zoom-controls"> + <span class="zoom-label">Vertical:</span> + <button id="v-zoom-out">−</button> + <button id="v-zoom-reset">Reset</button> + <button id="v-zoom-in">+</button> + </div> + </div> + <div id="canvas-container"> + <canvas id="timeline-canvas"></canvas> + </div> + <div id="info"> + <div><span class="info-label">Pan:</span> Click and drag</div> + <div><span class="info-label">Zoom:</span> Mouse wheel</div> + <div><span class="info-label">Scroll:</span> Shift + wheel</div> + <div><span class="info-label">Patterns:</span> <span id="pattern-count">0</span></div> + <div><span class="info-label">Duration:</span> <span id="duration">0s</span></div> + </div> + + <script> + const canvas = document.getElementById('timeline-canvas'); + const ctx = canvas.getContext('2d'); + const container = document.getElementById('canvas-container'); + const fileInput = document.getElementById('file-input'); + const filenameSpan = document.getElementById('filename'); + + // State + let trackData = null; + let zoomLevel = 1.0; + let verticalZoom = 1.0; + let offsetX = 0; + let offsetY = 0; + let isDragging = false; + let lastMouseX = 0; + let lastMouseY = 0; + + // Constants + const PIXELS_PER_SECOND = 80; // Base scale + const PATTERN_HEIGHT = 40; // Base height + const PATTERN_SPACING = 5; + const TIMELINE_TOP = 80; + const LEFT_MARGIN = 120; + const TICK_HEIGHT = 8; + + // Pattern colors (deterministic based on name hash) + function getPatternColor(patternName) { + let hash = 0; + for (let i = 0; i < patternName.length; i++) { + hash = patternName.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = Math.abs(hash % 360); + return `hsl(${hue}, 70%, 55%)`; + } + + // Parse .track file + function parseTrackFile(content) { + const lines = content.split('\n'); + const patterns = {}; + const score = []; + let currentPattern = null; + let currentPatternLength = 1.0; // Default: 1 unit + let inScore = false; + let bpm = 120.0; // Default BPM + + for (let line of lines) { + line = line.trim(); + + // Skip comments and empty lines + if (line.startsWith('#') || line.length === 0) continue; + + // BPM directive + if (line.startsWith('BPM ')) { + bpm = parseFloat(line.substring(4).trim()); + continue; + } + + // Pattern definition with optional LENGTH + if (line.startsWith('PATTERN ')) { + const tokens = line.substring(8).trim().split(/\s+/); + currentPattern = tokens[0]; + currentPatternLength = 1.0; // Default + + // Check for LENGTH parameter + const lengthIdx = tokens.indexOf('LENGTH'); + if (lengthIdx !== -1 && lengthIdx + 1 < tokens.length) { + currentPatternLength = parseFloat(tokens[lengthIdx + 1]); + } + + patterns[currentPattern] = { + events: [], + unitLength: currentPatternLength + }; + inScore = false; + continue; + } + + // Score section + if (line === 'SCORE') { + inScore = true; + currentPattern = null; + continue; + } + + // Pattern events (unit_time, sample, volume, pan) + if (currentPattern && !inScore) { + const parts = line.split(',').map(s => s.trim()); + if (parts.length >= 2) { + patterns[currentPattern].events.push({ + unitTime: parseFloat(parts[0]), + sample: parts[1], + volume: parts.length > 2 ? parseFloat(parts[2]) : 1.0, + pan: parts.length > 3 ? parseFloat(parts[3]) : 0.0 + }); + } + } + + // Score entries (unit_time, pattern_name) + if (inScore) { + const parts = line.split(',').map(s => s.trim()); + if (parts.length >= 2) { + score.push({ + unitTime: parseFloat(parts[0]), + pattern: parts[1] + }); + } + } + } + + return { patterns, score, bpm }; + } + + // Convert unit-less time to seconds + // 1 unit = 4 beats, at given BPM + function unitsToSeconds(units, bpm) { + const BEATS_PER_UNIT = 4.0; + const unitDurationSec = (BEATS_PER_UNIT / bpm) * 60.0; + return units * unitDurationSec; + } + + // Get pattern duration in seconds + function getPatternDuration(pattern, bpm) { + if (!pattern || !pattern.unitLength) return unitsToSeconds(1.0, bpm); + return unitsToSeconds(pattern.unitLength, bpm); + } + + // Draw timeline + function drawTimeline() { + if (!trackData) return; + + const { patterns, score, bpm } = trackData; + + // Find max time for canvas sizing + const maxTime = score.length > 0 + ? Math.max(...score.map(s => unitsToSeconds(s.unitTime, bpm) + getPatternDuration(patterns[s.pattern], bpm))) + : 60; + + // Update canvas size + canvas.width = LEFT_MARGIN + (maxTime * PIXELS_PER_SECOND * zoomLevel) + 100; + canvas.height = TIMELINE_TOP + (score.length * (PATTERN_HEIGHT * verticalZoom + PATTERN_SPACING)) + 100; + + // Clear canvas + ctx.fillStyle = '#1e1e1e'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw time ruler + drawTimeRuler(maxTime); + + // Draw beat background + drawBeatBackground(maxTime); + + // Group score entries by time for stacking + const stackedPatterns = []; + for (const entry of score) { + const startTime = unitsToSeconds(entry.unitTime, bpm); + const duration = getPatternDuration(patterns[entry.pattern], bpm); + const endTime = startTime + duration; + + // Find stack level (avoid overlaps) + let stackLevel = 0; + while (true) { + const conflicts = stackedPatterns.filter(p => + p.stackLevel === stackLevel && + !(endTime <= p.startTime || startTime >= p.endTime) + ); + if (conflicts.length === 0) break; + stackLevel++; + } + + const pattern = patterns[entry.pattern]; + stackedPatterns.push({ + patternName: entry.pattern, + startTime, + endTime, + duration, + stackLevel, + unitLength: pattern ? pattern.unitLength : 1.0, + events: pattern ? pattern.events : [] + }); + } + + // Draw pattern boxes + for (const item of stackedPatterns) { + drawPatternBox(item); + } + + // Update info + document.getElementById('pattern-count').textContent = Object.keys(patterns).length; + document.getElementById('duration').textContent = `${maxTime.toFixed(1)}s`; + } + + // Draw time ruler + function drawTimeRuler(maxTime) { + ctx.strokeStyle = '#3e3e3e'; + ctx.fillStyle = '#d4d4d4'; + ctx.font = '12px monospace'; + ctx.textAlign = 'center'; + + const rulerY = TIMELINE_TOP - 20; + + // Draw ruler line + ctx.beginPath(); + ctx.moveTo(LEFT_MARGIN, rulerY); + ctx.lineTo(LEFT_MARGIN + maxTime * PIXELS_PER_SECOND * zoomLevel, rulerY); + ctx.stroke(); + + // Draw time markers + const interval = zoomLevel < 0.5 ? 10 : zoomLevel < 1.0 ? 5 : 2; + for (let t = 0; t <= maxTime; t += interval) { + const x = LEFT_MARGIN + t * PIXELS_PER_SECOND * zoomLevel; + ctx.beginPath(); + ctx.moveTo(x, rulerY - 5); + ctx.lineTo(x, rulerY + 5); + ctx.stroke(); + + ctx.fillText(`${t}s`, x, rulerY - 10); + } + } + + // Draw beat background (alternating colored rectangles) + function drawBeatBackground(maxTime) { + const beatsPerSecond = 2; // 120 BPM = 2 beats per second + const beatWidth = (PIXELS_PER_SECOND * zoomLevel) / beatsPerSecond; + const totalBeats = Math.ceil(maxTime * beatsPerSecond); + + // Alternating colors for beats + const color1 = '#2a2a2a'; // Slightly lighter than background + const color2 = '#1e1e1e'; // Background color + + for (let beat = 0; beat < totalBeats; beat++) { + const x = LEFT_MARGIN + beat * beatWidth; + const isEvenBeat = beat % 2 === 0; + + ctx.fillStyle = isEvenBeat ? color1 : color2; + // Draw from TIMELINE_TOP to avoid covering the time ruler + ctx.fillRect(x, TIMELINE_TOP, beatWidth, canvas.height - TIMELINE_TOP); + } + } + + // Draw pattern box + function drawPatternBox(item) { + const x = LEFT_MARGIN + item.startTime * PIXELS_PER_SECOND * zoomLevel; + const y = TIMELINE_TOP + item.stackLevel * (PATTERN_HEIGHT * verticalZoom + PATTERN_SPACING); + const width = item.duration * PIXELS_PER_SECOND * zoomLevel; + const height = PATTERN_HEIGHT * verticalZoom; + + const color = getPatternColor(item.patternName); + + // Draw box + ctx.fillStyle = color + '33'; // Semi-transparent + ctx.fillRect(x, y, width, height); + + // Draw border + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + + // Draw beat grid lines + ctx.strokeStyle = '#ffffff33'; // Semi-transparent white + ctx.lineWidth = 1; + const beatsPerSecond = 2; // 120 BPM = 2 beats per second + const beatWidth = (PIXELS_PER_SECOND * zoomLevel) / beatsPerSecond; + const numBeats = Math.ceil(item.duration * beatsPerSecond); + + for (let beat = 1; beat < numBeats; beat++) { + const beatX = x + beat * beatWidth; + ctx.beginPath(); + ctx.moveTo(beatX, y); + ctx.lineTo(beatX, y + height); + ctx.stroke(); + + // Beat number label (small) + if (verticalZoom > 0.8) { + ctx.fillStyle = '#ffffff55'; + ctx.font = `${Math.min(9, 8 * verticalZoom)}px monospace`; + ctx.textAlign = 'center'; + ctx.fillText(`${beat}`, beatX, y + 12); + } + } + + // Draw pattern name + ctx.fillStyle = '#ffffff'; + ctx.font = `bold ${Math.min(14, 12 * verticalZoom)}px sans-serif`; + ctx.textAlign = 'left'; + ctx.fillText(item.patternName, x + 5, y + height / 2 - 5); + + // Draw time label + ctx.font = `${Math.min(11, 10 * verticalZoom)}px monospace`; + ctx.fillStyle = '#9cdcfe'; + ctx.fillText(`${item.startTime.toFixed(1)}s`, x + 5, y + height / 2 + 10); + + // Draw sample ticks + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + for (const event of item.events) { + // Convert unit_time to position within pattern + // event.unitTime ranges from 0 to item.unitLength + // width represents the full pattern duration + const tickX = x + (event.unitTime / item.unitLength) * width; + const tickY = y + height - TICK_HEIGHT * verticalZoom; + + // Vertical tick mark + ctx.beginPath(); + ctx.moveTo(tickX, tickY); + ctx.lineTo(tickX, y + height - 2); + ctx.stroke(); + + // Volume indicator (height of tick varies with volume) + const volumeHeight = TICK_HEIGHT * verticalZoom * event.volume; + ctx.fillStyle = color; + ctx.fillRect(tickX - 1, tickY, 3, volumeHeight); + } + } + + // Load file + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + + filenameSpan.textContent = file.name; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + trackData = parseTrackFile(event.target.result); + zoomLevel = 1.0; + verticalZoom = 1.0; + offsetX = 0; + offsetY = 0; + drawTimeline(); + } catch (err) { + alert('Error parsing track file: ' + err.message); + console.error(err); + } + }; + reader.readAsText(file); + }); + + // Zoom controls + document.getElementById('zoom-in').addEventListener('click', () => { + zoomLevel = Math.min(zoomLevel * 1.2, 10); + drawTimeline(); + }); + + document.getElementById('zoom-out').addEventListener('click', () => { + zoomLevel = Math.max(zoomLevel / 1.2, 0.1); + drawTimeline(); + }); + + document.getElementById('zoom-reset').addEventListener('click', () => { + zoomLevel = 1.0; + drawTimeline(); + }); + + document.getElementById('v-zoom-in').addEventListener('click', () => { + verticalZoom = Math.min(verticalZoom * 1.2, 5); + drawTimeline(); + }); + + document.getElementById('v-zoom-out').addEventListener('click', () => { + verticalZoom = Math.max(verticalZoom / 1.2, 0.5); + drawTimeline(); + }); + + document.getElementById('v-zoom-reset').addEventListener('click', () => { + verticalZoom = 1.0; + drawTimeline(); + }); + + // Mouse wheel zoom/scroll + container.addEventListener('wheel', (e) => { + if (!trackData) return; + + if (e.shiftKey) { + // Horizontal scroll (use deltaX when Shift is pressed) + e.preventDefault(); + container.scrollLeft += e.deltaX || e.deltaY; + } else { + // Horizontal zoom + e.preventDefault(); + const delta = e.deltaY < 0 ? 1.1 : 0.9; + zoomLevel = Math.max(0.1, Math.min(10, zoomLevel * delta)); + drawTimeline(); + } + }, { passive: false }); + + // Panning + canvas.addEventListener('mousedown', (e) => { + isDragging = true; + lastMouseX = e.clientX; + lastMouseY = e.clientY; + }); + + window.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + const dx = e.clientX - lastMouseX; + const dy = e.clientY - lastMouseY; + + container.scrollLeft -= dx; + container.scrollTop -= dy; + + lastMouseX = e.clientX; + lastMouseY = e.clientY; + }); + + window.addEventListener('mouseup', () => { + isDragging = false; + }); + + // Initial draw + drawTimeline(); + </script> +</body> +</html> diff --git a/tools/tracker_compiler.cc b/tools/tracker_compiler.cc index 59d4187..314099d 100644 --- a/tools/tracker_compiler.cc +++ b/tools/tracker_compiler.cc @@ -99,7 +99,7 @@ struct Sample { }; struct Event { - float beat; + float unit_time; // Unit-less time within pattern std::string sample_name; float volume, pan; }; @@ -107,6 +107,7 @@ struct Event { struct Pattern { std::string name; std::vector<Event> events; + float unit_length; // Pattern duration in units (default 1.0 for 4-beat patterns) }; struct Trigger { @@ -177,6 +178,14 @@ int main(int argc, char** argv) { } else if (cmd == "PATTERN") { Pattern p; ss >> p.name; + p.unit_length = 1.0f; // Default: 1 unit = 4 beats + // Check for optional LENGTH parameter + std::string next_token; + if (ss >> next_token) { + if (next_token == "LENGTH") { + ss >> p.unit_length; + } + } current_section = "PATTERN:" + p.name; patterns.push_back(p); pattern_map[p.name] = patterns.size() - 1; @@ -184,18 +193,18 @@ int main(int argc, char** argv) { current_section = "SCORE"; } else { if (current_section.rfind("PATTERN:", 0) == 0) { - // Parse event: beat, sample, vol, pan + // Parse event: unit_time, sample, vol, pan Event e; - float beat; + float unit_time; std::stringstream ss2(line); - ss2 >> beat; + ss2 >> unit_time; char comma; ss2 >> comma; std::string sname; ss2 >> sname; if (sname.back() == ',') sname.pop_back(); - e.beat = beat; + e.unit_time = unit_time; e.sample_name = sname; ss2 >> e.volume >> comma >> e.pan; @@ -276,7 +285,7 @@ int main(int argc, char** argv) { // For now, assume sample_map is used for both generated and asset // samples. This will need refinement if asset samples are not in // sample_map directly. - fprintf(out_file, " { %.1ff, %d, %.1ff, %.1ff },\n", e.beat, + fprintf(out_file, " { %.2ff, %d, %.1ff, %.1ff },\n", e.unit_time, sample_map[e.sample_name], e.volume, e.pan); } fprintf(out_file, "};\n"); @@ -285,8 +294,8 @@ int main(int argc, char** argv) { fprintf(out_file, "const TrackerPattern g_tracker_patterns[] = {\n"); for (const auto& p : patterns) { - fprintf(out_file, " { PATTERN_EVENTS_%s, %zu, 4.0f }, // %s\n", - p.name.c_str(), p.events.size(), p.name.c_str()); + fprintf(out_file, " { PATTERN_EVENTS_%s, %zu, %.2ff }, // %s\n", + p.name.c_str(), p.events.size(), p.unit_length, p.name.c_str()); } fprintf(out_file, "};\n"); fprintf(out_file, "const uint32_t g_tracker_patterns_count = %zu;\n\n", |
