#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 // ============================================================ AppState dispatcher_get_state(void) { pthread_mutex_lock(&dispatcher.state_mutex); AppState state = dispatcher.state; pthread_mutex_unlock(&dispatcher.state_mutex); return 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] = 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] = 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 AppState clip_trigger(AppState state, int clip_index) { if (clip_index < 0 || clip_index >= MAX_CLIPS) return state; Clip *clip = &state.clips[clip_index]; // Do NOT save undo here - caller will do it switch (clip->state) { case CLIP_EMPTY: clip->state = CLIP_RECORDING; clip->write_position = 0; clip->buffer_size = 0; clip->read_position = 0; break; case CLIP_RECORDING: clip->state = CLIP_LOOPING; clip->buffer_size = clip->write_position; clip->read_position = 0; break; case CLIP_LOOPING: clip->state = CLIP_STOPPED; clip->read_position = 0; break; case CLIP_STOPPED: clip->state = CLIP_LOOPING; clip->read_position = 0; break; } return state; } static AppState scene_trigger(AppState state, int scene_index) { if (scene_index < 0 || scene_index >= MAX_SCENES) return state; // 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 = CLIP_INDEX(scene_index, 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 = CLIP_INDEX(scene_index, ch); state = 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; Clip *clip = &state.clips[clip_index]; save_undo_state(&state, clip_index); clip->state = CLIP_EMPTY; clip->buffer_size = 0; clip->write_position = 0; clip->read_position = 0; if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); return state; } static AppState undo_action(AppState state) { if (state.undo.undo_index <= 0) return state; // 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]; 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; } static AppState redo_action(AppState state) { if (state.undo.redo_index <= state.undo.undo_index) return state; // 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]; switch (clip->state) { case CLIP_EMPTY: clip->state = CLIP_RECORDING; clip->write_position = 0; clip->buffer_size = 0; clip->read_position = 0; break; case CLIP_RECORDING: clip->state = CLIP_LOOPING; clip->buffer_size = clip->write_position; clip->read_position = 0; break; case CLIP_LOOPING: clip->state = CLIP_STOPPED; clip->read_position = 0; break; case CLIP_STOPPED: clip->state = CLIP_LOOPING; clip->read_position = 0; break; } } } state.undo.undo_index += batch_size; return state; } AppState 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); } case ACTION_TRIGGER_SCENE: return scene_trigger(state, action.data.trigger_scene.scene_index); case ACTION_RESET_CLIP: return clip_reset(state, action.data.reset_clip.clip_index); case ACTION_SET_QUANTIZE_MODE: state.quantize_mode = action.data.set_quantize_mode.mode; return state; case ACTION_SET_QUANTIZE_THRESHOLD: state.quantize_threshold = action.data.set_quantize_threshold.threshold; return state; case ACTION_TRANSPORT_PLAY: state.transport_state = TRANSPORT_PLAYING; return state; case ACTION_TRANSPORT_PAUSE: state.transport_state = TRANSPORT_PAUSED; return state; 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; case ACTION_TRANSPORT_TOGGLE_PLAY: state.transport_state = (state.transport_state == TRANSPORT_PLAYING) ? TRANSPORT_PAUSED : TRANSPORT_PLAYING; return state; 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; case ACTION_SET_BPM: state.bpm = action.data.set_bpm.bpm; state.samples_per_beat = (state.sample_rate * 60.0) / state.bpm; return state; 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; case ACTION_UNDO: return undo_action(state); case ACTION_REDO: return redo_action(state); 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 state; } 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)); clip->state = CLIP_LOOPING; clip->buffer_size = copy_size; clip->write_position = copy_size; clip->read_position = 0; printf("Loaded clip %d from %s\n", clip_idx, action.data.load_clip.filename); } free(new_buffer); } } return state; } case ACTION_MIDI_NOTE_ON: { int clip_index = action.data.midi_note_on.note % MAX_CLIPS; return clip_trigger(state, clip_index); } case ACTION_MIDI_SCENE_LAUNCH: { return scene_trigger(state, action.data.midi_scene_launch.scene_index); } 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 state; } 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 state; } 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 state; } 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 state; } 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 state; } case ACTION_PROCESS_AUDIO: return state; case ACTION_SAVE_PROJECT: { fs_save_project(action.data.save_project.filename, &state); return state; } case ACTION_LOAD_PROJECT: { // Reset clips first for (int i = 0; i < MAX_CLIPS; i++) { Clip *clip = &state.clips[i]; clip->state = CLIP_EMPTY; clip->buffer_size = 0; clip->write_position = 0; clip->read_position = 0; if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); } // 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 state; } case ACTION_QUIT: state.running = false; return state; default: return state; } } // ============================================================ // 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)) { dispatcher.state = 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) { dispatcher.state = *initial_state; 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); 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); }