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/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
36
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
// reducer.c is no longer needed; all state management is in dispatcher.c
|
||||
|
||||
Reference in New Issue
Block a user