diff --git a/dispatcher.c b/dispatcher.c index bc853fd..a0e3753 100644 --- a/dispatcher.c +++ b/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 { diff --git a/dispatcher.h b/dispatcher.h index 6c29ab0..e48a13e 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -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 diff --git a/test_engine.c b/test_engine.c index 2f5ebe1..493d24d 100644 --- a/test_engine.c +++ b/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);