From af4b27dfb0862bc4a76d716dacd24acf73ee059a Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 4 Feb 2026 14:38:04 +0100 Subject: feat(audio): Trigger pattern events individually for tempo scaling Refactored tracker system to trigger individual events as separate voices instead of compositing patterns into single spectrograms. This enables notes within patterns to respect tempo scaling dynamically. Key Changes: - Added ActivePattern tracking with start_music_time and next_event_idx - Individual events trigger when their beat time is reached - Elapsed beats calculated dynamically: (music_time - start_time) / beat_duration - Removed pattern compositing logic (paste_spectrogram) - Each note now triggers as separate voice with volume/pan parameters Behavior: - Tempo scaling (via music_time) now affects note spacing within patterns - At 2.0x tempo: patterns trigger 2x faster AND notes within play 2x faster - At 0.5x tempo: patterns trigger 2x slower AND notes within play 2x slower Testing: - Updated test_tracker to verify event-based triggering at specific beat times - All 17 tests pass (100%) - WAV dump confirms tempo scaling works correctly: * 0-10s: steady 1.00x tempo * 10-15s: acceleration to 2.00x tempo * 15-20s: reset to 1.00x tempo * 20-25s: deceleration to 0.50x tempo * 25s+: return to normal Result: Music time advances at variable rates (61.24s in 60s physical time), and notes within patterns correctly accelerate/decelerate with tempo changes. handoff(Claude): Tempo scaling now affects notes within patterns Co-Authored-By: Claude Sonnet 4.5 --- src/audio/tracker.cc | 189 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 122 insertions(+), 67 deletions(-) (limited to 'src/audio/tracker.cc') diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 8f3da38..cb97f23 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -7,13 +7,23 @@ 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 +// 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 @@ -24,6 +34,14 @@ void tracker_init() { 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; + } +} + +void tracker_reset() { + g_last_trigger_idx = 0; + for (int i = 0; i < MAX_SPECTROGRAMS; ++i) { + g_active_patterns[i].active = false; } } @@ -41,81 +59,118 @@ static int get_free_pool_slot() { return slot; } -void tracker_update(float time_sec) { +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 +static void trigger_note_event(const TrackerEvent& event) { + std::vector note_data; + int note_frames = 0; + + // Load or generate the note spectrogram + AssetId aid = g_tracker_sample_assets[event.sample_id]; + if (aid != AssetId::ASSET_LAST_ID) { + size_t size; + const uint8_t* data = GetAsset(aid, &size); + if (data && size >= sizeof(SpecHeader)) { + const SpecHeader* header = (const SpecHeader*)data; + note_frames = header->num_frames; + const float* src_spectral_data = + (const float*)(data + sizeof(SpecHeader)); + note_data.assign(src_spectral_data, + src_spectral_data + (size_t)note_frames * DCT_SIZE); + } + } else { + const NoteParams& params = g_tracker_samples[event.sample_id]; + note_data = generate_note_spectrogram(params, ¬e_frames); + } + + if (note_frames > 0) { + const int slot = get_free_pool_slot(); + + // 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; + } + + // Allocate and register new note + 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_spec_pool[slot].synth_id = synth_register_spectrogram(&spec); + g_spec_pool[slot].active = true; + + synth_trigger_voice(g_spec_pool[slot].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 > time_sec) + if (trigger.time_sec > music_time_sec) break; - const TrackerPattern& pattern = g_tracker_patterns[trigger.pattern_id]; - - int dest_num_frames = 0; - std::vector pattern_data; - - float beat_to_sec = 60.0f / g_tracker_score.bpm; - - for (uint32_t i = 0; i < pattern.num_events; ++i) { - const TrackerEvent& event = pattern.events[i]; - - std::vector note_data; - int note_frames = 0; - - AssetId aid = g_tracker_sample_assets[event.sample_id]; - if (aid != AssetId::ASSET_LAST_ID) { - size_t size; - const uint8_t* data = GetAsset(aid, &size); - if (data && size >= sizeof(SpecHeader)) { - const SpecHeader* header = (const SpecHeader*)data; - note_frames = header->num_frames; - const float* src_spectral_data = - (const float*)(data + sizeof(SpecHeader)); - note_data.assign(src_spectral_data, - src_spectral_data + (size_t)note_frames * DCT_SIZE); - } - } else { - const NoteParams& params = g_tracker_samples[event.sample_id]; - note_data = generate_note_spectrogram(params, ¬e_frames); - } - - if (note_frames > 0) { - int frame_offset = - (int)(event.beat * beat_to_sec * 32000.0f / DCT_SIZE); - paste_spectrogram(pattern_data, &dest_num_frames, note_data, - note_frames, frame_offset); - } + // 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; } - if (dest_num_frames > 0) { - const int slot = get_free_pool_slot(); - - // 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; - } - - // 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)); - - 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; - - 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++; + } + + // 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++; } - g_last_trigger_idx++; + // If all events have been triggered, mark pattern as complete + if (active.next_event_idx >= pattern.num_events) { + active.active = false; + } } } -- cgit v1.2.3