diff options
| -rw-r--r-- | assets/music.track | 382 | ||||
| -rw-r--r-- | assets/test_demo.track | 40 | ||||
| -rw-r--r-- | convert_track.py | 77 | ||||
| -rw-r--r-- | doc/TRACKER.md | 81 | ||||
| -rw-r--r-- | src/audio/tracker.cc | 23 | ||||
| -rw-r--r-- | src/audio/tracker.h | 8 | ||||
| -rw-r--r-- | src/generated/music_data.cc | 372 | ||||
| -rw-r--r-- | src/generated/test_demo_music.cc | 38 | ||||
| -rw-r--r-- | tools/track_visualizer/index.html | 71 | ||||
| -rw-r--r-- | tools/tracker_compiler.cc | 25 |
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", |
