summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/music.track382
-rw-r--r--assets/test_demo.track40
-rw-r--r--convert_track.py77
-rw-r--r--doc/TRACKER.md81
-rw-r--r--src/audio/tracker.cc23
-rw-r--r--src/audio/tracker.h8
-rw-r--r--src/generated/music_data.cc372
-rw-r--r--src/generated/test_demo_music.cc38
-rw-r--r--tools/track_visualizer/index.html71
-rw-r--r--tools/tracker_compiler.cc25
10 files changed, 668 insertions, 449 deletions
diff --git a/assets/music.track b/assets/music.track
index 4159372..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
+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
+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
+PATTERN snare_dense LENGTH 1.0
# 0.5, ASSET_SNARE_3, 0.9, 0.0
- 1.0, ASSET_SNARE_1, 1.1, 0.1
+ 0.25, 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
+ 0.62, 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
+ 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_E3, 0.4, 0.0
- 2.0, NOTE_E3, 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_E3, 0.5, 0.0
- 1.0, NOTE_E3, 0.4, 0.0
- 2.0, NOTE_E3, 0.5, 0.0
- 2.5, NOTE_E3, 0.35, 0.0
- 3.0, NOTE_E3, 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_E3, 0.5, 0.0
- 1.0, NOTE_E3, 0.4, 0.0
- 2.0, NOTE_G3, 0.5, 0.0
- 3.0, NOTE_G3, 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_E3, 0.5, 0.0
- 1.0, NOTE_D3, 0.45, 0.0
- 2.0, NOTE_C2, 0.5, 0.0
- 3.0, NOTE_G3, 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_E3, 0.6, 0.0
- 0.25, NOTE_E3, 0.5, 0.1
- 0.75, NOTE_E3, 0.55, -0.1
- 1.5, NOTE_E3, 0.5, 0.0
- 2.0, NOTE_E3, 0.6, 0.0
- 2.75, NOTE_G3, 0.55, 0.1
- 3.25, NOTE_E3, 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_E3, 0.6, 0.0
- 0.5, NOTE_D3, 0.55, -0.1
- 1.25, NOTE_E3, 0.5, 0.1
- 1.75, NOTE_D3, 0.5, 0.0
- 2.0, NOTE_C2, 0.6, 0.0
- 2.5, NOTE_E3, 0.5, 0.1
- 3.0, NOTE_G3, 0.6, 0.0
- 3.5, NOTE_E3, 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_E3, 0.65, 0.0
- 0.25, NOTE_E3, 0.5, 0.0
- 0.5, NOTE_E3, 0.55, 0.1
- 1.0, NOTE_G3, 0.6, 0.0
- 1.5, NOTE_E3, 0.5, -0.1
- 2.25, NOTE_D2, 0.55, 0.0
- 2.75, NOTE_E3, 0.5, 0.1
- 3.5, NOTE_E3, 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 9d6bdf4..6ae5c67 100644
--- a/assets/test_demo.track
+++ b/assets/test_demo.track
@@ -1,28 +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
- 4.0, drums_with_crash
- 8.0, drums_basic
- 12.0, drums_with_crash
+ 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/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/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 ee28402..7db925a 100644
--- a/src/generated/music_data.cc
+++ b/src/generated/music_data.cc
@@ -24,8 +24,9 @@ const NoteParams g_tracker_samples[] = {
{ 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,151 +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 },
+ { 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 },
+ { 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[] = {
- { 1.0f, 3, 1.1f, 0.1f },
- { 2.5f, 6, 0.9f, 0.0f },
- { 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, 2, 4.0f }, // kick_basic
- { PATTERN_EVENTS_kick_varied, 2, 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, 3, 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;
@@ -199,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 = {
@@ -290,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 9e04741..f77984e 100644
--- a/src/generated/test_demo_music.cc
+++ b/src/generated/test_demo_music.cc
@@ -20,36 +20,40 @@ 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 },
- { 4.0f, 1 },
- { 8.0f, 0 },
- { 12.0f, 1 },
+ { 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 = {
- SCORE_TRIGGERS, 4, 120.0f
+ SCORE_TRIGGERS, 8, 120.0f
};
// ============================================================
diff --git a/tools/track_visualizer/index.html b/tools/track_visualizer/index.html
index 70a5fd8..4a613ec 100644
--- a/tools/track_visualizer/index.html
+++ b/tools/track_visualizer/index.html
@@ -161,7 +161,9 @@
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();
@@ -169,10 +171,28 @@
// Skip comments and empty lines
if (line.startsWith('#') || line.length === 0) continue;
- // Pattern definition
+ // BPM directive
+ if (line.startsWith('BPM ')) {
+ bpm = parseFloat(line.substring(4).trim());
+ continue;
+ }
+
+ // Pattern definition with optional LENGTH
if (line.startsWith('PATTERN ')) {
- currentPattern = line.substring(8).trim();
- patterns[currentPattern] = [];
+ 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;
}
@@ -184,12 +204,12 @@
continue;
}
- // Pattern events (beat, sample, volume, pan)
+ // Pattern events (unit_time, sample, volume, pan)
if (currentPattern && !inScore) {
const parts = line.split(',').map(s => s.trim());
if (parts.length >= 2) {
- patterns[currentPattern].push({
- beat: parseFloat(parts[0]),
+ 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
@@ -197,36 +217,44 @@
}
}
- // Score entries (time, pattern_name)
+ // Score entries (unit_time, pattern_name)
if (inScore) {
const parts = line.split(',').map(s => s.trim());
if (parts.length >= 2) {
score.push({
- time: parseFloat(parts[0]),
+ unitTime: parseFloat(parts[0]),
pattern: parts[1]
});
}
}
}
- return { patterns, score };
+ 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;
}
- // Calculate pattern duration (max beat time)
- function getPatternDuration(pattern) {
- if (pattern.length === 0) return 4.0; // Default 4 beats
- return Math.max(...pattern.map(e => e.beat)) + 1.0;
+ // 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 } = trackData;
+ const { patterns, score, bpm } = trackData;
// Find max time for canvas sizing
const maxTime = score.length > 0
- ? Math.max(...score.map(s => s.time + getPatternDuration(patterns[s.pattern] || [])))
+ ? Math.max(...score.map(s => unitsToSeconds(s.unitTime, bpm) + getPatternDuration(patterns[s.pattern], bpm)))
: 60;
// Update canvas size
@@ -246,8 +274,8 @@
// Group score entries by time for stacking
const stackedPatterns = [];
for (const entry of score) {
- const startTime = entry.time;
- const duration = getPatternDuration(patterns[entry.pattern] || []);
+ const startTime = unitsToSeconds(entry.unitTime, bpm);
+ const duration = getPatternDuration(patterns[entry.pattern], bpm);
const endTime = startTime + duration;
// Find stack level (avoid overlaps)
@@ -261,13 +289,15 @@
stackLevel++;
}
+ const pattern = patterns[entry.pattern];
stackedPatterns.push({
patternName: entry.pattern,
startTime,
endTime,
duration,
stackLevel,
- events: patterns[entry.pattern] || []
+ unitLength: pattern ? pattern.unitLength : 1.0,
+ events: pattern ? pattern.events : []
});
}
@@ -385,7 +415,10 @@
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
for (const event of item.events) {
- const tickX = x + event.beat * (PIXELS_PER_SECOND * zoomLevel) / beatsPerSecond;
+ // 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
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",