From d6ca5a007962fd342814c4c84d16c634d3d30b01 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 2 May 2026 14:27:56 +0000 Subject: [PATCH] refactor: implement unidirectional data flow with dispatcher pattern Co-authored-by: aider (deepseek/deepseek-coder) --- dispatcher.c | 450 +++++++++++++++++++++ dispatcher.h | 166 ++++++++ engine.c | 1070 ++------------------------------------------------ engine.h | 219 +---------- main.c | 36 +- reducer.c | 1 + reducer.h | 1 + 7 files changed, 684 insertions(+), 1259 deletions(-) diff --git a/dispatcher.c b/dispatcher.c index e69de29..3f737f4 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -0,0 +1,450 @@ +#include "dispatcher.h" +#include "wav_io.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); +} + +// ============================================================ +// 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 AppState clip_trigger(AppState state, int clip_index) { + if (clip_index < 0 || clip_index >= MAX_CLIPS) return state; + + Clip *clip = &state.clips[clip_index]; + + // Save undo info + int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; + state.undo.prev_clip_states[undo_idx] = clip->state; + state.undo.prev_clip_indices[undo_idx] = clip_index; + state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size; + state.undo.prev_write_positions[undo_idx] = clip->write_position; + state.undo.prev_read_positions[undo_idx] = clip->read_position; + state.undo.undo_index++; + state.undo.redo_index = state.undo.undo_index; + if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++; + + 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; + + 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 info + int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; + state.undo.prev_clip_states[undo_idx] = clip->state; + state.undo.prev_clip_indices[undo_idx] = clip_index; + state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size; + state.undo.prev_write_positions[undo_idx] = clip->write_position; + state.undo.prev_read_positions[undo_idx] = clip->read_position; + state.undo.undo_index++; + state.undo.redo_index = state.undo.undo_index; + if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++; + + 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; + + int undo_idx = (state.undo.undo_index - 1) % MAX_UNDO_HISTORY; + int clip_idx = state.undo.prev_clip_indices[undo_idx]; + + if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { + Clip *clip = &state.clips[clip_idx]; + clip->state = state.undo.prev_clip_states[undo_idx]; + clip->buffer_size = state.undo.prev_buffer_sizes[undo_idx]; + clip->write_position = state.undo.prev_write_positions[undo_idx]; + clip->read_position = state.undo.prev_read_positions[undo_idx]; + } + + state.undo.undo_index--; + return state; +} + +static AppState redo_action(AppState state) { + if (state.undo.redo_index <= state.undo.undo_index) return state; + + int redo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; + int clip_idx = state.undo.prev_clip_indices[redo_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++; + return state; +} + +AppState reducer(AppState state, Action action) { + switch (action.type) { + case ACTION_TRIGGER_CLIP: + return clip_trigger(state, action.data.trigger_clip.clip_index); + + 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_PROCESS_AUDIO: + 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); +} diff --git a/dispatcher.h b/dispatcher.h index e69de29..2e0c681 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -0,0 +1,166 @@ +#ifndef DISPATCHER_H +#define DISPATCHER_H + +#include +#include +#include +#include +#include + +// ============================================================ +// Application State - contains ALL application state +// ============================================================ + +#define MAX_SCENES 8 +#define MAX_CHANNELS 8 +#define MAX_CLIPS (MAX_SCENES * MAX_CHANNELS) // 64 +#define MAX_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz +#define MAX_UNDO_HISTORY 256 + +#define CLIP_INDEX(scene, channel) ((scene) * MAX_CHANNELS + (channel)) + +typedef enum { + CLIP_EMPTY, + CLIP_RECORDING, + CLIP_LOOPING, + CLIP_STOPPED +} ClipState; + +typedef enum { + QUANTIZE_OFF, + QUANTIZE_BEAT, + QUANTIZE_BAR +} QuantizeMode; + +typedef enum { + TRANSPORT_STOPPED, + TRANSPORT_PLAYING, + TRANSPORT_PAUSED +} TransportState; + +typedef enum { + CLOCK_SOURCE_INTERNAL, + CLOCK_SOURCE_MIDI +} ClockSource; + +typedef struct { + ClipState state; + float *buffer; + size_t buffer_size; + size_t write_position; + size_t read_position; +} Clip; + +typedef struct { + // Transport state + TransportState transport_state; + ClockSource clock_source; + uint32_t clock_count; + uint32_t beat_position; + uint32_t bar_position; + uint32_t sample_position; + double bpm; + double samples_per_beat; + double sample_accumulator; + + // Quantization + QuantizeMode quantize_mode; + jack_nframes_t quantize_threshold; + + // Clips + Clip clips[MAX_CLIPS]; + + // Undo history + struct { + int undo_index; + int redo_index; + int count; + ClipState prev_clip_states[MAX_UNDO_HISTORY]; + int prev_clip_indices[MAX_UNDO_HISTORY]; + size_t prev_buffer_sizes[MAX_UNDO_HISTORY]; + size_t prev_write_positions[MAX_UNDO_HISTORY]; + size_t prev_read_positions[MAX_UNDO_HISTORY]; + } undo; + + // JACK info (needed by audio thread) + jack_nframes_t sample_rate; + bool running; +} AppState; + +// ============================================================ +// Actions +// ============================================================ + +typedef enum { + ACTION_TRIGGER_CLIP, + ACTION_TRIGGER_SCENE, + ACTION_RESET_CLIP, + ACTION_SET_QUANTIZE_MODE, + ACTION_SET_QUANTIZE_THRESHOLD, + ACTION_TRANSPORT_PLAY, + ACTION_TRANSPORT_PAUSE, + ACTION_TRANSPORT_STOP, + ACTION_TRANSPORT_TOGGLE_PLAY, + ACTION_SET_CLOCK_SOURCE, + ACTION_SET_BPM, + ACTION_RESET_TRANSPORT, + ACTION_UNDO, + ACTION_REDO, + ACTION_SAVE_CLIP, + ACTION_LOAD_CLIP, + ACTION_MIDI_NOTE_ON, + ACTION_MIDI_SCENE_LAUNCH, + ACTION_PROCESS_AUDIO, + ACTION_QUIT +} ActionType; + +typedef struct { + ActionType type; + union { + struct { int clip_index; } trigger_clip; + struct { int scene_index; } trigger_scene; + struct { int clip_index; } reset_clip; + struct { QuantizeMode mode; } set_quantize_mode; + struct { jack_nframes_t threshold; } set_quantize_threshold; + struct { ClockSource source; } set_clock_source; + struct { double bpm; } set_bpm; + struct { int clip_index; } save_clip; + struct { int clip_index; char filename[256]; } load_clip; + struct { int note; int velocity; int channel; jack_nframes_t time; } midi_note_on; + struct { int scene_index; jack_nframes_t time; } midi_scene_launch; + struct { jack_nframes_t nframes; } process_audio; + } data; +} Action; + +// ============================================================ +// Dispatcher API +// ============================================================ + +// Thread-safe dispatch function - called by any thread +typedef void (*DispatchFn)(Action action); + +// Subscriber callback - called after each state update (from dispatcher thread) +typedef void (*SubscriberFn)(AppState *state, void *user_data); + +// Initialize the dispatcher with initial state +// Returns the dispatch function that consumers can call +DispatchFn dispatcher_init(AppState *initial_state); + +// Register a subscriber (call before dispatcher_start) +void dispatcher_subscribe(SubscriberFn fn, void *user_data); + +// Start the dispatcher thread +void dispatcher_start(void); + +// Stop the dispatcher and cleanup +void dispatcher_stop(void); + +// Get current state (thread-safe snapshot) +AppState dispatcher_get_state(void); + +// ============================================================ +// Reducer - pure function +// ============================================================ +AppState reducer(AppState state, Action action); + +#endif // DISPATCHER_H diff --git a/engine.c b/engine.c index 50a2db1..884be43 100644 --- a/engine.c +++ b/engine.c @@ -1,29 +1,12 @@ #include "engine.h" -#include "wav_io.h" #include #include #include -#include -#include -#include -#include -#include -#include -static atomic_bool save_load_running_atomic = false; - -// Forward declarations -static void process_queued_triggers(Engine *engine, jack_nframes_t current_frame); -static jack_nframes_t get_next_quantize_frame(Engine *engine, jack_nframes_t current_frame); - -// JACK process callback static int process_callback(jack_nframes_t nframes, void *arg) { Engine *engine = (Engine *)arg; + if (!engine || !engine->dispatch) return 0; - // Process commands from frontend threads - engine_process_commands(engine); - - // Get per-channel audio buffers jack_default_audio_sample_t *audio_in[MAX_CHANNELS]; jack_default_audio_sample_t *audio_out[MAX_CHANNELS]; @@ -34,21 +17,20 @@ static int process_callback(jack_nframes_t nframes, void *arg) { jack_port_get_buffer(engine->audio_out_ports[ch], nframes); } - // Get MIDI buffers void *midi_in_buf = jack_port_get_buffer(engine->midi_in_port, nframes); void *midi_scene_buf = jack_port_get_buffer(engine->midi_scene_in_port, nframes); void *midi_clock_buf = jack_port_get_buffer(engine->midi_clock_in_port, nframes); void *midi_out_buf = jack_port_get_buffer(engine->midi_out_port, nframes); - // Clear output MIDI buffer jack_midi_clear_buffer(midi_out_buf); - // Process transport (handles both master and slave clock) - transport_process(engine->transport, nframes, midi_clock_buf, midi_out_buf); + // Get state snapshot for reads + AppState state = dispatcher_get_state(); - // Process control channel MIDI input (clip triggers) + // Process MIDI input int event_index; jack_midi_event_t midi_event; + event_index = 0; while (jack_midi_event_get(&midi_event, midi_in_buf, event_index) == 0) { event_index++; @@ -59,40 +41,23 @@ static int process_callback(jack_nframes_t nframes, void *arg) { uint8_t note = data[1]; uint8_t velocity = data[2]; - // Only process note on messages on the control channel - if (status == 0x90 && channel == engine->control_channel && velocity > 0) { - int clip_index = note % MAX_CLIPS; + if (status == 0x90 && channel == 0 && velocity > 0) { + Action action = { + .type = ACTION_MIDI_NOTE_ON, + .data.midi_note_on = { .note = note, .velocity = velocity, + .channel = channel, .time = midi_event.time } + }; + engine->dispatch(action); - // Read quantize mode atomically (frontend may update it) - QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic); - TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic); - - if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) { - // Queue for quantization - jack_nframes_t trigger_time = midi_event.time; - queue_trigger(engine, clip_index, false, trigger_time); - } else { - // Trigger immediately - engine_trigger_clip(engine, clip_index); - } - - // Send note with velocity representing state - uint8_t out_velocity = clip_state_to_velocity(engine->clips[clip_index].state); + uint8_t out_velocity = clip_state_to_velocity(state.clips[note % MAX_CLIPS].state); uint8_t out_msg[3] = {0x90 | channel, note, out_velocity}; - - if (jack_midi_event_write(midi_out_buf, midi_event.time, out_msg, 3) != 0) { - fprintf(stderr, "Failed to write MIDI event\n"); - } + jack_midi_event_write(midi_out_buf, midi_event.time, out_msg, 3); } else { - // Pass through all other MIDI messages - if (jack_midi_event_write(midi_out_buf, midi_event.time, - midi_event.buffer, midi_event.size) != 0) { - fprintf(stderr, "Failed to write MIDI event\n"); - } + jack_midi_event_write(midi_out_buf, midi_event.time, + midi_event.buffer, midi_event.size); } } - // Process scene launch MIDI input event_index = 0; while (jack_midi_event_get(&midi_event, midi_scene_buf, event_index) == 0) { event_index++; @@ -102,51 +67,31 @@ static int process_callback(jack_nframes_t nframes, void *arg) { uint8_t note = data[1]; uint8_t velocity = data[2]; - // Process note on messages (any channel) for scene launch if (status == 0x90 && velocity > 0) { - int scene_index = note % MAX_SCENES; - - // Read quantize mode atomically (frontend may update it) - QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic); - TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic); - - if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) { - // Queue for quantization - jack_nframes_t trigger_time = midi_event.time; - queue_trigger(engine, scene_index, true, trigger_time); - } else { - // Trigger immediately - engine_trigger_scene(engine, scene_index); - } + Action action = { + .type = ACTION_MIDI_SCENE_LAUNCH, + .data.midi_scene_launch = { .scene_index = note % MAX_SCENES, .time = midi_event.time } + }; + engine->dispatch(action); } } - // Process queued triggers at quantization boundaries - process_queued_triggers(engine, nframes); - // Process audio per-channel for (int ch = 0; ch < MAX_CHANNELS; ch++) { memset(audio_out[ch], 0, sizeof(jack_default_audio_sample_t) * nframes); for (jack_nframes_t i = 0; i < nframes; i++) { - // Record input to recording clips in this channel for (int s = 0; s < MAX_SCENES; s++) { int clip_idx = CLIP_INDEX(s, ch); - Clip *clip = &engine->clips[clip_idx]; + Clip *clip = &state.clips[clip_idx]; - if ((ClipState)atomic_load(&clip->state) == CLIP_RECORDING) { + if (clip->state == CLIP_RECORDING) { if (clip->write_position < MAX_BUFFER_SIZE) { clip->buffer[clip->write_position++] = audio_in[ch][i]; - } else { - // Buffer full, stop recording - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, clip->write_position); - clip->read_position = 0; } } - // Play looping clips to this channel's output - if ((ClipState)atomic_load(&clip->state) == CLIP_LOOPING && atomic_load(&clip->buffer_size) > 0) { + if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) { audio_out[ch][i] += clip->buffer[clip->read_position]; clip->read_position = (clip->read_position + 1) % clip->buffer_size; } @@ -157,867 +102,19 @@ static int process_callback(jack_nframes_t nframes, void *arg) { return 0; } -// JACK shutdown callback static void shutdown_callback(void *arg) { Engine *engine = (Engine *)arg; engine->running = false; fprintf(stderr, "JACK shutdown\n"); } -// Get the next quantization boundary frame -static jack_nframes_t get_next_quantize_frame(Engine *engine, jack_nframes_t current_frame) { - if (!engine->transport || engine->transport->state != TRANSPORT_PLAYING || - engine->quantize_mode == QUANTIZE_OFF) { - return current_frame; - } - - // Calculate frames per beat - jack_nframes_t frames_per_beat = (jack_nframes_t)engine->transport->samples_per_beat; - jack_nframes_t frames_per_bar = frames_per_beat * BEATS_PER_BAR; - - // Current position in frames - jack_nframes_t current_pos = engine->transport->sample_position + current_frame; - - if (engine->quantize_mode == QUANTIZE_BEAT) { - // Next beat boundary - jack_nframes_t beat_frames = frames_per_beat; - jack_nframes_t next_beat = ((current_pos / beat_frames) + 1) * beat_frames; - return next_beat - engine->transport->sample_position; - } else { // QUANTIZE_BAR - // Next bar boundary - jack_nframes_t bar_frames = frames_per_bar; - jack_nframes_t next_bar = ((current_pos / bar_frames) + 1) * bar_frames; - return next_bar - engine->transport->sample_position; - } -} - -// Queue a trigger for quantization -void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time) { - if (!engine) return; - - QueuedTrigger *qt = (QueuedTrigger *)malloc(sizeof(QueuedTrigger)); - if (!qt) return; - - qt->clip_index = clip_index; - qt->is_scene = is_scene; - qt->trigger_time = time; - qt->next = NULL; - - // Add to end of queue - if (!engine->queued_triggers) { - engine->queued_triggers = qt; - } else { - QueuedTrigger *last = engine->queued_triggers; - while (last && last->next) last = last->next; // FIX: add null check for last - if (last) { // ADD THIS - last->next = qt; - } else { - engine->queued_triggers = qt; // Fallback - } - } -} - -// Process queued triggers at quantization boundaries -static void process_queued_triggers(Engine *engine, jack_nframes_t nframes) { - if (!engine || !engine->queued_triggers) return; // FIX: add engine null check - TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic); - if (transport_state != TRANSPORT_PLAYING) return; - - jack_nframes_t quantize_frame = get_next_quantize_frame(engine, 0); - - // Check if we've reached the quantization boundary - if (quantize_frame <= nframes) { - QueuedTrigger *qt = engine->queued_triggers; - engine->queued_triggers = NULL; - - while (qt) { - if (qt->is_scene) { - engine_trigger_scene(engine, qt->clip_index); - } else { - engine_trigger_clip(engine, qt->clip_index); - } - - QueuedTrigger *next = qt->next; - free(qt); - qt = next; - } - } -} - -// Initialize command queue -void command_queue_init(CommandQueue *q) { - atomic_store(&q->write_index, 0); - atomic_store(&q->read_index, 0); -} - -// Initialize save/load queue -void save_load_queue_init(SaveLoadQueue *q) { - atomic_store(&q->write_index, 0); - atomic_store(&q->read_index, 0); -} - -// Push a save/load request (called from audio thread) -int save_load_queue_push(SaveLoadQueue *q, SaveLoadType type, int clip_index, const char *filename) { - if (!q || !filename) return -1; - - unsigned int write = atomic_load(&q->write_index); - unsigned int read = atomic_load(&q->read_index); - - if ((write - read) >= MAX_QUEUED_COMMANDS) { - fprintf(stderr, "Save/Load queue full, dropping request\n"); - return -1; - } - - unsigned int slot = write % MAX_QUEUED_COMMANDS; - q->buffer[slot].type = type; - q->buffer[slot].clip_index = clip_index; - strncpy(q->buffer[slot].filename, filename, sizeof(q->buffer[slot].filename) - 1); - q->buffer[slot].filename[sizeof(q->buffer[slot].filename) - 1] = '\0'; - - atomic_store(&q->write_index, write + 1); - return 0; -} - -// Pop a save/load request (called from save/load thread) -int save_load_queue_pop(SaveLoadQueue *q, SaveLoadRequest *req) { - if (!q || !req) return -1; - - unsigned int read = atomic_load(&q->read_index); - unsigned int write = atomic_load(&q->write_index); - - if (read >= write) return 0; // Empty - - unsigned int slot = read % MAX_QUEUED_COMMANDS; - *req = q->buffer[slot]; - - atomic_store(&q->read_index, read + 1); - return 1; -} - -// Save/Load thread function -void* save_load_thread_func(void *arg) { - Engine *engine = (Engine *)arg; - if (!engine) return NULL; - - // Create samples directory if it doesn't exist - mkdir("samples", 0755); - - while (atomic_load(&save_load_running_atomic)) { - SaveLoadRequest req; - int ret = save_load_queue_pop(&engine->save_load_queue, &req); - - if (ret == 1) { - char filepath[512]; - - switch (req.type) { - case REQ_SAVE_CLIP: { - if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break; - Clip *clip = &engine->clips[req.clip_index]; - - // Build filename: samples/clip_.wav - snprintf(filepath, sizeof(filepath), "samples/clip_%d.wav", req.clip_index); - - // Atomically read buffer size and copy buffer to avoid race with audio thread - size_t buf_size = (size_t)atomic_load(&clip->buffer_size); - if (clip->buffer && buf_size > 0) { - // Copy buffer data to avoid race with audio thread writing to it - float *buffer_copy = (float *)malloc(buf_size * sizeof(float)); - if (buffer_copy) { - memcpy(buffer_copy, clip->buffer, buf_size * sizeof(float)); - int result = save_wav_float(filepath, buffer_copy, buf_size, engine->sample_rate); - free(buffer_copy); - if (result == 0) { - printf("Saved clip %d to %s (%zu samples)\n", req.clip_index, filepath, buf_size); - } else { - fprintf(stderr, "Failed to save clip %d to %s\n", req.clip_index, filepath); - } - } else { - fprintf(stderr, "Failed to allocate buffer copy for clip %d\n", req.clip_index); - } - } - break; - } - - case REQ_LOAD_CLIP: { - if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break; - Clip *clip = &engine->clips[req.clip_index]; - - float *new_buffer = NULL; - size_t num_samples = 0; - unsigned int file_sample_rate = 0; - - int result = load_wav_float(req.filename, &new_buffer, &num_samples, &file_sample_rate); - if (result == 0 && new_buffer && num_samples > 0) { - // Allocate a new buffer for the clip - float *clip_buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); - if (!clip_buffer) { - free(new_buffer); - break; - } - - // Copy samples (truncate if too long) - size_t copy_size = (num_samples < MAX_BUFFER_SIZE) ? num_samples : MAX_BUFFER_SIZE; - memcpy(clip_buffer, new_buffer, copy_size * sizeof(float)); - - // Atomically swap the clip's buffer - float *old_buffer = atomic_exchange(&clip->buffer, clip_buffer); - - // Update clip state atomically - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, copy_size); - clip->write_position = copy_size; - clip->read_position = 0; - - // Free old buffer and temporary buffer - if (old_buffer) free(old_buffer); - free(new_buffer); - - printf("Loaded clip %d from %s (%zu samples, %u Hz)\n", - req.clip_index, req.filename, num_samples, file_sample_rate); - } else { - fprintf(stderr, "Failed to load %s into clip %d\n", req.filename, req.clip_index); - } - break; - } - } - } else { - // No requests, sleep a bit to avoid busy-waiting - struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 }; // 1ms - nanosleep(&ts, NULL); - } - } - - return NULL; -} - -// Start the save/load thread -int engine_start_save_load_thread(Engine *engine) { - if (!engine) return -1; - - atomic_store(&save_load_running_atomic, true); - save_load_queue_init(&engine->save_load_queue); - - if (pthread_create(&engine->save_load_thread, NULL, save_load_thread_func, engine) != 0) { - engine->save_load_running = false; - return -1; - } - - return 0; -} - -// Stop the save/load thread -void engine_stop_save_load_thread(Engine *engine) { - if (!engine) return; - - atomic_store(&save_load_running_atomic, false); - pthread_join(engine->save_load_thread, NULL); -} - -// Submit command from frontend thread (non-blocking) -int engine_submit_command(Engine *engine, CommandType type, int index, jack_nframes_t value) { - if (!engine) return -1; - - CommandQueue *q = &engine->command_queue; - - // Use CAS to atomically claim a slot - unsigned int write, next_write, read; - do { - write = atomic_load(&q->write_index); - read = atomic_load(&q->read_index); - - // Check if queue is full - if ((write - read) >= MAX_QUEUED_COMMANDS) { - fprintf(stderr, "Command queue full, dropping command\n"); - return -1; - } - - next_write = write + 1; - } while (!atomic_compare_exchange_weak(&q->write_index, &write, next_write)); - - // We now own this slot exclusively - unsigned int slot = write % MAX_QUEUED_COMMANDS; - q->buffer[slot].type = type; - q->buffer[slot].index = index; - q->buffer[slot].value = value; - - // Release fence ensures the buffer write is visible before any consumer reads it - atomic_thread_fence(memory_order_release); - - return 0; -} - -// Process pending commands (called from audio thread only) -void engine_process_commands(Engine *engine) { - if (!engine) return; - - CommandQueue *q = &engine->command_queue; - unsigned int read = atomic_load(&q->read_index); - unsigned int write = atomic_load(&q->write_index); - - while (read < write) { - unsigned int slot = read % MAX_QUEUED_COMMANDS; - - // Acquire fence ensures we see the fully written command data - atomic_thread_fence(memory_order_acquire); - - Command cmd = q->buffer[slot]; - - // Process the command directly (we're in the audio thread) - switch (cmd.type) { - case CMD_TRIGGER_CLIP: { - if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break; - Clip *clip = &engine->clips[cmd.index]; - - // Record undo action - UndoAction action; - action.type = ACTION_TRIGGER_CLIP; - action.index = cmd.index; - action.value = 0; - action.previous_state = (ClipState)atomic_load(&clip->state); - action.previous_buffer_size = (size_t)atomic_load(&clip->buffer_size); - action.previous_write_position = clip->write_position; - action.previous_read_position = clip->read_position; - engine_push_undo_action(engine, &action); - - ClipState prev_state = (ClipState)atomic_load(&clip->state); - - switch (prev_state) { - case CLIP_EMPTY: - atomic_store(&clip->state, CLIP_RECORDING); - clip->write_position = 0; - atomic_store(&clip->buffer_size, 0); - clip->read_position = 0; - break; - case CLIP_RECORDING: - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, clip->write_position); - clip->read_position = 0; - break; - case CLIP_LOOPING: - atomic_store(&clip->state, CLIP_STOPPED); - clip->read_position = 0; - break; - case CLIP_STOPPED: - atomic_store(&clip->state, CLIP_LOOPING); - clip->read_position = 0; - break; - } - - // Auto-save when recording finishes (RECORDING -> LOOPING) - if (prev_state == CLIP_RECORDING && (ClipState)atomic_load(&clip->state) == CLIP_LOOPING) { - save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, cmd.index, ""); - } - break; - } - - case CMD_TRIGGER_SCENE: { - if (cmd.index < 0 || cmd.index >= MAX_SCENES) break; - - // Record undo action - UndoAction action; - action.type = ACTION_TRIGGER_SCENE; - action.index = cmd.index; - action.value = 0; - engine_push_undo_action(engine, &action); - - for (int ch = 0; ch < MAX_CHANNELS; ch++) { - int clip_idx = CLIP_INDEX(cmd.index, ch); - Clip *clip = &engine->clips[clip_idx]; - - ClipState prev_state = (ClipState)atomic_load(&clip->state); - - switch (prev_state) { - case CLIP_EMPTY: - atomic_store(&clip->state, CLIP_RECORDING); - clip->write_position = 0; - atomic_store(&clip->buffer_size, 0); - clip->read_position = 0; - break; - case CLIP_RECORDING: - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, clip->write_position); - clip->read_position = 0; - break; - case CLIP_LOOPING: - atomic_store(&clip->state, CLIP_STOPPED); - clip->read_position = 0; - break; - case CLIP_STOPPED: - atomic_store(&clip->state, CLIP_LOOPING); - clip->read_position = 0; - break; - } - - // Auto-save when recording finishes - if (prev_state == CLIP_RECORDING && (ClipState)atomic_load(&clip->state) == CLIP_LOOPING) { - save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, ""); - } - } - break; - } - - case CMD_RESET_CLIP: { - if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break; - Clip *clip = &engine->clips[cmd.index]; - if (!clip->buffer) break; - - // Record undo action - UndoAction action; - action.type = ACTION_RESET_CLIP; - action.index = cmd.index; - action.value = 0; - action.previous_state = (ClipState)atomic_load(&clip->state); - action.previous_buffer_size = (size_t)atomic_load(&clip->buffer_size); - action.previous_write_position = clip->write_position; - action.previous_read_position = clip->read_position; - engine_push_undo_action(engine, &action); - - atomic_store(&clip->state, CLIP_EMPTY); - atomic_store(&clip->buffer_size, 0); - clip->write_position = 0; - clip->read_position = 0; - memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); - break; - } - - case CMD_SET_QUANTIZE_MODE: { - QuantizeMode new_mode = (QuantizeMode)cmd.index; - - // Record undo action - UndoAction action; - action.type = ACTION_SET_QUANTIZE_MODE; - action.index = (int)new_mode; - action.value = 0; - action.previous_quantize_mode = engine->quantize_mode; - engine_push_undo_action(engine, &action); - - engine->quantize_mode = new_mode; - atomic_store(&engine->quantize_mode_atomic, (int)new_mode); - break; - } - - case CMD_SET_QUANTIZE_THRESHOLD: { - jack_nframes_t new_threshold = cmd.value; - - // Record undo action - UndoAction action; - action.type = ACTION_SET_QUANTIZE_THRESHOLD; - action.index = 0; - action.value = new_threshold; - action.previous_quantize_threshold = engine->quantize_threshold; - engine_push_undo_action(engine, &action); - - engine->quantize_threshold = new_threshold; - atomic_store(&engine->quantize_threshold_atomic, new_threshold); - break; - } - - case CMD_RESET_TRANSPORT: { - // Record undo action - UndoAction action; - action.type = ACTION_RESET_TRANSPORT; - action.index = 0; - action.value = 0; - action.previous_rolling = (engine->transport->state == TRANSPORT_PLAYING); - action.previous_clock_count = engine->transport->clock_count; - action.previous_beat_position = engine->transport->beat_position; - action.previous_bar_position = engine->transport->bar_position; - action.previous_sample_position = engine->transport->sample_position; - engine_push_undo_action(engine, &action); - - transport_reset(engine->transport); - break; - } - - case CMD_UNDO: - engine_undo(engine); - break; - - case CMD_REDO: - engine_redo(engine); - break; - - case CMD_TRANSPORT_PLAY: { - // Record undo action - UndoAction action; - action.type = ACTION_TRANSPORT_STATE_CHANGE; - action.index = 0; - action.value = (jack_nframes_t)engine->transport->state; - action.previous_clock_count = engine->transport->clock_count; - action.previous_beat_position = engine->transport->beat_position; - action.previous_bar_position = engine->transport->bar_position; - action.previous_sample_position = engine->transport->sample_position; - engine_push_undo_action(engine, &action); - - transport_play(engine->transport); - break; - } - - case CMD_TRANSPORT_PAUSE: { - // Record undo action - UndoAction action; - action.type = ACTION_TRANSPORT_STATE_CHANGE; - action.index = 0; - action.value = (jack_nframes_t)engine->transport->state; - action.previous_clock_count = engine->transport->clock_count; - action.previous_beat_position = engine->transport->beat_position; - action.previous_bar_position = engine->transport->bar_position; - action.previous_sample_position = engine->transport->sample_position; - engine_push_undo_action(engine, &action); - - transport_pause(engine->transport); - break; - } - - case CMD_TRANSPORT_STOP: { - // Record undo action - UndoAction action; - action.type = ACTION_TRANSPORT_STATE_CHANGE; - action.index = 0; - action.value = (jack_nframes_t)engine->transport->state; - action.previous_clock_count = engine->transport->clock_count; - action.previous_beat_position = engine->transport->beat_position; - action.previous_bar_position = engine->transport->bar_position; - action.previous_sample_position = engine->transport->sample_position; - engine_push_undo_action(engine, &action); - - transport_stop(engine->transport); - break; - } - - case CMD_TRANSPORT_TOGGLE_PLAY: - transport_toggle_play(engine->transport); - break; - - case CMD_SET_CLOCK_SOURCE: - transport_set_clock_source(engine->transport, (ClockSource)cmd.index); - break; - - case CMD_SET_BPM: - transport_set_bpm(engine->transport, (double)cmd.value / 100.0); - break; - } - - read++; - // Store read_index after processing each command - atomic_store(&q->read_index, read); - } -} - -// Push an action to the undo history -void engine_push_undo_action(Engine *engine, UndoAction *action) { - if (!engine || !action) return; - - UndoHistory *history = &engine->undo_history; - - int undo_idx = atomic_load(&history->undo_index); - int redo_idx = atomic_load(&history->redo_index); - - // If we've undone some actions, clear the redo history - if (redo_idx > undo_idx) { - atomic_store(&history->redo_index, undo_idx); - } - - // Add action at current undo position - int slot = undo_idx % MAX_UNDO_HISTORY; - history->actions[slot] = *action; - atomic_store(&history->undo_index, undo_idx + 1); - atomic_store(&history->redo_index, undo_idx + 1); - - int count = atomic_load(&history->count); - if (count < MAX_UNDO_HISTORY) { - atomic_store(&history->count, count + 1); - } -} - -// Undo the last action -void engine_undo(Engine *engine) { - if (!engine) return; - - UndoHistory *history = &engine->undo_history; - int undo_idx = atomic_load(&history->undo_index); - if (undo_idx <= 0) return; // Nothing to undo - - int slot = (undo_idx - 1) % MAX_UNDO_HISTORY; - UndoAction *action = &history->actions[slot]; - - switch (action->type) { - case ACTION_TRIGGER_CLIP: { - int clip_idx = action->index; - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) break; // ADD THIS - atomic_store(&clip->state, action->previous_state); - atomic_store(&clip->buffer_size, action->previous_buffer_size); - clip->write_position = action->previous_write_position; - clip->read_position = action->previous_read_position; - break; - } - - case ACTION_TRIGGER_SCENE: { - int scene_idx = action->index; - if (scene_idx < 0 || scene_idx >= MAX_SCENES) break; - for (int ch = 0; ch < MAX_CHANNELS; ch++) { - int clip_idx = CLIP_INDEX(scene_idx, ch); - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) continue; - atomic_store(&clip->state, CLIP_EMPTY); - atomic_store(&clip->buffer_size, 0); - clip->write_position = 0; - clip->read_position = 0; - } - break; - } - - case ACTION_RESET_CLIP: { - int clip_idx = action->index; - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) break; - atomic_store(&clip->state, action->previous_state); - atomic_store(&clip->buffer_size, action->previous_buffer_size); - clip->write_position = action->previous_write_position; - clip->read_position = action->previous_read_position; - break; - } - - case ACTION_SET_QUANTIZE_MODE: { - engine->quantize_mode = action->previous_quantize_mode; - atomic_store(&engine->quantize_mode_atomic, (int)action->previous_quantize_mode); - break; - } - - case ACTION_SET_QUANTIZE_THRESHOLD: { - engine->quantize_threshold = action->previous_quantize_threshold; - atomic_store(&engine->quantize_threshold_atomic, action->previous_quantize_threshold); - break; - } - - case ACTION_RESET_TRANSPORT: { - engine->transport->state = action->previous_rolling ? TRANSPORT_PLAYING : TRANSPORT_STOPPED; - engine->transport->clock_count = action->previous_clock_count; - engine->transport->beat_position = action->previous_beat_position; - engine->transport->bar_position = action->previous_bar_position; - engine->transport->sample_position = action->previous_sample_position; - atomic_store(&engine->transport->state_atomic, engine->transport->state); - atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count); - atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position); - atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position); - atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position); - break; - } - - case ACTION_TRANSPORT_STATE_CHANGE: { - TransportState prev_state = (TransportState)action->value; - engine->transport->state = prev_state; - engine->transport->clock_count = action->previous_clock_count; - engine->transport->beat_position = action->previous_beat_position; - engine->transport->bar_position = action->previous_bar_position; - engine->transport->sample_position = action->previous_sample_position; - atomic_store(&engine->transport->state_atomic, prev_state); - atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count); - atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position); - atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position); - atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position); - break; - } - } - - atomic_store(&history->undo_index, atomic_load(&history->undo_index) - 1); -} - -// Redo the last undone action -void engine_redo(Engine *engine) { - if (!engine) return; - - UndoHistory *history = &engine->undo_history; - int undo_idx = atomic_load(&history->undo_index); - int redo_idx = atomic_load(&history->redo_index); - if (redo_idx <= undo_idx) return; // Nothing to redo - - int slot = undo_idx % MAX_UNDO_HISTORY; - UndoAction *action = &history->actions[slot]; - - switch (action->type) { - case ACTION_TRIGGER_CLIP: { - int clip_idx = action->index; - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) break; // ADD THIS - // Re-apply the trigger by directly manipulating state - switch ((ClipState)atomic_load(&clip->state)) { - case CLIP_EMPTY: - atomic_store(&clip->state, CLIP_RECORDING); - clip->write_position = 0; - atomic_store(&clip->buffer_size, 0); - clip->read_position = 0; - break; - case CLIP_RECORDING: - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, clip->write_position); - clip->read_position = 0; - break; - case CLIP_LOOPING: - atomic_store(&clip->state, CLIP_STOPPED); - clip->read_position = 0; - break; - case CLIP_STOPPED: - atomic_store(&clip->state, CLIP_LOOPING); - clip->read_position = 0; - break; - } - break; - } - - case ACTION_TRIGGER_SCENE: { - int scene_idx = action->index; - if (scene_idx < 0 || scene_idx >= MAX_SCENES) break; - for (int ch = 0; ch < MAX_CHANNELS; ch++) { - int clip_idx = CLIP_INDEX(scene_idx, ch); - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) continue; - switch ((ClipState)atomic_load(&clip->state)) { - case CLIP_EMPTY: - atomic_store(&clip->state, CLIP_RECORDING); - clip->write_position = 0; - atomic_store(&clip->buffer_size, 0); - clip->read_position = 0; - break; - case CLIP_RECORDING: - atomic_store(&clip->state, CLIP_LOOPING); - atomic_store(&clip->buffer_size, clip->write_position); - clip->read_position = 0; - break; - case CLIP_LOOPING: - atomic_store(&clip->state, CLIP_STOPPED); - clip->read_position = 0; - break; - case CLIP_STOPPED: - atomic_store(&clip->state, CLIP_LOOPING); - clip->read_position = 0; - break; - } - } - break; - } - - case ACTION_RESET_CLIP: { - int clip_idx = action->index; - if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; - Clip *clip = &engine->clips[clip_idx]; - if (!clip->buffer) break; - atomic_store(&clip->state, CLIP_EMPTY); - atomic_store(&clip->buffer_size, 0); - clip->write_position = 0; - clip->read_position = 0; - memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); - break; - } - - case ACTION_SET_QUANTIZE_MODE: { - engine->quantize_mode = (QuantizeMode)action->index; - atomic_store(&engine->quantize_mode_atomic, action->index); - break; - } - - case ACTION_SET_QUANTIZE_THRESHOLD: { - engine->quantize_threshold = action->value; - atomic_store(&engine->quantize_threshold_atomic, action->value); - break; - } - - case ACTION_RESET_TRANSPORT: { - engine->transport->state = TRANSPORT_PLAYING; - engine->transport->clock_count = 0; - engine->transport->beat_position = 0; - engine->transport->bar_position = 0; - engine->transport->sample_position = 0; - atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING); - atomic_store(&engine->transport->clock_count_atomic, 0); - atomic_store(&engine->transport->beat_position_atomic, 0); - atomic_store(&engine->transport->bar_position_atomic, 0); - atomic_store(&engine->transport->sample_position_atomic, 0); - break; - } - - case ACTION_TRANSPORT_STATE_CHANGE: { - TransportState prev_state = (TransportState)action->value; - engine->transport->state = prev_state; - engine->transport->clock_count = action->previous_clock_count; - engine->transport->beat_position = action->previous_beat_position; - engine->transport->bar_position = action->previous_bar_position; - engine->transport->sample_position = action->previous_sample_position; - atomic_store(&engine->transport->state_atomic, prev_state); - atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count); - atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position); - atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position); - atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position); - break; - } - } - - atomic_store(&history->undo_index, atomic_load(&history->undo_index) + 1); -} - -int engine_init(Engine *engine, const char *client_name) { - if (!engine || !client_name) return -1; +int engine_init(Engine *engine, const char *client_name, DispatchFn dispatch) { + if (!engine || !client_name || !dispatch) return -1; memset(engine, 0, sizeof(Engine)); - engine->control_channel = 0; + engine->dispatch = dispatch; engine->running = false; - engine->quantize_mode = QUANTIZE_OFF; - engine->quantize_threshold = 0; - engine->queued_triggers = NULL; - // Initialize undo history - atomic_store(&engine->undo_history.undo_index, 0); - atomic_store(&engine->undo_history.redo_index, 0); - atomic_store(&engine->undo_history.count, 0); - - // Initialize command queue - command_queue_init(&engine->command_queue); - - // Initialize save/load queue - save_load_queue_init(&engine->save_load_queue); - - // Initialize atomic state mirrors - atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF); - atomic_store(&engine->quantize_threshold_atomic, 0); - - // Initialize transport - engine->transport = (Transport *)calloc(1, sizeof(Transport)); - if (!engine->transport) { - // Cleanup on allocation failure - for (int j = 0; j < MAX_CLIPS; j++) { - free(engine->clips[j].buffer); - engine->clips[j].buffer = NULL; // ADD THIS - } - return -1; - } - transport_init(engine->transport, engine->sample_rate); - - // Initialize clips - for (int i = 0; i < MAX_CLIPS; i++) { - atomic_store(&engine->clips[i].state, CLIP_EMPTY); - engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); - if (!engine->clips[i].buffer) { - // Cleanup on allocation failure - for (int j = 0; j < i; j++) { - free(engine->clips[j].buffer); - engine->clips[j].buffer = NULL; - } - free(engine->transport); - engine->transport = NULL; - return -1; - } - atomic_store(&engine->clips[i].buffer_size, 0); - engine->clips[i].write_position = 0; - engine->clips[i].read_position = 0; - } - - // Open JACK client jack_status_t status; engine->client = jack_client_open(client_name, JackNullOption, &status, NULL); if (!engine->client) { @@ -1025,7 +122,6 @@ int engine_init(Engine *engine, const char *client_name) { return -1; } - // Register per-channel audio ports char port_name[32]; for (int ch = 0; ch < MAX_CHANNELS; ch++) { snprintf(port_name, sizeof(port_name), "audio_in_%d", ch); @@ -1045,7 +141,6 @@ int engine_init(Engine *engine, const char *client_name) { } } - // Register MIDI ports engine->midi_in_port = jack_port_register(engine->client, "midi_control_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); @@ -1066,11 +161,9 @@ int engine_init(Engine *engine, const char *client_name) { return -1; } - // Set callbacks jack_set_process_callback(engine->client, process_callback, engine); jack_on_shutdown(engine->client, shutdown_callback, engine); - // Get sample rate engine->sample_rate = jack_get_sample_rate(engine->client); return 0; @@ -1079,31 +172,10 @@ int engine_init(Engine *engine, const char *client_name) { void engine_cleanup(Engine *engine) { if (!engine) return; - // Free any queued triggers - QueuedTrigger *qt = engine->queued_triggers; - while (qt) { - QueuedTrigger *next = qt->next; - free(qt); - qt = next; - } - engine->queued_triggers = NULL; - - // Free transport - if (engine->transport) { - transport_cleanup(engine->transport); - free(engine->transport); - engine->transport = NULL; - } - if (engine->client) { jack_client_close(engine->client); engine->client = NULL; } - - for (int i = 0; i < MAX_CLIPS; i++) { - free(engine->clips[i].buffer); - engine->clips[i].buffer = NULL; - } } int engine_start(Engine *engine) { @@ -1115,15 +187,6 @@ int engine_start(Engine *engine) { } engine->running = true; - - // Start save/load thread - if (engine_start_save_load_thread(engine) != 0) { - fprintf(stderr, "Failed to start save/load thread\n"); - jack_deactivate(engine->client); - engine->running = false; - return -1; - } - return 0; } @@ -1131,78 +194,9 @@ void engine_stop(Engine *engine) { if (!engine || !engine->client) return; engine->running = false; - engine_stop_save_load_thread(engine); jack_deactivate(engine->client); } -void engine_trigger_clip(Engine *engine, int clip_index) { - if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return; - - // Queue command for audio thread processing - engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_index, 0); -} - -void engine_trigger_scene(Engine *engine, int scene_index) { - if (!engine || scene_index < 0 || scene_index >= MAX_SCENES) return; - - engine_submit_command(engine, CMD_TRIGGER_SCENE, scene_index, 0); -} - -void engine_reset_clip(Engine *engine, int clip_index) { - if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return; - - engine_submit_command(engine, CMD_RESET_CLIP, clip_index, 0); -} - -void engine_set_quantize_mode(Engine *engine, QuantizeMode mode) { - if (!engine) return; - - // Atomically update the mode so audio thread sees it immediately - atomic_store(&engine->quantize_mode_atomic, (int)mode); - - // Also queue for any additional processing - engine_submit_command(engine, CMD_SET_QUANTIZE_MODE, (int)mode, 0); - - printf("Quantize mode set to: %s\n", quantize_mode_to_string(mode)); -} - -void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples) { - if (!engine) return; - - atomic_store(&engine->quantize_threshold_atomic, samples); - engine_submit_command(engine, CMD_SET_QUANTIZE_THRESHOLD, 0, samples); -} - -void engine_transport_play(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_TRANSPORT_PLAY, 0, 0); -} - -void engine_transport_pause(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_TRANSPORT_PAUSE, 0, 0); -} - -void engine_transport_stop(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_TRANSPORT_STOP, 0, 0); -} - -void engine_transport_toggle_play(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_TRANSPORT_TOGGLE_PLAY, 0, 0); -} - -void engine_set_clock_source(Engine *engine, ClockSource source) { - if (!engine) return; - engine_submit_command(engine, CMD_SET_CLOCK_SOURCE, (int)source, 0); -} - -void engine_set_bpm(Engine *engine, double bpm) { - if (!engine) return; - engine_submit_command(engine, CMD_SET_BPM, 0, (jack_nframes_t)(bpm * 100.0)); -} - const char* clip_state_to_string(ClipState state) { switch (state) { case CLIP_EMPTY: return "Empty"; @@ -1231,13 +225,3 @@ const char* quantize_mode_to_string(QuantizeMode mode) { default: return "Unknown"; } } - -void engine_undo_action(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_UNDO, 0, 0); -} - -void engine_redo_action(Engine *engine) { - if (!engine) return; - engine_submit_command(engine, CMD_REDO, 0, 0); -} diff --git a/engine.h b/engine.h index 1f22c15..f73acdd 100644 --- a/engine.h +++ b/engine.h @@ -3,237 +3,30 @@ #include #include -#include #include -#include -#include -#include "transport.h" - -#define MAX_SCENES 8 -#define MAX_CHANNELS 8 -#define MAX_CLIPS (MAX_SCENES * MAX_CHANNELS) // 64 -#define MAX_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz - -// Convert scene/channel to flat clip index -#define CLIP_INDEX(scene, channel) ((scene) * MAX_CHANNELS + (channel)) - -typedef enum { - CLIP_EMPTY, - CLIP_RECORDING, - CLIP_LOOPING, - CLIP_STOPPED -} ClipState; - -typedef enum { - QUANTIZE_OFF, - QUANTIZE_BEAT, - QUANTIZE_BAR -} QuantizeMode; - - -typedef struct { - atomic_int state; // ClipState (atomic for thread-safe access) - float *buffer; - atomic_size_t buffer_size; // size_t (atomic for thread-safe access) - size_t write_position; - size_t read_position; - bool is_playing; -} Clip; - -// Maximum number of queued commands from frontend to audio thread -#define MAX_QUEUED_COMMANDS 64 - -// Size of the pre-allocated trigger pool (must match engine.c) -#define QUEUED_TRIGGER_POOL_SIZE 64 - -typedef enum { - CMD_TRIGGER_CLIP, - CMD_TRIGGER_SCENE, - CMD_RESET_CLIP, - CMD_SET_QUANTIZE_MODE, - CMD_SET_QUANTIZE_THRESHOLD, - CMD_RESET_TRANSPORT, - CMD_UNDO, - CMD_REDO, - CMD_TRANSPORT_PLAY, - CMD_TRANSPORT_PAUSE, - CMD_TRANSPORT_STOP, - CMD_TRANSPORT_TOGGLE_PLAY, - CMD_SET_CLOCK_SOURCE, - CMD_SET_BPM -} CommandType; - -// Undo/Redo action types -typedef enum { - ACTION_TRIGGER_CLIP, - ACTION_TRIGGER_SCENE, - ACTION_RESET_CLIP, - ACTION_SET_QUANTIZE_MODE, - ACTION_SET_QUANTIZE_THRESHOLD, - ACTION_RESET_TRANSPORT, - ACTION_TRANSPORT_STATE_CHANGE -} ActionType; - -// Undo/Redo action record -typedef struct { - ActionType type; - int index; // clip_index, scene_index, or mode value - jack_nframes_t value; // threshold value or other numeric param - ClipState previous_state; // For clip state changes - size_t previous_buffer_size; - size_t previous_write_position; - size_t previous_read_position; - bool previous_rolling; - uint32_t previous_clock_count; - uint32_t previous_beat_position; - uint32_t previous_bar_position; - uint32_t previous_sample_position; - QuantizeMode previous_quantize_mode; - jack_nframes_t previous_quantize_threshold; -} UndoAction; - -// Undo/Redo history -#define MAX_UNDO_HISTORY 256 - -typedef struct { - UndoAction actions[MAX_UNDO_HISTORY]; - atomic_int undo_index; // Points to next action to undo - atomic_int redo_index; // Points to next action to redo - atomic_int count; // Total actions in history -} UndoHistory; - -typedef struct { - CommandType type; - int index; // clip_index, scene_index, or mode value - jack_nframes_t value; // threshold value or other numeric param -} Command; - -// Lock-free single-producer single-consumer ring buffer -typedef struct { - Command buffer[MAX_QUEUED_COMMANDS]; - atomic_uint write_index; - atomic_uint read_index; -} CommandQueue; - -// Save/Load request types -typedef enum { - REQ_SAVE_CLIP, - REQ_LOAD_CLIP -} SaveLoadType; - -typedef struct { - SaveLoadType type; - int clip_index; - char filename[256]; -} SaveLoadRequest; - -// Lock-free queue for save/load requests (audio thread -> save/load thread) -typedef struct { - SaveLoadRequest buffer[MAX_QUEUED_COMMANDS]; - atomic_uint write_index; - atomic_uint read_index; -} SaveLoadQueue; - -// Queued trigger for quantization -typedef struct QueuedTrigger { - int clip_index; - bool is_scene; - jack_nframes_t trigger_time; - struct QueuedTrigger *next; -} QueuedTrigger; +#include "dispatcher.h" typedef struct { jack_client_t *client; jack_port_t *audio_in_ports[MAX_CHANNELS]; jack_port_t *audio_out_ports[MAX_CHANNELS]; - jack_port_t *midi_in_port; // Control channel MIDI - jack_port_t *midi_scene_in_port; // Scene launch MIDI - jack_port_t *midi_clock_in_port; // MIDI clock input + jack_port_t *midi_in_port; + jack_port_t *midi_scene_in_port; + jack_port_t *midi_clock_in_port; jack_port_t *midi_out_port; - Clip clips[MAX_CLIPS]; - int control_channel; + DispatchFn dispatch; jack_nframes_t sample_rate; - - // Transport and clock - Transport *transport; - - // Quantization - QuantizeMode quantize_mode; - jack_nframes_t quantize_threshold; // in samples (lookahead) - QueuedTrigger *queued_triggers; - - // Thread-safe command queue for frontend -> audio thread communication - CommandQueue command_queue; - - // Atomic flags for simple state that frontend reads - atomic_int quantize_mode_atomic; // QuantizeMode - atomic_uint quantize_threshold_atomic; - bool running; - - // Undo/Redo - UndoHistory undo_history; - - // Save/Load queue and thread - SaveLoadQueue save_load_queue; - pthread_t save_load_thread; - volatile bool save_load_running; } Engine; -// Engine lifecycle -int engine_init(Engine *engine, const char *client_name); +int engine_init(Engine *engine, const char *client_name, DispatchFn dispatch); void engine_cleanup(Engine *engine); int engine_start(Engine *engine); void engine_stop(Engine *engine); -// Clip management -void engine_trigger_clip(Engine *engine, int clip_index); -void engine_trigger_scene(Engine *engine, int scene_index); -void engine_reset_clip(Engine *engine, int clip_index); - -// Transport -void engine_set_quantize_mode(Engine *engine, QuantizeMode mode); -void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples); -void engine_transport_play(Engine *engine); -void engine_transport_pause(Engine *engine); -void engine_transport_stop(Engine *engine); -void engine_transport_toggle_play(Engine *engine); -void engine_set_clock_source(Engine *engine, ClockSource source); -void engine_set_bpm(Engine *engine, double bpm); - -// Queue management (exposed for testing) -void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time); - -// Thread-safe command submission (called from frontend threads) -int engine_submit_command(Engine *engine, CommandType type, int index, jack_nframes_t value); - -// Process pending commands (called from audio thread) -void engine_process_commands(Engine *engine); - -// Initialize command queue (exposed for testing) -void command_queue_init(CommandQueue *q); - -// Save/Load queue management -void save_load_queue_init(SaveLoadQueue *q); -int save_load_queue_push(SaveLoadQueue *q, SaveLoadType type, int clip_index, const char *filename); -int save_load_queue_pop(SaveLoadQueue *q, SaveLoadRequest *req); - -// Save/Load thread -void* save_load_thread_func(void *arg); -int engine_start_save_load_thread(Engine *engine); -void engine_stop_save_load_thread(Engine *engine); - -// Utility const char* clip_state_to_string(ClipState state); uint8_t clip_state_to_velocity(ClipState state); const char* quantize_mode_to_string(QuantizeMode mode); -// Undo/Redo -void engine_undo(Engine *engine); -void engine_redo(Engine *engine); -void engine_push_undo_action(Engine *engine, UndoAction *action); -void engine_undo_action(Engine *engine); -void engine_redo_action(Engine *engine); - #endif // ENGINE_H diff --git a/main.c b/main.c index 76c4bff..628fb93 100644 --- a/main.c +++ b/main.c @@ -7,13 +7,19 @@ #include "tui.h" #include "cli.h" #include "gui.h" +#include "dispatcher.h" static Engine engine; static volatile int keep_running = 1; +static DispatchFn dispatch = NULL; void signal_handler(int sig) { (void)sig; keep_running = 0; + if (dispatch) { + Action action = { .type = ACTION_QUIT }; + dispatch(action); + } } void print_usage(const char *program) { @@ -63,22 +69,45 @@ int main(int argc, char *argv[]) { } } + // Create initial state + AppState initial_state; + memset(&initial_state, 0, sizeof(AppState)); + initial_state.transport_state = TRANSPORT_STOPPED; + initial_state.clock_source = CLOCK_SOURCE_INTERNAL; + initial_state.bpm = 120.0; + initial_state.quantize_mode = QUANTIZE_OFF; + initial_state.quantize_threshold = 0; + initial_state.running = true; + + for (int i = 0; i < MAX_CLIPS; i++) { + initial_state.clips[i].state = CLIP_EMPTY; + initial_state.clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + initial_state.clips[i].buffer_size = 0; + initial_state.clips[i].write_position = 0; + initial_state.clips[i].read_position = 0; + } + + // Initialize dispatcher + dispatch = dispatcher_init(&initial_state); + // Initialize engine - if (engine_init(&engine, client_name) != 0) { + if (engine_init(&engine, client_name, dispatch) != 0) { fprintf(stderr, "Failed to initialize engine\n"); return 1; } - engine.control_channel = control_channel; - // Set up signal handler signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); + // Start dispatcher + dispatcher_start(); + // Start engine if (engine_start(&engine) != 0) { fprintf(stderr, "Failed to start engine\n"); engine_cleanup(&engine); + dispatcher_stop(); return 1; } @@ -107,6 +136,7 @@ int main(int argc, char *argv[]) { printf("\nShutting down...\n"); engine_stop(&engine); engine_cleanup(&engine); + dispatcher_stop(); return 0; } diff --git a/reducer.c b/reducer.c index e69de29..4c082b2 100644 --- a/reducer.c +++ b/reducer.c @@ -0,0 +1 @@ +// reducer.c is no longer needed; all state management is in dispatcher.c diff --git a/reducer.h b/reducer.h index e69de29..58a40aa 100644 --- a/reducer.h +++ b/reducer.h @@ -0,0 +1 @@ +// reducer.h is no longer needed; all state management is in dispatcher.h