#include "tracker.h" #include "audio.h" #include "audio/synth.h" #include "util/asset_manager.h" #include "util/debug.h" #include #include static uint32_t g_last_trigger_idx = 0; // Active pattern instance tracking struct ActivePattern { uint16_t pattern_id; float start_music_time; // When this pattern was triggered (music time) uint32_t next_event_idx; // Next event to trigger within this pattern bool active; }; static ActivePattern g_active_patterns[MAX_SPECTROGRAMS]; struct ManagedSpectrogram { int synth_id; float* data; bool active; }; // Simple pool for dynamic spectrograms (now for individual notes) static ManagedSpectrogram g_spec_pool[MAX_SPECTROGRAMS]; static int g_next_pool_slot = 0; // Round-robin allocation // CACHE: Pre-registered synth_ids for all samples (indexed by sample_id) // This eliminates redundant spectrogram generation and registration static int g_sample_synth_cache[256]; // Max 256 unique samples static bool g_cache_initialized = false; // Forward declarations static int get_free_pool_slot(); 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; g_spec_pool[i].active = false; g_active_patterns[i].active = false; } // Initialize sample cache if (!g_cache_initialized) { for (int i = 0; i < 256; ++i) { g_sample_synth_cache[i] = -1; } // Pre-register all unique samples (assets + generated notes) for (uint32_t sid = 0; sid < g_tracker_samples_count; ++sid) { AssetId aid = g_tracker_sample_assets[sid]; if (aid != AssetId::ASSET_LAST_ID) { // ASSET sample: Load once and cache size_t size; const uint8_t* data = GetAsset(aid, &size); if (data && size >= sizeof(SpecHeader)) { const SpecHeader* header = (const SpecHeader*)data; const int note_frames = header->num_frames; const float* spectral_data = (const float*)(data + sizeof(SpecHeader)); Spectrogram spec; spec.spectral_data_a = spectral_data; spec.spectral_data_b = spectral_data; spec.num_frames = note_frames; g_sample_synth_cache[sid] = synth_register_spectrogram(&spec); #if defined(DEBUG_LOG_TRACKER) if (g_sample_synth_cache[sid] == -1) { DEBUG_TRACKER( "[TRACKER INIT] Failed to cache asset sample_id=%d (aid=%d)\n", sid, (int)aid); } #endif /* defined(DEBUG_LOG_TRACKER) */ } } else { // GENERATED note: Generate once and cache const NoteParams& params = g_tracker_samples[sid]; int note_frames = 0; std::vector note_data = generate_note_spectrogram(params, ¬e_frames); if (note_frames > 0) { // Allocate persistent storage for this note const int slot = get_free_pool_slot(); g_spec_pool[slot].data = new float[note_data.size()]; memcpy(g_spec_pool[slot].data, note_data.data(), note_data.size() * sizeof(float)); Spectrogram spec; spec.spectral_data_a = g_spec_pool[slot].data; spec.spectral_data_b = g_spec_pool[slot].data; spec.num_frames = note_frames; g_sample_synth_cache[sid] = synth_register_spectrogram(&spec); g_spec_pool[slot].synth_id = g_sample_synth_cache[sid]; g_spec_pool[slot].active = true; // Mark as permanently allocated #if defined(DEBUG_LOG_TRACKER) if (g_sample_synth_cache[sid] == -1) { DEBUG_TRACKER( "[TRACKER INIT] Failed to cache generated sample_id=%d (freq=%.2f)\n", sid, params.base_freq); } #endif /* defined(DEBUG_LOG_TRACKER) */ } } } g_cache_initialized = true; #if defined(DEBUG_LOG_TRACKER) DEBUG_TRACKER( "[TRACKER INIT] Cached %d unique samples\n", g_tracker_samples_count); #endif /* defined(DEBUG_LOG_TRACKER) */ } } void tracker_reset() { g_last_trigger_idx = 0; for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { g_active_patterns[i].active = false; } } 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; } // 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; } static int get_free_pattern_slot() { for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { if (!g_active_patterns[i].active) return i; } return -1; // No free slots } // Helper to trigger a single note event (OPTIMIZED with caching) static void trigger_note_event(const TrackerEvent& event) { #if defined(DEBUG_LOG_TRACKER) // VALIDATION: Check sample_id bounds if (event.sample_id >= g_tracker_samples_count) { DEBUG_TRACKER( "[TRACKER ERROR] Invalid sample_id=%d (max=%d)\n", event.sample_id, g_tracker_samples_count - 1); return; } // VALIDATION: Check volume and pan ranges if (event.volume < 0.0f || event.volume > 2.0f) { DEBUG_TRACKER( "[TRACKER WARNING] Unusual volume=%.2f for sample_id=%d\n", event.volume, event.sample_id); } if (event.pan < -1.0f || event.pan > 1.0f) { DEBUG_TRACKER( "[TRACKER WARNING] Invalid pan=%.2f for sample_id=%d\n", event.pan, event.sample_id); } #endif /* defined(DEBUG_LOG_TRACKER) */ // OPTIMIZED: Use cached synth_id instead of regenerating spectrogram const int cached_synth_id = g_sample_synth_cache[event.sample_id]; #if defined(DEBUG_LOG_TRACKER) if (cached_synth_id == -1) { DEBUG_TRACKER( "[TRACKER ERROR] No cached synth_id for sample_id=%d (init failed?)\n", event.sample_id); return; } #endif /* defined(DEBUG_LOG_TRACKER) */ // Trigger voice directly with cached spectrogram synth_trigger_voice(cached_synth_id, event.volume, event.pan); } void tracker_update(float music_time_sec) { // 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) 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].next_event_idx = 0; g_active_patterns[slot].active = true; } g_last_trigger_idx++; } // 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; ActivePattern& active = g_active_patterns[i]; const TrackerPattern& pattern = g_tracker_patterns[active.pattern_id]; // Calculate elapsed beats since pattern started const float elapsed_music_time = music_time_sec - active.start_music_time; const float elapsed_beats = elapsed_music_time / beat_duration; // Trigger all events that have passed their beat time while (active.next_event_idx < pattern.num_events) { const TrackerEvent& event = pattern.events[active.next_event_idx]; if (event.beat > elapsed_beats) break; // This event hasn't reached its time yet // Trigger this event as an individual voice trigger_note_event(event); active.next_event_idx++; } // If all events have been triggered, mark pattern as complete if (active.next_event_idx >= pattern.num_events) { active.active = false; } } }