699 lines
25 KiB
C
699 lines
25 KiB
C
#include "dispatcher.h"
|
|
#include "wav_io.h"
|
|
#include "fs.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
// ============================================================
|
|
// 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);
|
|
}
|