summaryrefslogtreecommitdiff
path: root/src/audio/tracker.cc
blob: cb97f23d49d8c8d0f02ad56587f6e7acd9d5dff8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include "tracker.h"
#include "audio.h"
#include "audio/synth.h"
#include "util/asset_manager.h"
#include <cstring>
#include <vector>

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

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;
  }
}

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
static void trigger_note_event(const TrackerEvent& event) {
  std::vector<float> 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, &note_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 > 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;
    }
  }
}