#include "dispatcher.h" #include "wav_io.h" #include "fs.h" #include #include #include #include // ============================================================ // Internal dispatcher state // ============================================================ typedef struct SubscriberNode { SubscriberFn fn; void *user_data; struct SubscriberNode *next; } SubscriberNode; static struct { AppState state; atomic_bool running; pthread_t thread; pthread_mutex_t state_mutex; pthread_mutex_t subscribers_mutex; pthread_cond_t action_cond; // Lock-free queue for actions (multiple producers, single consumer) Action action_buffer[256]; atomic_uint write_index; atomic_uint read_index; SubscriberNode *subscribers; } dispatcher; // ============================================================ // Queue operations // ============================================================ static bool push_action(Action action) { unsigned int write = atomic_load(&dispatcher.write_index); unsigned int read = atomic_load(&dispatcher.read_index); if ((write - read) >= 256) { fprintf(stderr, "Dispatcher action queue full\n"); return false; } unsigned int slot = write % 256; dispatcher.action_buffer[slot] = action; atomic_store(&dispatcher.write_index, write + 1); return true; } static bool pop_action(Action *action) { unsigned int read = atomic_load(&dispatcher.read_index); unsigned int write = atomic_load(&dispatcher.write_index); if (read >= write) return false; unsigned int slot = read % 256; *action = dispatcher.action_buffer[slot]; atomic_store(&dispatcher.read_index, read + 1); return true; } // ============================================================ // Dispatch function (thread-safe, called by any thread) // ============================================================ static void dispatch_function(Action action) { push_action(action); pthread_cond_signal(&dispatcher.action_cond); // Trigger autosave for non-quit actions if (action.type != ACTION_QUIT) { fs_trigger_autosave(); } } // ============================================================ // Subscriber management // ============================================================ void dispatcher_subscribe(SubscriberFn fn, void *user_data) { pthread_mutex_lock(&dispatcher.subscribers_mutex); SubscriberNode *node = malloc(sizeof(SubscriberNode)); if (node) { node->fn = fn; node->user_data = user_data; node->next = dispatcher.subscribers; dispatcher.subscribers = node; } pthread_mutex_unlock(&dispatcher.subscribers_mutex); } static void notify_subscribers(AppState *state) { pthread_mutex_lock(&dispatcher.subscribers_mutex); SubscriberNode *node = dispatcher.subscribers; while (node) { node->fn(state, node->user_data); node = node->next; } pthread_mutex_unlock(&dispatcher.subscribers_mutex); } // ============================================================ // State access // ============================================================ void dispatcher_get_state(AppState *out) { pthread_mutex_lock(&dispatcher.state_mutex); *out = dispatcher.state; pthread_mutex_unlock(&dispatcher.state_mutex); } AppState* dispatcher_get_state_ptr(void) { return &dispatcher.state; } // ============================================================ // Reducer implementation // ============================================================ static void save_undo_state(AppState *state, int clip_index) { int undo_idx = state->undo.undo_index % MAX_UNDO_HISTORY; state->undo.prev_clip_states[undo_idx] = (ClipState)atomic_load(&state->clips[clip_index].state); state->undo.prev_clip_indices[undo_idx] = clip_index; state->undo.prev_buffer_sizes[undo_idx] = state->clips[clip_index].buffer_size; state->undo.prev_write_positions[undo_idx] = state->clips[clip_index].write_position; state->undo.prev_read_positions[undo_idx] = atomic_load(&state->clips[clip_index].read_position); state->undo.batch_sizes[undo_idx] = 0; // Will be set by caller state->undo.undo_index++; state->undo.redo_index = state->undo.undo_index; if (state->undo.count < MAX_UNDO_HISTORY) state->undo.count++; } static void clip_trigger(AppState *state, int clip_index) { if (clip_index < 0 || clip_index >= MAX_CLIPS) return; Clip *clip = &state->clips[clip_index]; // Do NOT save undo here - caller will do it ClipState current_state = (ClipState)atomic_load(&clip->state); switch (current_state) { case CLIP_EMPTY: atomic_store(&clip->state, CLIP_RECORDING); clip->write_position = 0; clip->buffer_size = 0; atomic_store(&clip->read_position, 0); break; case CLIP_RECORDING: { // Transition to looping: copy from ring buffer to clip buffer atomic_store(&clip->state, CLIP_LOOPING); atomic_store(&clip->read_position, 0); // Determine which channel this clip belongs to 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 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_size = to_copy; clip->write_position = to_copy; atomic_store(&state->record_read_pos[channel], wp); } break; } case CLIP_LOOPING: atomic_store(&clip->state, CLIP_STOPPED); atomic_store(&clip->read_position, 0); break; case CLIP_STOPPED: atomic_store(&clip->state, CLIP_LOOPING); atomic_store(&clip->read_position, 0); break; } } 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]; ClipState current_state = (ClipState)atomic_load(&clip->state); switch (current_state) { case CLIP_EMPTY: atomic_store(&clip->state, CLIP_RECORDING); clip->event_count = 0; clip->read_index = 0; break; case CLIP_RECORDING: atomic_store(&clip->state, CLIP_LOOPING); clip->read_index = 0; break; case CLIP_LOOPING: atomic_store(&clip->state, CLIP_STOPPED); clip->read_index = 0; break; case CLIP_STOPPED: atomic_store(&clip->state, CLIP_LOOPING); clip->read_index = 0; break; } } 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; for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = scene_index * MAX_CHANNELS + ch; save_undo_state(state, clip_idx); } // Mark all entries in this batch with the batch size 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; } // Now apply the changes for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = scene_index * MAX_CHANNELS + ch; clip_trigger(state, clip_idx); } } static void clip_reset(AppState *state, int clip_index) { if (clip_index < 0 || clip_index >= MAX_CLIPS) return; Clip *clip = &state->clips[clip_index]; save_undo_state(state, clip_index); atomic_store(&clip->state, CLIP_EMPTY); clip->buffer_size = 0; clip->write_position = 0; atomic_store(&clip->read_position, 0); if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); // 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])); } 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]; 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]; if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { Clip *clip = &state->clips[clip_idx]; atomic_store(&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]; atomic_store(&clip->read_position, state->undo.prev_read_positions[current_idx]); } } state->undo.undo_index -= batch_size; } 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]; 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]; if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { Clip *clip = &state->clips[clip_idx]; ClipState current_state = (ClipState)atomic_load(&clip->state); switch (current_state) { case CLIP_EMPTY: atomic_store(&clip->state, CLIP_RECORDING); clip->write_position = 0; clip->buffer_size = 0; atomic_store(&clip->read_position, 0); break; case CLIP_RECORDING: atomic_store(&clip->state, CLIP_LOOPING); clip->buffer_size = clip->write_position; atomic_store(&clip->read_position, 0); break; case CLIP_LOOPING: atomic_store(&clip->state, CLIP_STOPPED); atomic_store(&clip->read_position, 0); break; case CLIP_STOPPED: atomic_store(&clip->state, CLIP_LOOPING); atomic_store(&clip->read_position, 0); break; } } } state->undo.undo_index += batch_size; } 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); clip_trigger(state, clip_idx); return; } case ACTION_TRIGGER_SCENE: scene_trigger(state, action.data.trigger_scene.scene_index); return; case ACTION_RESET_CLIP: 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; case ACTION_SET_QUANTIZE_THRESHOLD: state->quantize_threshold = action.data.set_quantize_threshold.threshold; return; case ACTION_TRANSPORT_PLAY: state->transport_state = TRANSPORT_PLAYING; return; case ACTION_TRANSPORT_PAUSE: 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; case ACTION_TRANSPORT_TOGGLE_PLAY: state->transport_state = (state->transport_state == TRANSPORT_PLAYING) ? TRANSPORT_PAUSED : TRANSPORT_PLAYING; 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; case ACTION_SET_BPM: 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; case ACTION_UNDO: undo_action(state); return; case ACTION_REDO: 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]; 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); printf("Saved clip %d to %s\n", clip_idx, filepath); } } 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]; float *new_buffer = NULL; size_t num_samples = 0; unsigned int file_sample_rate = 0; if (load_wav_float(action.data.load_clip.filename, &new_buffer, &num_samples, &file_sample_rate) == 0 && new_buffer) { if (clip->buffer) free(clip->buffer); clip->buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); if (clip->buffer) { size_t copy_size = (num_samples < MAX_BUFFER_SIZE) ? num_samples : MAX_BUFFER_SIZE; memcpy(clip->buffer, new_buffer, copy_size * sizeof(float)); atomic_store(&clip->state, CLIP_LOOPING); clip->buffer_size = copy_size; clip->write_position = copy_size; atomic_store(&clip->read_position, 0); printf("Loaded clip %d from %s\n", clip_idx, action.data.load_clip.filename); } free(new_buffer); } } return; } case ACTION_MIDI_NOTE_ON: { int clip_index = action.data.midi_note_on.note % MAX_CLIPS; save_undo_state(state, clip_index); clip_trigger(state, clip_index); return; } case ACTION_MIDI_SCENE_LAUNCH: { 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, action.data.rack_add_plugin.uri, action.data.rack_add_plugin.type); } 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); } return; } case ACTION_RACK_SET_PARAMETER: { int channel = action.data.rack_set_parameter.channel; int plugin_idx = action.data.rack_set_parameter.plugin_index; 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); } 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); } 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; } return; } case ACTION_PROCESS_AUDIO: return; case ACTION_SET_SHOW_MIDI_GRID: state->show_midi_grid = action.data.set_show_midi_grid.show; return; case ACTION_MIDI_CLIP_TRIGGER: 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) { atomic_store(&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; } 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'; } return; } case ACTION_SAVE_PROJECT: { 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]; atomic_store(&clip->state, CLIP_EMPTY); clip->buffer_size = 0; clip->write_position = 0; atomic_store(&clip->read_position, 0); if (clip->buffer) { memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); } else { clip->buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); } // NEW: Reset midi clips MidiClip *mclip = &state->midi_clips[i]; atomic_store(&mclip->state, CLIP_EMPTY); mclip->event_count = 0; mclip->read_index = 0; mclip->max_events = MAX_MIDI_EVENTS; // Ensure events pointer is valid if (mclip->events == NULL) { mclip->events = (MidiEvent *)calloc(MAX_MIDI_EVENTS, sizeof(MidiEvent)); mclip->max_events = MAX_MIDI_EVENTS; } } // Reset Carla host for (int ch = 0; ch < MAX_CHANNELS; 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) { free(plugin->parameters); plugin->parameters = NULL; } if (plugin->parameter_names) { for (int pi = 0; pi < plugin->num_parameters; pi++) { free(plugin->parameter_names[pi]); } free(plugin->parameter_names); plugin->parameter_names = NULL; } } rack->num_plugins = 0; rack->volume = 1.0f; rack->bypassed = false; } fs_load_project(action.data.load_project.filename, state); return; } case ACTION_QUIT: state->running = false; return; default: return; } } // ============================================================ // Dispatcher thread // ============================================================ static void* dispatcher_thread_func(void *arg) { (void)arg; while (atomic_load(&dispatcher.running)) { Action action; pthread_mutex_lock(&dispatcher.state_mutex); if (pop_action(&action)) { reducer(&dispatcher.state, action); notify_subscribers(&dispatcher.state); pthread_mutex_unlock(&dispatcher.state_mutex); } else { struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 }; pthread_cond_timedwait(&dispatcher.action_cond, &dispatcher.state_mutex, &ts); pthread_mutex_unlock(&dispatcher.state_mutex); } } return NULL; } // ============================================================ // Public API // ============================================================ DispatchFn dispatcher_init(AppState *initial_state) { memcpy(&dispatcher.state, initial_state, sizeof(AppState)); // Initialize ring buffer positions for (int ch = 0; ch < MAX_CHANNELS; ch++) { atomic_store(&dispatcher.state.record_write_pos[ch], 0); atomic_store(&dispatcher.state.record_read_pos[ch], 0); } // NEW: Ensure midi clip events are allocated (in case initial_state has NULL pointers) for (int i = 0; i < MAX_CLIPS; i++) { if (dispatcher.state.midi_clips[i].events == NULL) { dispatcher.state.midi_clips[i].events = (MidiEvent *)calloc(MAX_MIDI_EVENTS, sizeof(MidiEvent)); dispatcher.state.midi_clips[i].max_events = MAX_MIDI_EVENTS; } atomic_store(&dispatcher.state.midi_clips[i].state, CLIP_EMPTY); } atomic_store(&dispatcher.running, false); atomic_store(&dispatcher.write_index, 0); atomic_store(&dispatcher.read_index, 0); dispatcher.subscribers = NULL; pthread_mutex_init(&dispatcher.state_mutex, NULL); pthread_mutex_init(&dispatcher.subscribers_mutex, NULL); pthread_cond_init(&dispatcher.action_cond, NULL); return dispatch_function; } void dispatcher_start(void) { atomic_store(&dispatcher.running, true); pthread_create(&dispatcher.thread, NULL, dispatcher_thread_func, NULL); } void dispatcher_stop(void) { atomic_store(&dispatcher.running, false); pthread_cond_signal(&dispatcher.action_cond); pthread_join(dispatcher.thread, NULL); // NEW: Free midi clip events for (int i = 0; i < MAX_CLIPS; i++) { free(dispatcher.state.midi_clips[i].events); dispatcher.state.midi_clips[i].events = NULL; atomic_store(&dispatcher.state.midi_clips[i].state, CLIP_EMPTY); } pthread_mutex_lock(&dispatcher.subscribers_mutex); SubscriberNode *node = dispatcher.subscribers; while (node) { SubscriberNode *next = node->next; free(node); node = next; } dispatcher.subscribers = NULL; pthread_mutex_unlock(&dispatcher.subscribers_mutex); pthread_mutex_destroy(&dispatcher.state_mutex); pthread_mutex_destroy(&dispatcher.subscribers_mutex); pthread_cond_destroy(&dispatcher.action_cond); }