summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt18
-rw-r--r--HANDOFF_2026-02-07_AssetRegeneration.md112
-rw-r--r--HANDOFF_2026-02-07_Final.md55
-rw-r--r--assets/music.track392
-rw-r--r--assets/test_demo.track44
-rw-r--r--assets/test_gantt.seq11
-rw-r--r--convert_track.py77
-rw-r--r--doc/TRACKER.md81
-rwxr-xr-xscripts/test_gantt_html.sh102
-rwxr-xr-xscripts/test_gantt_output.sh70
-rw-r--r--src/audio/tracker.cc23
-rw-r--r--src/audio/tracker.h8
-rw-r--r--src/generated/music_data.cc383
-rw-r--r--src/generated/test_demo_music.cc40
-rw-r--r--src/gpu/effects/flash_effect.cc10
-rw-r--r--src/gpu/effects/post_process_helper.cc15
-rw-r--r--src/gpu/effects/post_process_helper.h6
-rw-r--r--src/main.cc1
-rw-r--r--src/test_demo.cc42
-rw-r--r--tools/track_visualizer/README.md82
-rw-r--r--tools/track_visualizer/index.html537
-rw-r--r--tools/tracker_compiler.cc25
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",