fix: change reducer to take pointer to AppState to avoid stack overflow

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-05 09:32:21 +00:00
parent 4167419d54
commit 8e05c2f0ab
3 changed files with 192 additions and 194 deletions

View File

@@ -138,10 +138,10 @@ static void save_undo_state(AppState *state, int clip_index) {
if (state->undo.count < MAX_UNDO_HISTORY) state->undo.count++;
}
static AppState clip_trigger(AppState state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return state;
static void clip_trigger(AppState *state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return;
Clip *clip = &state.clips[clip_index];
Clip *clip = &state->clips[clip_index];
// Do NOT save undo here - caller will do it
@@ -161,18 +161,18 @@ static AppState clip_trigger(AppState state, int clip_index) {
int channel = clip_index % MAX_CHANNELS;
// Read from ring buffer
size_t wp = atomic_load(&state.record_write_pos[channel]);
size_t rp = atomic_load(&state.record_read_pos[channel]);
size_t wp = atomic_load(&state->record_write_pos[channel]);
size_t rp = atomic_load(&state->record_read_pos[channel]);
size_t available = wp - rp;
if (available > 0 && clip->buffer != NULL) {
size_t to_copy = (available < MAX_BUFFER_SIZE) ? available : MAX_BUFFER_SIZE;
for (size_t i = 0; i < to_copy; i++) {
clip->buffer[i] = state.record_buffer[channel][(rp + i) % MAX_BUFFER_SIZE];
clip->buffer[i] = state->record_buffer[channel][(rp + i) % MAX_BUFFER_SIZE];
}
clip->buffer_size = to_copy;
clip->write_position = to_copy;
atomic_store(&state.record_read_pos[channel], wp);
atomic_store(&state->record_read_pos[channel], wp);
}
break;
}
@@ -185,14 +185,12 @@ static AppState clip_trigger(AppState state, int clip_index) {
clip->read_position = 0;
break;
}
return state;
}
static AppState midi_clip_trigger(AppState state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return state;
static void midi_clip_trigger(AppState *state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return;
MidiClip *clip = &state.midi_clips[clip_index];
MidiClip *clip = &state->midi_clips[clip_index];
switch (clip->state) {
case CLIP_EMPTY:
@@ -213,41 +211,37 @@ static AppState midi_clip_trigger(AppState state, int clip_index) {
clip->read_index = 0;
break;
}
return state;
}
static AppState scene_trigger(AppState state, int scene_index) {
if (scene_index < 0 || scene_index >= MAX_SCENES * 8) return state; // 8 grids
static void scene_trigger(AppState *state, int scene_index) {
if (scene_index < 0 || scene_index >= MAX_SCENES * 8) return; // 8 grids
// Save undo info for all clips in the scene as a batch
int batch_start = state.undo.undo_index;
int batch_start = state->undo.undo_index;
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = scene_index * MAX_CHANNELS + ch;
save_undo_state(&state, clip_idx);
save_undo_state(state, clip_idx);
}
// Mark all entries in this batch with the batch size
int batch_end = state.undo.undo_index;
int batch_end = state->undo.undo_index;
for (int i = batch_start; i < batch_end; i++) {
int idx = i % MAX_UNDO_HISTORY;
state.undo.batch_sizes[idx] = batch_end - batch_start;
state->undo.batch_sizes[idx] = batch_end - batch_start;
}
// Now apply the changes
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = scene_index * MAX_CHANNELS + ch;
state = clip_trigger(state, clip_idx);
clip_trigger(state, clip_idx);
}
return state;
}
static AppState clip_reset(AppState state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return state;
static void clip_reset(AppState *state, int clip_index) {
if (clip_index < 0 || clip_index >= MAX_CLIPS) return;
Clip *clip = &state.clips[clip_index];
Clip *clip = &state->clips[clip_index];
save_undo_state(&state, clip_index);
save_undo_state(state, clip_index);
clip->state = CLIP_EMPTY;
clip->buffer_size = 0;
@@ -257,52 +251,49 @@ static AppState clip_reset(AppState state, int clip_index) {
// Also reset the ring buffer read position for this channel
int channel = clip_index % MAX_CHANNELS;
atomic_store(&state.record_read_pos[channel], atomic_load(&state.record_write_pos[channel]));
return state;
atomic_store(&state->record_read_pos[channel], atomic_load(&state->record_write_pos[channel]));
}
static AppState undo_action(AppState state) {
if (state.undo.undo_index <= 0) return state;
static void undo_action(AppState *state) {
if (state->undo.undo_index <= 0) return;
// Get the batch size for the current undo entry
int undo_idx = (state.undo.undo_index - 1) % MAX_UNDO_HISTORY;
int batch_size = state.undo.batch_sizes[undo_idx];
int undo_idx = (state->undo.undo_index - 1) % MAX_UNDO_HISTORY;
int batch_size = state->undo.batch_sizes[undo_idx];
if (batch_size == 0) batch_size = 1; // Single clip operation
// Undo all clips in the batch
for (int i = 0; i < batch_size; i++) {
int current_idx = (state.undo.undo_index - 1 - i) % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[current_idx];
int current_idx = (state->undo.undo_index - 1 - i) % MAX_UNDO_HISTORY;
int clip_idx = state->undo.prev_clip_indices[current_idx];
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
clip->state = state.undo.prev_clip_states[current_idx];
clip->buffer_size = state.undo.prev_buffer_sizes[current_idx];
clip->write_position = state.undo.prev_write_positions[current_idx];
clip->read_position = state.undo.prev_read_positions[current_idx];
Clip *clip = &state->clips[clip_idx];
clip->state = state->undo.prev_clip_states[current_idx];
clip->buffer_size = state->undo.prev_buffer_sizes[current_idx];
clip->write_position = state->undo.prev_write_positions[current_idx];
clip->read_position = state->undo.prev_read_positions[current_idx];
}
}
state.undo.undo_index -= batch_size;
return state;
state->undo.undo_index -= batch_size;
}
static AppState redo_action(AppState state) {
if (state.undo.redo_index <= state.undo.undo_index) return state;
static void redo_action(AppState *state) {
if (state->undo.redo_index <= state->undo.undo_index) return;
// Get the batch size for the next redo entry
int redo_idx = state.undo.undo_index % MAX_UNDO_HISTORY;
int batch_size = state.undo.batch_sizes[redo_idx];
int redo_idx = state->undo.undo_index % MAX_UNDO_HISTORY;
int batch_size = state->undo.batch_sizes[redo_idx];
if (batch_size == 0) batch_size = 1;
// Redo all clips in the batch
for (int i = 0; i < batch_size; i++) {
int current_idx = (state.undo.undo_index + i) % MAX_UNDO_HISTORY;
int clip_idx = state.undo.prev_clip_indices[current_idx];
int current_idx = (state->undo.undo_index + i) % MAX_UNDO_HISTORY;
int clip_idx = state->undo.prev_clip_indices[current_idx];
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
Clip *clip = &state->clips[clip_idx];
switch (clip->state) {
case CLIP_EMPTY:
clip->state = CLIP_RECORDING;
@@ -327,102 +318,106 @@ static AppState redo_action(AppState state) {
}
}
state.undo.undo_index += batch_size;
return state;
state->undo.undo_index += batch_size;
}
AppState reducer(AppState state, Action action) {
void reducer(AppState *state, Action action) {
switch (action.type) {
case ACTION_TRIGGER_CLIP: {
int clip_idx = action.data.trigger_clip.clip_index;
save_undo_state(&state, clip_idx);
return clip_trigger(state, clip_idx);
save_undo_state(state, clip_idx);
clip_trigger(state, clip_idx);
return;
}
case ACTION_TRIGGER_SCENE:
return scene_trigger(state, action.data.trigger_scene.scene_index);
scene_trigger(state, action.data.trigger_scene.scene_index);
return;
case ACTION_RESET_CLIP:
return clip_reset(state, action.data.reset_clip.clip_index);
clip_reset(state, action.data.reset_clip.clip_index);
return;
case ACTION_SET_QUANTIZE_MODE:
state.quantize_mode = action.data.set_quantize_mode.mode;
return state;
state->quantize_mode = action.data.set_quantize_mode.mode;
return;
case ACTION_SET_QUANTIZE_THRESHOLD:
state.quantize_threshold = action.data.set_quantize_threshold.threshold;
return state;
state->quantize_threshold = action.data.set_quantize_threshold.threshold;
return;
case ACTION_TRANSPORT_PLAY:
state.transport_state = TRANSPORT_PLAYING;
return state;
state->transport_state = TRANSPORT_PLAYING;
return;
case ACTION_TRANSPORT_PAUSE:
state.transport_state = TRANSPORT_PAUSED;
return state;
state->transport_state = TRANSPORT_PAUSED;
return;
case ACTION_TRANSPORT_STOP:
state.transport_state = TRANSPORT_STOPPED;
state.clock_count = 0;
state.beat_position = 0;
state.bar_position = 0;
state.sample_position = 0;
state.sample_accumulator = 0.0;
return state;
state->transport_state = TRANSPORT_STOPPED;
state->clock_count = 0;
state->beat_position = 0;
state->bar_position = 0;
state->sample_position = 0;
state->sample_accumulator = 0.0;
return;
case ACTION_TRANSPORT_TOGGLE_PLAY:
state.transport_state = (state.transport_state == TRANSPORT_PLAYING)
state->transport_state = (state->transport_state == TRANSPORT_PLAYING)
? TRANSPORT_PAUSED : TRANSPORT_PLAYING;
return state;
return;
case ACTION_SET_CLOCK_SOURCE:
state.clock_source = action.data.set_clock_source.source;
state.clock_count = 0;
state.beat_position = 0;
state.bar_position = 0;
state.sample_position = 0;
state.sample_accumulator = 0.0;
return state;
state->clock_source = action.data.set_clock_source.source;
state->clock_count = 0;
state->beat_position = 0;
state->bar_position = 0;
state->sample_position = 0;
state->sample_accumulator = 0.0;
return;
case ACTION_SET_BPM:
state.bpm = action.data.set_bpm.bpm;
state.samples_per_beat = (state.sample_rate * 60.0) / state.bpm;
return state;
state->bpm = action.data.set_bpm.bpm;
state->samples_per_beat = (state->sample_rate * 60.0) / state->bpm;
return;
case ACTION_RESET_TRANSPORT:
state.transport_state = TRANSPORT_STOPPED;
state.clock_count = 0;
state.beat_position = 0;
state.bar_position = 0;
state.sample_position = 0;
state.sample_accumulator = 0.0;
return state;
state->transport_state = TRANSPORT_STOPPED;
state->clock_count = 0;
state->beat_position = 0;
state->bar_position = 0;
state->sample_position = 0;
state->sample_accumulator = 0.0;
return;
case ACTION_UNDO:
return undo_action(state);
undo_action(state);
return;
case ACTION_REDO:
return redo_action(state);
redo_action(state);
return;
case ACTION_SAVE_CLIP: {
int clip_idx = action.data.save_clip.clip_index;
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
Clip *clip = &state->clips[clip_idx];
if (clip->buffer && clip->buffer_size > 0) {
char filepath[512];
snprintf(filepath, sizeof(filepath), "samples/clip_%d.wav", clip_idx);
mkdir("samples", 0755);
save_wav_float(filepath, clip->buffer, clip->buffer_size, state.sample_rate);
save_wav_float(filepath, clip->buffer, clip->buffer_size, state->sample_rate);
printf("Saved clip %d to %s\n", clip_idx, filepath);
}
}
return state;
return;
}
case ACTION_LOAD_CLIP: {
int clip_idx = action.data.load_clip.clip_index;
if (clip_idx >= 0 && clip_idx < MAX_CLIPS) {
Clip *clip = &state.clips[clip_idx];
Clip *clip = &state->clips[clip_idx];
float *new_buffer = NULL;
size_t num_samples = 0;
unsigned int file_sample_rate = 0;
@@ -444,36 +439,38 @@ AppState reducer(AppState state, Action action) {
free(new_buffer);
}
}
return state;
return;
}
case ACTION_MIDI_NOTE_ON: {
int clip_index = action.data.midi_note_on.note % MAX_CLIPS;
save_undo_state(&state, clip_index);
return clip_trigger(state, clip_index);
save_undo_state(state, clip_index);
clip_trigger(state, clip_index);
return;
}
case ACTION_MIDI_SCENE_LAUNCH: {
return scene_trigger(state, action.data.midi_scene_launch.scene_index);
scene_trigger(state, action.data.midi_scene_launch.scene_index);
return;
}
case ACTION_RACK_ADD_PLUGIN: {
int channel = action.data.rack_add_plugin.channel;
if (channel >= 0 && channel < MAX_CHANNELS) {
carla_add_plugin(&state.carla_host, channel,
carla_add_plugin(&state->carla_host, channel,
action.data.rack_add_plugin.uri,
action.data.rack_add_plugin.type);
}
return state;
return;
}
case ACTION_RACK_REMOVE_PLUGIN: {
int channel = action.data.rack_remove_plugin.channel;
int plugin_idx = action.data.rack_remove_plugin.plugin_index;
if (channel >= 0 && channel < MAX_CHANNELS) {
carla_remove_plugin(&state.carla_host, channel, plugin_idx);
carla_remove_plugin(&state->carla_host, channel, plugin_idx);
}
return state;
return;
}
case ACTION_RACK_SET_PARAMETER: {
@@ -482,68 +479,69 @@ AppState reducer(AppState state, Action action) {
int param_idx = action.data.rack_set_parameter.param_index;
float value = action.data.rack_set_parameter.value;
if (channel >= 0 && channel < MAX_CHANNELS) {
carla_set_parameter(&state.carla_host, channel, plugin_idx, param_idx, value);
carla_set_parameter(&state->carla_host, channel, plugin_idx, param_idx, value);
}
return state;
return;
}
case ACTION_RACK_SET_VOLUME: {
int channel = action.data.rack_set_volume.channel;
float volume = action.data.rack_set_volume.volume;
if (channel >= 0 && channel < MAX_CHANNELS) {
carla_set_channel_volume(&state.carla_host, channel, volume);
carla_set_channel_volume(&state->carla_host, channel, volume);
}
return state;
return;
}
case ACTION_RACK_BYPASS: {
int channel = action.data.rack_bypass.channel;
bool bypass = action.data.rack_bypass.bypass;
if (channel >= 0 && channel < MAX_CHANNELS) {
state.carla_host.channel_racks[channel].bypassed = bypass;
state->carla_host.channel_racks[channel].bypassed = bypass;
}
return state;
return;
}
case ACTION_PROCESS_AUDIO:
return state;
return;
case ACTION_SET_SHOW_MIDI_GRID:
state.show_midi_grid = action.data.set_show_midi_grid.show;
return state;
state->show_midi_grid = action.data.set_show_midi_grid.show;
return;
case ACTION_MIDI_CLIP_TRIGGER:
return midi_clip_trigger(state, action.data.midi_clip_trigger.clip_index);
midi_clip_trigger(state, action.data.midi_clip_trigger.clip_index);
return;
case ACTION_MIDI_CLIP_RESET: {
int idx = action.data.midi_clip_reset.clip_index;
if (idx >= 0 && idx < MAX_CLIPS) {
state.midi_clips[idx].state = CLIP_EMPTY;
state.midi_clips[idx].event_count = 0;
state.midi_clips[idx].read_index = 0;
state->midi_clips[idx].state = CLIP_EMPTY;
state->midi_clips[idx].event_count = 0;
state->midi_clips[idx].read_index = 0;
// Don't free events here - just reset count
}
return state;
return;
}
case ACTION_SET_CHANNEL_NAME: {
int ch = action.data.set_channel_name.channel;
if (ch >= 0 && ch < MAX_CHANNELS) {
strncpy(state.channel_names[ch], action.data.set_channel_name.name, 63);
state.channel_names[ch][63] = '\0';
strncpy(state->channel_names[ch], action.data.set_channel_name.name, 63);
state->channel_names[ch][63] = '\0';
}
return state;
return;
}
case ACTION_SAVE_PROJECT: {
fs_save_project(action.data.save_project.filename, &state);
return state;
fs_save_project(action.data.save_project.filename, state);
return;
}
case ACTION_LOAD_PROJECT: {
// Reset clips first
for (int i = 0; i < MAX_CLIPS; i++) {
Clip *clip = &state.clips[i];
Clip *clip = &state->clips[i];
clip->state = CLIP_EMPTY;
clip->buffer_size = 0;
clip->write_position = 0;
@@ -555,7 +553,7 @@ AppState reducer(AppState state, Action action) {
}
// NEW: Reset midi clips
MidiClip *mclip = &state.midi_clips[i];
MidiClip *mclip = &state->midi_clips[i];
mclip->state = CLIP_EMPTY;
mclip->event_count = 0;
mclip->read_index = 0;
@@ -569,7 +567,7 @@ AppState reducer(AppState state, Action action) {
// Reset Carla host
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
ChannelRack *rack = &state.carla_host.channel_racks[ch];
ChannelRack *rack = &state->carla_host.channel_racks[ch];
for (int p = 0; p < rack->num_plugins; p++) {
PluginInfo *plugin = &rack->plugins[p];
if (plugin->parameters) {
@@ -589,16 +587,16 @@ AppState reducer(AppState state, Action action) {
rack->bypassed = false;
}
fs_load_project(action.data.load_project.filename, &state);
return state;
fs_load_project(action.data.load_project.filename, state);
return;
}
case ACTION_QUIT:
state.running = false;
return state;
state->running = false;
return;
default:
return state;
return;
}
}
@@ -615,7 +613,7 @@ static void* dispatcher_thread_func(void *arg) {
pthread_mutex_lock(&dispatcher.state_mutex);
if (pop_action(&action)) {
dispatcher.state = reducer(dispatcher.state, action);
reducer(&dispatcher.state, action);
notify_subscribers(&dispatcher.state);
pthread_mutex_unlock(&dispatcher.state_mutex);
} else {