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:
264
dispatcher.c
264
dispatcher.c
@@ -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 {
|
||||
|
||||
@@ -222,8 +222,8 @@ void dispatcher_get_state(AppState *out);
|
||||
AppState* dispatcher_get_state_ptr(void);
|
||||
|
||||
// ============================================================
|
||||
// Reducer - pure function
|
||||
// Reducer - pure function (takes pointer to avoid stack copy)
|
||||
// ============================================================
|
||||
AppState reducer(AppState state, Action action);
|
||||
void reducer(AppState *state, Action action);
|
||||
|
||||
#endif // DISPATCHER_H
|
||||
|
||||
118
test_engine.c
118
test_engine.c
@@ -83,7 +83,7 @@ void test_trigger_empty_starts_recording(void) {
|
||||
AppState *state = create_test_state();
|
||||
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
assert(state->clips[0].write_position == 0);
|
||||
@@ -101,14 +101,14 @@ void test_trigger_recording_starts_looping(void) {
|
||||
|
||||
// Start recording
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Simulate some recording
|
||||
state->clips[0].write_position = 100;
|
||||
|
||||
// Trigger again to stop recording and start looping
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
assert(state->clips[0].buffer_size == 100);
|
||||
assert(state->clips[0].read_position == 0);
|
||||
@@ -130,7 +130,7 @@ void test_trigger_looping_stops(void) {
|
||||
state->clips[0].read_position = 50;
|
||||
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_STOPPED);
|
||||
assert(state->clips[0].read_position == 0);
|
||||
|
||||
@@ -150,7 +150,7 @@ void test_trigger_stopped_resumes_looping(void) {
|
||||
state->clips[0].buffer_size = 100;
|
||||
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
assert(state->clips[0].read_position == 0);
|
||||
|
||||
@@ -168,21 +168,21 @@ void test_full_cycle(void) {
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
|
||||
// Empty -> Recording
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Recording -> Looping
|
||||
state->clips[0].write_position = 200;
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
assert(state->clips[0].buffer_size == 200);
|
||||
|
||||
// Looping -> Stopped
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_STOPPED);
|
||||
|
||||
// Stopped -> Looping
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -201,21 +201,21 @@ void test_multiple_clips(void) {
|
||||
Action action2 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 2 } };
|
||||
|
||||
// Clip 0: Empty -> Recording
|
||||
*state = reducer(*state, action0);
|
||||
reducer(state, action0);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 1: Empty -> Recording
|
||||
*state = reducer(*state, action1);
|
||||
reducer(state, action1);
|
||||
assert(state->clips[1].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 0: Recording -> Looping
|
||||
state->clips[0].write_position = 100;
|
||||
*state = reducer(*state, action0);
|
||||
reducer(state, action0);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
assert(state->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
||||
|
||||
// Clip 2: Empty -> Recording
|
||||
*state = reducer(*state, action2);
|
||||
reducer(state, action2);
|
||||
assert(state->clips[2].state == CLIP_RECORDING);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -236,7 +236,7 @@ void test_reset_clip(void) {
|
||||
state->clips[0].read_position = 50;
|
||||
|
||||
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_EMPTY);
|
||||
assert(state->clips[0].buffer_size == 0);
|
||||
assert(state->clips[0].write_position == 0);
|
||||
@@ -287,10 +287,10 @@ void test_invalid_clip_index(void) {
|
||||
Action reset_neg = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = -1 } };
|
||||
Action reset_max = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = MAX_CLIPS } };
|
||||
|
||||
*state = reducer(*state, action_neg);
|
||||
*state = reducer(*state, action_max);
|
||||
*state = reducer(*state, reset_neg);
|
||||
*state = reducer(*state, reset_max);
|
||||
reducer(state, action_neg);
|
||||
reducer(state, action_max);
|
||||
reducer(state, reset_neg);
|
||||
reducer(state, reset_max);
|
||||
|
||||
// Verify no state changes
|
||||
assert(state->clips[0].state == CLIP_EMPTY);
|
||||
@@ -309,14 +309,14 @@ void test_buffer_overflow(void) {
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
|
||||
// Start recording
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Fill buffer to max
|
||||
state->clips[0].write_position = MAX_BUFFER_SIZE;
|
||||
|
||||
// Trigger should stop recording and start looping
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clips[0].state == CLIP_LOOPING);
|
||||
assert(state->clips[0].buffer_size == MAX_BUFFER_SIZE);
|
||||
|
||||
@@ -356,7 +356,7 @@ void test_transport_reset(void) {
|
||||
state->sample_position = 10000;
|
||||
|
||||
Action action = { .type = ACTION_RESET_TRANSPORT };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
assert(state->transport_state == TRANSPORT_STOPPED);
|
||||
assert(state->clock_count == 0);
|
||||
@@ -381,13 +381,13 @@ void test_quantize_mode_setting(void) {
|
||||
Action action_bar = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BAR } };
|
||||
Action action_off = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } };
|
||||
|
||||
*state = reducer(*state, action_beat);
|
||||
reducer(state, action_beat);
|
||||
assert(state->quantize_mode == QUANTIZE_BEAT);
|
||||
|
||||
*state = reducer(*state, action_bar);
|
||||
reducer(state, action_bar);
|
||||
assert(state->quantize_mode == QUANTIZE_BAR);
|
||||
|
||||
*state = reducer(*state, action_off);
|
||||
reducer(state, action_off);
|
||||
assert(state->quantize_mode == QUANTIZE_OFF);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -417,11 +417,11 @@ void test_quantize_threshold_setting(void) {
|
||||
assert(state->quantize_threshold == 0);
|
||||
|
||||
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold = { .threshold = 1000 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->quantize_threshold == 1000);
|
||||
|
||||
action.data.set_quantize_threshold.threshold = 0;
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->quantize_threshold == 0);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -437,17 +437,17 @@ void test_undo_redo(void) {
|
||||
|
||||
// Trigger clip 0 (empty -> recording)
|
||||
Action trigger = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, trigger);
|
||||
reducer(state, trigger);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Undo
|
||||
Action undo = { .type = ACTION_UNDO };
|
||||
*state = reducer(*state, undo);
|
||||
reducer(state, undo);
|
||||
assert(state->clips[0].state == CLIP_EMPTY);
|
||||
|
||||
// Redo
|
||||
Action redo = { .type = ACTION_REDO };
|
||||
*state = reducer(*state, redo);
|
||||
reducer(state, redo);
|
||||
assert(state->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -462,7 +462,7 @@ void test_midi_note_on(void) {
|
||||
AppState *state = create_test_state();
|
||||
|
||||
Action action = { .type = ACTION_MIDI_NOTE_ON, .data.midi_note_on = { .note = 60, .velocity = 100, .channel = 0, .time = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
int clip_idx = 60 % MAX_CLIPS;
|
||||
assert(state->clips[clip_idx].state == CLIP_RECORDING);
|
||||
@@ -479,7 +479,7 @@ void test_scene_trigger(void) {
|
||||
AppState *state = create_test_state();
|
||||
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
int clip_idx = CLIP_INDEX(0, ch);
|
||||
@@ -502,16 +502,16 @@ void test_transport_play_pause_stop(void) {
|
||||
Action stop = { .type = ACTION_TRANSPORT_STOP };
|
||||
Action toggle = { .type = ACTION_TRANSPORT_TOGGLE_PLAY };
|
||||
|
||||
*state = reducer(*state, play);
|
||||
reducer(state, play);
|
||||
assert(state->transport_state == TRANSPORT_PLAYING);
|
||||
|
||||
*state = reducer(*state, pause);
|
||||
reducer(state, pause);
|
||||
assert(state->transport_state == TRANSPORT_PAUSED);
|
||||
|
||||
*state = reducer(*state, toggle);
|
||||
reducer(state, toggle);
|
||||
assert(state->transport_state == TRANSPORT_PLAYING);
|
||||
|
||||
*state = reducer(*state, stop);
|
||||
reducer(state, stop);
|
||||
assert(state->transport_state == TRANSPORT_STOPPED);
|
||||
assert(state->clock_count == 0);
|
||||
assert(state->beat_position == 0);
|
||||
@@ -532,7 +532,7 @@ void test_bpm_setting(void) {
|
||||
assert(state->bpm == DEFAULT_BPM);
|
||||
|
||||
Action action = { .type = ACTION_SET_BPM, .data.set_bpm = { .bpm = 140.0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->bpm == 140.0);
|
||||
double expected_spp = (state->sample_rate * 60.0) / 140.0;
|
||||
assert(state->samples_per_beat == expected_spp);
|
||||
@@ -551,7 +551,7 @@ void test_clock_source_setting(void) {
|
||||
assert(state->clock_source == CLOCK_SOURCE_INTERNAL);
|
||||
|
||||
Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = CLOCK_SOURCE_MIDI } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->clock_source == CLOCK_SOURCE_MIDI);
|
||||
assert(state->clock_count == 0);
|
||||
assert(state->beat_position == 0);
|
||||
@@ -664,16 +664,16 @@ void test_save_load_clip(void) {
|
||||
|
||||
// Save clip
|
||||
Action save = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, save);
|
||||
reducer(state, save);
|
||||
|
||||
// Reset clip
|
||||
Action reset = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
|
||||
*state = reducer(*state, reset);
|
||||
reducer(state, reset);
|
||||
assert(state->clips[0].state == CLIP_EMPTY);
|
||||
|
||||
// Load clip (requires a filename; we'll use a dummy that will fail gracefully)
|
||||
Action load = { .type = ACTION_LOAD_CLIP, .data.load_clip = { .clip_index = 0, .filename = "samples/clip_0.wav" } };
|
||||
*state = reducer(*state, load);
|
||||
reducer(state, load);
|
||||
// If file doesn't exist, clip remains empty (no crash)
|
||||
// We just verify no crash
|
||||
|
||||
@@ -690,7 +690,7 @@ void test_quit_action(void) {
|
||||
assert(state->running == true);
|
||||
|
||||
Action quit = { .type = ACTION_QUIT };
|
||||
*state = reducer(*state, quit);
|
||||
reducer(state, quit);
|
||||
assert(state->running == false);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -724,7 +724,7 @@ void test_midi_clip_trigger_empty_starts_recording(void) {
|
||||
AppState *state = create_test_state();
|
||||
|
||||
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
||||
assert(state->midi_clips[0].event_count == 0);
|
||||
@@ -743,14 +743,14 @@ void test_midi_clip_trigger_recording_starts_looping(void) {
|
||||
|
||||
// Start recording
|
||||
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Simulate some recorded events
|
||||
state->midi_clips[0].event_count = 10;
|
||||
|
||||
// Trigger again to stop recording and start looping
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
||||
assert(state->midi_clips[0].read_index == 0);
|
||||
assert(state->midi_clips[0].event_count == 10);
|
||||
@@ -772,7 +772,7 @@ void test_midi_clip_trigger_looping_stops(void) {
|
||||
state->midi_clips[0].read_index = 5;
|
||||
|
||||
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->midi_clips[0].state == CLIP_STOPPED);
|
||||
assert(state->midi_clips[0].read_index == 0);
|
||||
|
||||
@@ -792,7 +792,7 @@ void test_midi_clip_trigger_stopped_resumes_looping(void) {
|
||||
state->midi_clips[0].event_count = 10;
|
||||
|
||||
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
||||
assert(state->midi_clips[0].read_index == 0);
|
||||
|
||||
@@ -844,7 +844,7 @@ void test_midi_clip_reset(void) {
|
||||
state->midi_clips[0].read_index = 10;
|
||||
|
||||
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
assert(state->midi_clips[0].state == CLIP_EMPTY);
|
||||
assert(state->midi_clips[0].event_count == 0);
|
||||
assert(state->midi_clips[0].read_index == 0);
|
||||
@@ -867,21 +867,21 @@ void test_midi_clip_multiple_clips(void) {
|
||||
Action action2 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 2 } };
|
||||
|
||||
// Clip 0: Empty -> Recording
|
||||
*state = reducer(*state, action0);
|
||||
reducer(state, action0);
|
||||
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 1: Empty -> Recording
|
||||
*state = reducer(*state, action1);
|
||||
reducer(state, action1);
|
||||
assert(state->midi_clips[1].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 0: Recording -> Looping
|
||||
state->midi_clips[0].event_count = 10;
|
||||
*state = reducer(*state, action0);
|
||||
reducer(state, action0);
|
||||
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
||||
assert(state->midi_clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
||||
|
||||
// Clip 2: Empty -> Recording
|
||||
*state = reducer(*state, action2);
|
||||
reducer(state, action2);
|
||||
assert(state->midi_clips[2].state == CLIP_RECORDING);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -901,10 +901,10 @@ void test_midi_clip_invalid_index(void) {
|
||||
Action reset_neg = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = -1 } };
|
||||
Action reset_max = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = MAX_CLIPS } };
|
||||
|
||||
*state = reducer(*state, action_neg);
|
||||
*state = reducer(*state, action_max);
|
||||
*state = reducer(*state, reset_neg);
|
||||
*state = reducer(*state, reset_max);
|
||||
reducer(state, action_neg);
|
||||
reducer(state, action_max);
|
||||
reducer(state, reset_neg);
|
||||
reducer(state, reset_max);
|
||||
|
||||
// Verify no state changes
|
||||
assert(state->midi_clips[0].state == CLIP_EMPTY);
|
||||
@@ -923,11 +923,11 @@ void test_midi_clip_show_grid_toggle(void) {
|
||||
assert(state->show_midi_grid == false);
|
||||
|
||||
Action action_on = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = true } };
|
||||
*state = reducer(*state, action_on);
|
||||
reducer(state, action_on);
|
||||
assert(state->show_midi_grid == true);
|
||||
|
||||
Action action_off = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = false } };
|
||||
*state = reducer(*state, action_off);
|
||||
reducer(state, action_off);
|
||||
assert(state->show_midi_grid == false);
|
||||
|
||||
destroy_test_state(state);
|
||||
@@ -954,7 +954,7 @@ void test_midi_clip_events_persist_after_reset(void) {
|
||||
|
||||
// Reset the clip
|
||||
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
// Events pointer should still be the same (not freed)
|
||||
assert(state->midi_clips[0].events == original_events);
|
||||
@@ -984,7 +984,7 @@ void test_midi_clip_trigger_with_events(void) {
|
||||
|
||||
// Trigger to resume looping
|
||||
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
||||
*state = reducer(*state, action);
|
||||
reducer(state, action);
|
||||
|
||||
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
||||
assert(state->midi_clips[0].event_count == 2);
|
||||
|
||||
Reference in New Issue
Block a user