refactor: implement unidirectional data flow with dispatcher pattern

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-02 14:27:56 +00:00
parent 2face8fe3d
commit d6ca5a0079
7 changed files with 684 additions and 1259 deletions

View File

@@ -0,0 +1,450 @@
#include "dispatcher.h"
#include "wav_io.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);
}
// ============================================================
// 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);
}

View File

@@ -0,0 +1,166 @@
#ifndef DISPATCHER_H
#define DISPATCHER_H
#include <stdint.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <jack/jack.h>
#include <pthread.h>
// ============================================================
// 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

1070
engine.c
View File

File diff suppressed because it is too large Load Diff

219
engine.h
View File

@@ -3,237 +3,30 @@
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <pthread.h>
#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

36
main.c
View File

@@ -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;
}

View File

@@ -0,0 +1 @@
// reducer.c is no longer needed; all state management is in dispatcher.c

View File

@@ -0,0 +1 @@
// reducer.h is no longer needed; all state management is in dispatcher.h