summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md16
-rw-r--r--assets/music.track226
-rw-r--r--doc/ASSET_SYSTEM.md213
-rw-r--r--src/audio/synth.h5
-rw-r--r--src/audio/tracker.cc51
-rw-r--r--src/main.cc3
-rw-r--r--tools/tracker_compiler.cc118
7 files changed, 564 insertions, 68 deletions
diff --git a/TODO.md b/TODO.md
index 48d8528..dd18f72 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,7 +2,21 @@
This file tracks prioritized tasks with detailed attack plans.
-## Recently Completed
+## Recently Completed (February 3, 2026)
+- [x] **Music System Bug Fixes & Rock Track Implementation**:
+ - [x] **Fixed Critical Pool Exhaustion Bug**: Tracker pool slots were never freed, causing music to stop after 8 patterns. Implemented round-robin slot allocation with proper cleanup.
+ - [x] **Fixed Note Name Parsing**: Added automatic note-name-to-frequency conversion (E2→82.4Hz, A4→440Hz, etc.) in `tracker_compiler.cc`. Bass and melody now play correctly.
+ - [x] **Fixed Timing Mismatch**: Patterns are 4 beats (2 seconds at 120 BPM), but SCORE was triggering every 4 seconds. Updated to trigger every 2 seconds for continuous playback.
+ - [x] **Implemented Dynamic Resource Sizing**: `tracker_compiler` now analyzes scores to determine optimal MAX_VOICES/MAX_SPECTROGRAMS values (max simultaneous patterns: 5, recommended: 10 each).
+ - [x] **Created Rock Demo Track**: 11 patterns (drums, bass, melody, power chords) with 49 triggers over 25 seconds, using 14 drum samples + 10 procedural notes.
+
+ **Known Issues**:
+ - Audio playback quality could be improved (some artifacts/cuts remain)
+ - Note synthesis parameters (duration, harmonics) use default values - need tuning for better sound
+ - Pattern overlaps might cause voice exhaustion under heavy load
+ - No dynamic volume/envelope control for melodic notes
+
+## Recently Completed (Previous)
- [x] **Task #26: Shader Asset Testing & Validation**:
- [x] **Attack Plan - `ShaderComposer` Unit Tests**: Add tests to `test_shader_composer.cc` to verify correct snippet registration, retrieval, and composition for various WGSL shader assets.
- [x] **Attack Plan - Asset Content Validation**: Implement checks (e.g., in `test_assets.cc`) to ensure loaded WGSL shader assets are non-empty and contain expected entry points (`vs_main`, `fs_main`, `main` for compute shaders).
diff --git a/assets/music.track b/assets/music.track
index bb6e4ba..c0b343e 100644
--- a/assets/music.track
+++ b/assets/music.track
@@ -1,69 +1,205 @@
+# Rock Demo Track - Full drum kit arrangement
# Samples: name, freq, duration, amp, attack, harmonics, harmonic_decay (for generated)
# OR SAMPLE ASSET_ASSETID (for assets)
-SAMPLE kick, 50, 0.2, 1.0, 0.01, 1, 0.5
-SAMPLE snare, 200, 0.2, 0.8, 0.01, 5, 0.7
-SAMPLE hihat, 1000, 0.05, 0.3, 0.005, 10, 0.4
+
+# Import all available drum samples
SAMPLE ASSET_KICK_1
+SAMPLE ASSET_KICK_2
+SAMPLE ASSET_KICK_3
SAMPLE ASSET_SNARE_1
+SAMPLE ASSET_SNARE_2
+SAMPLE ASSET_SNARE_3
+SAMPLE ASSET_SNARE_4
SAMPLE ASSET_HIHAT_1
+SAMPLE ASSET_HIHAT_2
+SAMPLE ASSET_HIHAT_3
+SAMPLE ASSET_HIHAT_4
+SAMPLE ASSET_CRASH_1
+SAMPLE ASSET_RIDE_1
+SAMPLE ASSET_SPLASH_1
+SAMPLE ASSET_BASS_1
+SAMPLE ASSET_SYNTH_BASS_1
-# Patterns: beat, sample, volume, pan
-PATTERN drum_loop
+# Rock beat - kick and snare foundation (4/4 time)
+PATTERN rock_beat
0.0, ASSET_KICK_1, 1.0, 0.0
- 1.0, ASSET_SNARE_1, 0.7, 0.0
+ 1.0, ASSET_SNARE_3, 0.9, 0.1
2.0, ASSET_KICK_1, 1.0, 0.0
- 2.5, ASSET_KICK_1, 0.6, 0.2
- 3.0, ASSET_SNARE_1, 0.7, 0.0
+ 2.5, ASSET_KICK_1, 0.7, -0.1
+ 3.0, ASSET_SNARE_3, 0.9, 0.1
+
+# Heavy double kick pattern
+PATTERN double_kick
+ 0.0, ASSET_KICK_3, 1.0, 0.0
+ 0.5, ASSET_KICK_2, 0.8, -0.2
+ 1.0, ASSET_SNARE_4, 0.95, 0.2
+ 1.5, ASSET_KICK_3, 0.7, 0.1
+ 2.0, ASSET_KICK_3, 1.0, 0.0
+ 2.5, ASSET_KICK_2, 0.8, -0.2
+ 3.0, ASSET_SNARE_4, 0.95, 0.2
+ 3.5, ASSET_KICK_3, 0.6, 0.1
+
+# Standard 8th note hi-hat pattern
+PATTERN hihat_8th
+ 0.0, ASSET_HIHAT_2, 0.6, -0.3
+ 0.5, ASSET_HIHAT_1, 0.45, 0.3
+ 1.0, ASSET_HIHAT_2, 0.6, -0.3
+ 1.5, ASSET_HIHAT_1, 0.45, 0.3
+ 2.0, ASSET_HIHAT_2, 0.6, -0.3
+ 2.5, ASSET_HIHAT_1, 0.45, 0.3
+ 3.0, ASSET_HIHAT_2, 0.6, -0.3
+ 3.5, ASSET_HIHAT_1, 0.45, 0.3
+
+# 16th note hi-hat for intensity
+PATTERN hihat_16th
+ 0.0, ASSET_HIHAT_3, 0.7, -0.4
+ 0.25, ASSET_HIHAT_1, 0.35, 0.4
+ 0.5, ASSET_HIHAT_3, 0.55, -0.2
+ 0.75, ASSET_HIHAT_1, 0.35, 0.2
+ 1.0, ASSET_HIHAT_3, 0.7, -0.4
+ 1.25, ASSET_HIHAT_1, 0.35, 0.4
+ 1.5, ASSET_HIHAT_3, 0.55, -0.2
+ 1.75, ASSET_HIHAT_1, 0.35, 0.2
+ 2.0, ASSET_HIHAT_3, 0.7, -0.4
+ 2.25, ASSET_HIHAT_1, 0.35, 0.4
+ 2.5, ASSET_HIHAT_3, 0.55, -0.2
+ 2.75, ASSET_HIHAT_1, 0.35, 0.2
+ 3.0, ASSET_HIHAT_3, 0.7, -0.4
+ 3.25, ASSET_HIHAT_1, 0.35, 0.4
+ 3.5, ASSET_HIHAT_3, 0.55, -0.2
+ 3.75, ASSET_HIHAT_1, 0.35, 0.2
+
+# Ride cymbal pattern for variation
+PATTERN ride_pattern
+ 0.0, ASSET_RIDE_1, 0.65, 0.5
+ 0.5, ASSET_RIDE_1, 0.5, -0.3
+ 1.0, ASSET_RIDE_1, 0.65, 0.5
+ 1.5, ASSET_RIDE_1, 0.5, -0.3
+ 2.0, ASSET_RIDE_1, 0.65, 0.5
+ 2.5, ASSET_RIDE_1, 0.5, -0.3
+ 3.0, ASSET_RIDE_1, 0.7, 0.5
+ 3.5, ASSET_RIDE_1, 0.5, -0.3
+
+# Crash accents (single crash at start)
+PATTERN crash_accent
+ 0.0, ASSET_CRASH_1, 0.85, 0.0
+
+# Splash accent for transitions
+PATTERN splash_accent
+ 0.0, ASSET_SPLASH_1, 0.75, 0.3
+
+# Fill pattern with snare rolls and toms
+PATTERN snare_fill
+ 0.0, ASSET_SNARE_2, 0.6, -0.4
+ 0.25, ASSET_SNARE_2, 0.65, -0.2
+ 0.5, ASSET_SNARE_2, 0.7, 0.0
+ 0.75, ASSET_SNARE_3, 0.75, 0.2
+ 1.0, ASSET_SNARE_3, 0.8, 0.4
+ 1.25, ASSET_SNARE_4, 0.85, 0.2
+ 1.5, ASSET_SNARE_4, 0.9, 0.0
+ 1.75, ASSET_SNARE_4, 0.95, -0.2
-PATTERN hihat_roll
- 0.0, ASSET_HIHAT_1, 0.5, -0.5
- 0.5, ASSET_HIHAT_1, 0.4, 0.5
- 1.0, ASSET_HIHAT_1, 0.5, -0.5
- 1.5, ASSET_HIHAT_1, 0.4, 0.5
- 2.0, ASSET_HIHAT_1, 0.5, -0.5
- 2.5, ASSET_HIHAT_1, 0.4, 0.5
- 3.0, ASSET_HIHAT_1, 0.5, -0.5
- 3.5, ASSET_HIHAT_1, 0.4, 0.5
+# Power chord progression in E minor
+PATTERN power_riff
+ 0.0, E2, 0.8, -0.5
+ 0.5, E2, 0.6, -0.5
+ 1.0, G2, 0.8, -0.3
+ 1.5, G2, 0.6, -0.3
+ 2.0, A2, 0.8, 0.3
+ 2.5, A2, 0.6, 0.3
+ 3.0, B2, 0.8, 0.5
+ 3.5, B2, 0.6, 0.5
-PATTERN em_melody
+# Lead melody in E minor scale
+PATTERN lead_melody
0.0, E4, 0.7, 0.0
- 0.5, G4, 0.7, 0.1
+ 0.5, G4, 0.65, 0.1
1.0, B4, 0.7, -0.1
- 1.5, D5, 0.7, 0.2
- 2.0, E5, 0.7, -0.2
- 2.5, D5, 0.7, 0.1
+ 1.5, D5, 0.65, 0.2
+ 2.0, E5, 0.75, -0.2
+ 2.5, D5, 0.65, 0.1
3.0, B4, 0.7, 0.0
- 3.5, G4, 0.7, -0.1
+ 3.5, A4, 0.65, -0.1
+
+# Bass line synced with kick
+PATTERN bass_line
+ 0.0, E2, 0.9, 0.0
+ 1.0, E2, 0.75, 0.0
+ 2.0, E2, 0.9, 0.0
+ 2.5, E2, 0.7, 0.0
+ 3.0, G2, 0.85, 0.0
# Score: time_sec, pattern_name
+# NOTE: Patterns are 4 beats = 2 seconds at 120 BPM
SCORE
- 0.0, drum_loop
- 0.0, hihat_roll
- 0.0, em_melody
+# Intro - Build up with hi-hat and kick (0-4s)
+ 0.0, crash_accent
+ 0.0, hihat_8th
+ 0.0, rock_beat
+
+ 2.0, hihat_8th
+ 2.0, rock_beat
+
+# Main groove - Add bass (4-8s)
+ 4.0, crash_accent
+ 4.0, hihat_8th
+ 4.0, rock_beat
+ 4.0, bass_line
+
+ 6.0, hihat_8th
+ 6.0, rock_beat
+ 6.0, bass_line
+
+# Add melody and increase intensity (8-12s)
+ 8.0, splash_accent
+ 8.0, hihat_16th
+ 8.0, double_kick
+ 8.0, bass_line
+ 8.0, lead_melody
+
+ 10.0, hihat_16th
+ 10.0, double_kick
+ 10.0, bass_line
+ 10.0, lead_melody
+
+# Ride variation (12-16s)
+ 12.0, crash_accent
+ 12.0, ride_pattern
+ 12.0, double_kick
+ 12.0, power_riff
- 4.0, drum_loop
- 4.0, hihat_roll
- 4.0, em_melody
+ 14.0, ride_pattern
+ 14.0, double_kick
+ 14.0, power_riff
- 8.0, drum_loop
- 8.0, hihat_roll
- 8.0, em_melody
+# Fill and breakdown (16-18s)
+ 16.0, snare_fill
+ 16.0, hihat_16th
- 12.0, drum_loop
- 12.0, hihat_roll
- 12.0, em_melody
+ 17.0, splash_accent
+ 17.0, hihat_8th
+ 17.0, rock_beat
+ 17.0, bass_line
- 16.0, drum_loop
- 16.0, em_melody
+# Climax with all elements (18-22s)
+ 18.0, crash_accent
+ 18.0, hihat_16th
+ 18.0, double_kick
+ 18.0, bass_line
+ 18.0, lead_melody
- 20.0, drum_loop
- 20.0, hihat_roll
- 20.0, em_melody
+ 20.0, hihat_16th
+ 20.0, double_kick
+ 20.0, bass_line
+ 20.0, power_riff
- 24.0, drum_loop
- 24.0, em_melody
+# Final fill and outro (22-24s)
+ 22.0, snare_fill
+ 22.0, hihat_16th
- 28.0, drum_loop
- 28.0, hihat_roll
- 28.0, em_melody
+ 23.0, crash_accent
+ 23.0, rock_beat
+ 23.0, bass_line
+# Ending crash
+ 25.0, crash_accent
diff --git a/doc/ASSET_SYSTEM.md b/doc/ASSET_SYSTEM.md
index 3493936..9cf15ba 100644
--- a/doc/ASSET_SYSTEM.md
+++ b/doc/ASSET_SYSTEM.md
@@ -73,3 +73,216 @@ Shader code (WGSL) can also be managed as assets.
construct a `std::string` or `std::string_view` using the returned pointer
and size.
4. Register them with the `ShaderComposer`.
+
+---
+
+# Current Implementation Status
+
+## What's Working ✅
+- **Build-Time Packer** (`tools/asset_packer.cc`): Parses `demo_assets.txt`, embeds binary files as C++ arrays
+- **Runtime Manager** (`src/util/asset_manager.{h,cc}`): O(1) retrieval with caching, SIOF fix via singleton
+- **Asset Types**: Static binaries (`.spec`, `.wgsl`), procedural textures (`PROC(func, params)`)
+- **Safety Guarantees**: 16-byte alignment, null-terminator for strings, size reporting
+- **Testing**: `test_assets.cc` validates retrieval, caching, procedural generation
+- **Current Manifest**: 33 assets (15 audio samples, 17 WGSL shaders, 1 procedural texture)
+
+## Design Strengths
+- Enum-based type safety (no magic numbers or strings at runtime)
+- Zero runtime cost for static assets (direct pointer return)
+- Clean separation of build-time and runtime concerns
+- Procedural generation support (saves binary size)
+- Thread-safe for immutable assets (cache is write-once)
+
+## Design Weaknesses
+- **No compression**: Only `NONE` supported, critical blocker for 64k goal
+- **STL dependencies**: `std::map`, `std::string` in runtime code (conflicts with CRT replacement)
+- **Hardcoded procedural dimensions**: Assumes 256×256 RGBA8 (lines 85-86 in `asset_manager.cc`)
+- **No integrity checks**: No CRC/hash validation for asset correctness
+- **Limited asset types**: No mesh/scene format yet (needed for Task #18)
+
+---
+
+# Asset System Improvement Tasks
+
+## High Priority (Critical for 64k Goal)
+
+### Task #27: Asset Compression Layer
+**Goal**: Implement runtime decompression to reduce binary size by 30-50%.
+
+**Attack Plan**:
+- [ ] **27.1**: Add compression type enum (`NONE`, `ZLIB`, `RLE`, `CUSTOM`)
+- [ ] **27.2**: Update `asset_packer.cc` to compress assets during build
+ - Integrate a lightweight compression library (e.g., miniz for zlib)
+ - Store compressed size + original size in `AssetRecord`
+- [ ] **27.3**: Implement runtime decompressor in `asset_manager.cc`
+ - Allocate decompressed buffer on first `GetAsset()` call
+ - Cache decompressed data, allow `DropAsset()` to free memory
+- [ ] **27.4**: Update `assets.txt` format to specify compression per-asset
+ - Example: `KICK_1, ZLIB, kick1.spec, "Compressed kick sample"`
+- [ ] **27.5**: Add tests for compressed asset retrieval and DropAsset
+
+**Size Impact**: Estimated 30-50% reduction on asset blob (shaders compress well, spectrograms moderately).
+
+**Dependency**: None (standalone improvement).
+
+---
+
+### Task #28: Spectrogram Quantization
+**Goal**: Compress audio assets from `float32` to `uint16_t` with logarithmic frequency binning.
+
+**Attack Plan**:
+- [ ] **28.1**: Research optimal frequency bin distribution (logarithmic vs linear)
+- [ ] **28.2**: Update `spectool` to export quantized `.spec` format
+ - Add `--quantize` flag to `spectool analyze`
+ - Map spectral values to `uint16_t` range with configurable dynamic range
+- [ ] **28.3**: Update audio synthesis engine to decode quantized spectrograms
+ - Add dequantization step in `src/audio/synth.cc`
+- [ ] **28.4**: Re-generate all `.spec` assets with quantization enabled
+- [ ] **28.5**: Add tests to verify audio quality degradation is acceptable
+
+**Size Impact**: 2-4× reduction on `.spec` files (currently 15 audio assets in manifest).
+
+**Dependency**: Task #27 (compression) for maximum size savings.
+
+---
+
+### Task #29: WGSL Shader Minification
+**Goal**: Minify WGSL shaders to reduce text asset size.
+
+**Attack Plan**:
+- [ ] **29.1**: Implement WGSL minifier in `tools/wgsl_minify.py`
+ - Remove comments, unnecessary whitespace
+ - Rename variables to single-letter identifiers (preserve entry points)
+ - Strip debug labels and unused code
+- [ ] **29.2**: Integrate minifier into build pipeline
+ - Add `MINIFY` compression type to `asset_packer.cc`
+ - Auto-minify all `SHADER_*` assets during build
+- [ ] **29.3**: Validate minified shaders compile correctly
+ - Add shader compilation test to CI/CD
+
+**Size Impact**: 40-60% reduction on shader text (currently 17 WGSL shaders in manifest).
+
+**Dependency**: None (standalone improvement).
+
+---
+
+## Medium Priority (Code Hygiene & Maintainability)
+
+### Task #20.1: Remove STL from Asset Manager (Part of Task #20)
+**Goal**: Eliminate `std::map` and `std::string` to prepare for CRT replacement.
+
+**Attack Plan**:
+- [ ] **20.1.1**: Replace `kAssetManagerProcGenFuncMap` (line 21 in `asset_manager.cc`)
+ - Switch to static array lookup or compile-time switch-case
+ - Example:
+ ```cpp
+ static const struct { const char* name; ProcGenFunc func; } kProcGenFuncs[] = {
+ {"gen_noise", procedural::gen_noise},
+ {"gen_grid", procedural::gen_grid},
+ {"make_periodic", procedural::make_periodic},
+ };
+ ```
+- [ ] **20.1.2**: Replace `std::map` in `asset_packer.cc` (build-time tool, lower priority)
+- [ ] **20.1.3**: Verify no STL usage remains in `src/util/asset_manager.cc`
+
+**Size Impact**: Minor (few KB), but critical for future CRT replacement.
+
+**Dependency**: None.
+
+---
+
+### Task #30: Procedural Asset Generalization
+**Goal**: Support variable dimensions and formats for procedural textures.
+
+**Attack Plan**:
+- [ ] **30.1**: Update `assets.txt` format to encode dimensions in `proc_params`
+ - Example: `NOISE_TEX, PROC(gen_noise, 256, 256, 4, 1234, 16), _, "Width, Height, Channels, Seed, Freq"`
+- [ ] **30.2**: Update `asset_packer.cc` to parse dimension parameters
+- [ ] **30.3**: Update `GetAsset()` to read dimensions from `proc_params`
+ - Allocate buffer dynamically based on `width * height * channels`
+- [ ] **30.4**: Add support for grayscale (1 channel) and RGB (3 channels) formats
+
+**Size Impact**: Negligible (unlocks new use cases, no binary size change).
+
+**Dependency**: None.
+
+---
+
+### Task #31: Asset Integrity Validation
+**Goal**: Add CRC32/hash checks to detect corrupted or incorrect assets.
+
+**Attack Plan**:
+- [ ] **31.1**: Compute CRC32 for each asset during build (`asset_packer.cc`)
+ - Store CRC in `AssetRecord` struct
+- [ ] **31.2**: Add optional validation in `GetAsset()` (debug builds only)
+ - Compare computed CRC against stored value
+ - Log warnings if mismatch detected
+- [ ] **31.3**: Add `--verify-assets` flag to test suite
+ - Run full integrity check on all assets
+
+**Size Impact**: +4 bytes per asset (CRC32), negligible overhead.
+
+**Dependency**: None.
+
+---
+
+## Lower Priority (Future Enhancements)
+
+### Task #18.1: 3D Asset Format Support (Part of Task #18)
+**Goal**: Extend asset system to handle binary mesh/scene data from Blender.
+
+**Attack Plan**:
+- [ ] **18.1.1**: Define binary mesh format (vertices, indices, normals, UVs)
+ - Consider alignment requirements for GPU upload
+- [ ] **18.1.2**: Update `asset_packer.cc` to handle `.mesh` files
+ - Add `MESH3D` asset type
+- [ ] **18.1.3**: Implement runtime parser in `src/3d/mesh_loader.cc`
+ - Parse binary blob into `Mesh` struct
+- [ ] **18.1.4**: Create Blender export script (`tools/blender_export.py`)
+
+**Size Impact**: Depends on scene complexity (meshes can be large, consider compression).
+
+**Dependency**: Task #27 (compression) for mesh data.
+
+---
+
+### Task #32: Asset Hot-Reloading (Development Only)
+**Goal**: Enable live asset updates for faster iteration (under `!STRIP_ALL`).
+
+**Attack Plan**:
+- [ ] **32.1**: Add file watcher for `assets/final/` directory
+ - Monitor `.spec`, `.wgsl` files for changes
+- [ ] **32.2**: Trigger rebuild of `assets_data.cc` on file change
+ - Invoke `asset_packer` and recompile
+- [ ] **32.3**: Add API to reload specific assets at runtime
+ - `ReloadAsset(AssetId)` for development use
+
+**Size Impact**: None (stripped from final build).
+
+**Dependency**: None (development-only feature).
+
+---
+
+### Task #33: Asset Usage Tracking & Dead Asset Elimination
+**Goal**: Identify and remove unused assets to reduce binary size.
+
+**Attack Plan**:
+- [ ] **33.1**: Add instrumentation to track `GetAsset()` calls
+ - Log which assets are accessed during a full demo run
+- [ ] **33.2**: Generate report of unused assets
+- [ ] **33.3**: Remove unused entries from `demo_assets.txt`
+
+**Size Impact**: Depends on dead assets (could save 5-15% if unused assets exist).
+
+**Dependency**: None.
+
+---
+
+# Priority Recommendation
+
+For the **64k goal**, prioritize in this order:
+1. **Task #27** (Compression) - Biggest size win
+2. **Task #29** (Shader Minification) - Easy win, low risk
+3. **Task #28** (Spectrogram Quantization) - Medium effort, high impact
+4. **Task #20.1** (Remove STL) - Required for future CRT replacement
+5. **Task #18.1** (3D Assets) - Once size budget allows
diff --git a/src/audio/synth.h b/src/audio/synth.h
index a0720f2..a8f15a9 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -7,8 +7,11 @@
#include "dct.h"
#include <cstdint>
+// Based on tracker score analysis (see generated/music_data.cc)
+// Max simultaneous patterns: 5, recommended: 10 each
+// Using 16 for comfortable headroom
#define MAX_VOICES 16
-#define MAX_SPECTROGRAMS 8
+#define MAX_SPECTROGRAMS 16
struct Spectrogram {
const float* spectral_data_a; // Front buffer
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 5d99a45..470123a 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -15,9 +15,11 @@ struct ManagedSpectrogram {
// Simple pool for dynamic spectrograms
static ManagedSpectrogram g_spec_pool[MAX_SPECTROGRAMS];
+static int g_next_pool_slot = 0; // Round-robin allocation
void tracker_init() {
g_last_trigger_idx = 0;
+ g_next_pool_slot = 0;
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
g_spec_pool[i].synth_id = -1;
g_spec_pool[i].data = nullptr;
@@ -26,11 +28,17 @@ void tracker_init() {
}
static int get_free_pool_slot() {
+ // Try to find an inactive slot first (unused slots)
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
if (!g_spec_pool[i].active)
return i;
}
- return -1;
+
+ // If all slots are active, reuse the oldest one (round-robin)
+ // This automatically handles cleanup of old patterns
+ const int slot = g_next_pool_slot;
+ g_next_pool_slot = (g_next_pool_slot + 1) % MAX_SPECTROGRAMS;
+ return slot;
}
void tracker_update(float time_sec) {
@@ -50,7 +58,7 @@ void tracker_update(float time_sec) {
for (uint32_t i = 0; i < pattern.num_events; ++i) {
const TrackerEvent& event = pattern.events[i];
-
+
std::vector<float> note_data;
int note_frames = 0;
@@ -77,27 +85,32 @@ void tracker_update(float time_sec) {
}
if (dest_num_frames > 0) {
- int slot = get_free_pool_slot();
- if (slot != -1) {
- if (g_spec_pool[slot].synth_id != -1) {
- synth_unregister_spectrogram(g_spec_pool[slot].synth_id);
- delete[] g_spec_pool[slot].data;
- }
+ const int slot = get_free_pool_slot();
- g_spec_pool[slot].data = new float[pattern_data.size()];
- memcpy(g_spec_pool[slot].data, pattern_data.data(),
- pattern_data.size() * sizeof(float));
+ // Clean up old data in this slot if reusing
+ if (g_spec_pool[slot].synth_id != -1) {
+ synth_unregister_spectrogram(g_spec_pool[slot].synth_id);
+ g_spec_pool[slot].synth_id = -1;
+ }
+ if (g_spec_pool[slot].data != nullptr) {
+ delete[] g_spec_pool[slot].data;
+ g_spec_pool[slot].data = nullptr;
+ }
- Spectrogram spec;
- spec.spectral_data_a = g_spec_pool[slot].data;
- spec.spectral_data_b = g_spec_pool[slot].data;
- spec.num_frames = dest_num_frames;
+ // Allocate and register new pattern
+ g_spec_pool[slot].data = new float[pattern_data.size()];
+ memcpy(g_spec_pool[slot].data, pattern_data.data(),
+ pattern_data.size() * sizeof(float));
- g_spec_pool[slot].synth_id = synth_register_spectrogram(&spec);
- g_spec_pool[slot].active = true;
+ Spectrogram spec;
+ spec.spectral_data_a = g_spec_pool[slot].data;
+ spec.spectral_data_b = g_spec_pool[slot].data;
+ spec.num_frames = dest_num_frames;
- synth_trigger_voice(g_spec_pool[slot].synth_id, 1.0f, 0.0f);
- }
+ g_spec_pool[slot].synth_id = synth_register_spectrogram(&spec);
+ g_spec_pool[slot].active = true;
+
+ synth_trigger_voice(g_spec_pool[slot].synth_id, 1.0f, 0.0f);
}
g_last_trigger_idx++;
diff --git a/src/main.cc b/src/main.cc
index 7114460..76e366a 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -102,6 +102,7 @@ int main(int argc, char** argv) {
const int step = beat_count % 16;
+/*
// Bass pattern
if (step % 4 == 0) {
float* back_buffer = synth_begin_update(bass_id);
@@ -112,7 +113,7 @@ int main(int argc, char** argv) {
}
synth_trigger_voice(bass_id, 0.9f, 1.2f);
}
-
+*/
++beat_count;
}
tracker_update((float)t);
diff --git a/tools/tracker_compiler.cc b/tools/tracker_compiler.cc
index b4c72b2..5cecbb8 100644
--- a/tools/tracker_compiler.cc
+++ b/tools/tracker_compiler.cc
@@ -1,3 +1,4 @@
+#include <cmath>
#include <cstdio>
#include <fstream>
#include <iostream>
@@ -12,6 +13,72 @@ enum SampleType {
ASSET
};
+// Convert note name (e.g., "C4", "A#3", "Eb2") to frequency in Hz
+static float note_name_to_freq(const std::string& note_name) {
+ if (note_name.empty())
+ return 0.0f;
+
+ // Parse note (C, C#, D, etc.) and octave
+ const char* str = note_name.c_str();
+ char note_char = str[0];
+ int semitone = 0;
+
+ // Map note name to semitone (C=0, D=2, E=4, F=5, G=7, A=9, B=11)
+ switch (note_char) {
+ case 'C':
+ semitone = 0;
+ break;
+ case 'D':
+ semitone = 2;
+ break;
+ case 'E':
+ semitone = 4;
+ break;
+ case 'F':
+ semitone = 5;
+ break;
+ case 'G':
+ semitone = 7;
+ break;
+ case 'A':
+ semitone = 9;
+ break;
+ case 'B':
+ semitone = 11;
+ break;
+ default:
+ return 0.0f; // Invalid note
+ }
+
+ int idx = 1;
+ // Check for sharp (#) or flat (b)
+ if (str[idx] == '#') {
+ semitone++;
+ idx++;
+ } else if (str[idx] == 'b') {
+ semitone--;
+ idx++;
+ }
+
+ // Parse octave
+ int octave = atoi(&str[idx]);
+
+ // A4 = 440 Hz is our reference (A4 = octave 4, semitone 9)
+ // Formula: freq = 440 * 2^((semitone - 9 + 12*(octave - 4)) / 12)
+ const int midi_note = semitone + 12 * (octave + 1);
+ const int a4_midi = 69; // A4 = MIDI note 69
+ const float freq = 440.0f * powf(2.0f, (midi_note - a4_midi) / 12.0f);
+
+ return freq;
+}
+
+static bool is_note_name(const std::string& name) {
+ if (name.empty())
+ return false;
+ const char first = name[0];
+ return (first >= 'A' && first <= 'G');
+}
+
struct Sample {
std::string name;
SampleType type = GENERATED; // Default to GENERATED
@@ -119,6 +186,22 @@ int main(int argc, char** argv) {
e.beat = beat;
e.sample_name = sname;
ss2 >> e.volume >> comma >> e.pan;
+
+ // Auto-create SAMPLE entry for note names (e.g., "E2", "A4")
+ if (is_note_name(sname) && sample_map.find(sname) == sample_map.end()) {
+ Sample s;
+ s.name = sname;
+ s.type = GENERATED;
+ s.freq = note_name_to_freq(sname);
+ s.dur = 0.5f; // Default note duration
+ s.amp = 1.0f; // Default amplitude
+ s.attack = 0.01f; // Default attack
+ s.harmonics = 3; // Default harmonics
+ s.harmonic_decay = 0.6f; // Default decay
+ sample_map[s.name] = samples.size();
+ samples.push_back(s);
+ }
+
patterns.back().events.push_back(e);
} else if (current_section == "SCORE") {
Trigger t;
@@ -206,9 +289,42 @@ int main(int argc, char** argv) {
fprintf(out_file, "const TrackerScore g_tracker_score = {\n");
fprintf(out_file, " SCORE_TRIGGERS, %zu, %.1ff\n", score.size(), bpm);
- fprintf(out_file, "};\n");
+ fprintf(out_file, "};\n\n");
+
+ // Calculate maximum simultaneous patterns for optimal resource allocation
+ std::map<float, int> time_pattern_count;
+ for (const auto& t : score) {
+ time_pattern_count[t.time]++;
+ }
+
+ int max_simultaneous_patterns = 0;
+ for (const auto& entry : time_pattern_count) {
+ if (entry.second > max_simultaneous_patterns) {
+ max_simultaneous_patterns = entry.second;
+ }
+ }
+
+ // Add safety margin (2x) for overlapping pattern playback
+ const int recommended_voices = max_simultaneous_patterns * 2;
+ const int recommended_spectrograms = max_simultaneous_patterns * 2;
+
+ fprintf(out_file, "// Resource usage analysis:\n");
+ fprintf(out_file, "// Maximum simultaneous pattern triggers: %d\n",
+ max_simultaneous_patterns);
+ fprintf(out_file, "// Recommended MAX_VOICES: %d (current: see synth.h)\n",
+ recommended_voices);
+ fprintf(out_file,
+ "// Recommended MAX_SPECTROGRAMS: %d (current: see synth.h)\n",
+ recommended_spectrograms);
fclose(out_file);
+ printf("Tracker compilation successful.\n");
+ printf(" Patterns: %zu\n", patterns.size());
+ printf(" Score triggers: %zu\n", score.size());
+ printf(" Max simultaneous patterns: %d\n", max_simultaneous_patterns);
+ printf(" Recommended MAX_VOICES: %d\n", recommended_voices);
+ printf(" Recommended MAX_SPECTROGRAMS: %d\n", recommended_spectrograms);
+
return 0;
}