refactor: implement unidirectional data flow with dispatcher pattern
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
450
dispatcher.c
450
dispatcher.c
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
166
dispatcher.h
166
dispatcher.h
@@ -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
|
||||||
|
|||||||
219
engine.h
219
engine.h
@@ -3,237 +3,30 @@
|
|||||||
|
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdatomic.h>
|
#include "dispatcher.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;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_port_t *audio_in_ports[MAX_CHANNELS];
|
jack_port_t *audio_in_ports[MAX_CHANNELS];
|
||||||
jack_port_t *audio_out_ports[MAX_CHANNELS];
|
jack_port_t *audio_out_ports[MAX_CHANNELS];
|
||||||
jack_port_t *midi_in_port; // Control channel MIDI
|
jack_port_t *midi_in_port;
|
||||||
jack_port_t *midi_scene_in_port; // Scene launch MIDI
|
jack_port_t *midi_scene_in_port;
|
||||||
jack_port_t *midi_clock_in_port; // MIDI clock input
|
jack_port_t *midi_clock_in_port;
|
||||||
jack_port_t *midi_out_port;
|
jack_port_t *midi_out_port;
|
||||||
|
|
||||||
Clip clips[MAX_CLIPS];
|
DispatchFn dispatch;
|
||||||
int control_channel;
|
|
||||||
jack_nframes_t sample_rate;
|
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;
|
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;
|
||||||
|
|
||||||
// Engine lifecycle
|
int engine_init(Engine *engine, const char *client_name, DispatchFn dispatch);
|
||||||
int engine_init(Engine *engine, const char *client_name);
|
|
||||||
void engine_cleanup(Engine *engine);
|
void engine_cleanup(Engine *engine);
|
||||||
int engine_start(Engine *engine);
|
int engine_start(Engine *engine);
|
||||||
void engine_stop(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);
|
const char* clip_state_to_string(ClipState state);
|
||||||
uint8_t clip_state_to_velocity(ClipState state);
|
uint8_t clip_state_to_velocity(ClipState state);
|
||||||
const char* quantize_mode_to_string(QuantizeMode mode);
|
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
|
#endif // ENGINE_H
|
||||||
|
|||||||
36
main.c
36
main.c
@@ -7,13 +7,19 @@
|
|||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
#include "gui.h"
|
#include "gui.h"
|
||||||
|
#include "dispatcher.h"
|
||||||
|
|
||||||
static Engine engine;
|
static Engine engine;
|
||||||
static volatile int keep_running = 1;
|
static volatile int keep_running = 1;
|
||||||
|
static DispatchFn dispatch = NULL;
|
||||||
|
|
||||||
void signal_handler(int sig) {
|
void signal_handler(int sig) {
|
||||||
(void)sig;
|
(void)sig;
|
||||||
keep_running = 0;
|
keep_running = 0;
|
||||||
|
if (dispatch) {
|
||||||
|
Action action = { .type = ACTION_QUIT };
|
||||||
|
dispatch(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void print_usage(const char *program) {
|
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
|
// Initialize engine
|
||||||
if (engine_init(&engine, client_name) != 0) {
|
if (engine_init(&engine, client_name, dispatch) != 0) {
|
||||||
fprintf(stderr, "Failed to initialize engine\n");
|
fprintf(stderr, "Failed to initialize engine\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.control_channel = control_channel;
|
|
||||||
|
|
||||||
// Set up signal handler
|
// Set up signal handler
|
||||||
signal(SIGINT, signal_handler);
|
signal(SIGINT, signal_handler);
|
||||||
signal(SIGTERM, signal_handler);
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
|
// Start dispatcher
|
||||||
|
dispatcher_start();
|
||||||
|
|
||||||
// Start engine
|
// Start engine
|
||||||
if (engine_start(&engine) != 0) {
|
if (engine_start(&engine) != 0) {
|
||||||
fprintf(stderr, "Failed to start engine\n");
|
fprintf(stderr, "Failed to start engine\n");
|
||||||
engine_cleanup(&engine);
|
engine_cleanup(&engine);
|
||||||
|
dispatcher_stop();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +136,7 @@ int main(int argc, char *argv[]) {
|
|||||||
printf("\nShutting down...\n");
|
printf("\nShutting down...\n");
|
||||||
engine_stop(&engine);
|
engine_stop(&engine);
|
||||||
engine_cleanup(&engine);
|
engine_cleanup(&engine);
|
||||||
|
dispatcher_stop();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
// reducer.c is no longer needed; all state management is in dispatcher.c
|
||||||
|
|||||||
Reference in New Issue
Block a user