Files
jack-looper/dispatcher.h
Loic Coenen 8e05c2f0ab fix: change reducer to take pointer to AppState to avoid stack overflow
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-05 09:32:21 +00:00

230 lines
7.1 KiB
C

#ifndef DISPATCHER_H
#define DISPATCHER_H
#include <stdint.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <jack/jack.h>
#include <pthread.h>
#include "carla.h"
// ============================================================
// Application State - contains ALL application state
// ============================================================
#define MAX_SCENES 8
#define MAX_CHANNELS 8
#define MAX_CLIPS 512 // 8 grids * 8 scenes * 8 channels
#define GRID_ROWS MAX_SCENES
#define GRID_COLS MAX_CHANNELS
#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 {
uint8_t note;
uint8_t velocity;
uint32_t timestamp; // sample offset within the clip
} MidiEvent;
#define MAX_MIDI_EVENTS 44100 // ~1 second of dense MIDI data
typedef struct {
ClipState state;
MidiEvent *events; // Dynamically allocated
int event_count;
int max_events; // Allocated size
int read_index;
char channel_name[64]; // per-channel name for MIDI grid
} MidiClip;
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];
// MIDI clips (separate grid)
MidiClip midi_clips[MAX_CLIPS];
bool show_midi_grid; // View toggle: true = MIDI grid, false = Audio grid
char channel_names[MAX_CHANNELS][64]; // Channel names for audio grid
// 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];
int batch_sizes[MAX_UNDO_HISTORY]; // NEW: track batch sizes
int current_batch_size; // NEW: current batch being recorded
} undo;
// Ring buffers for real-time recording (written by JACK callback, read by dispatcher)
// Lock-free: single producer (JACK callback), single consumer (dispatcher thread)
float record_buffer[MAX_CHANNELS][MAX_BUFFER_SIZE];
atomic_size_t record_write_pos[MAX_CHANNELS]; // Only written by JACK callback
atomic_size_t record_read_pos[MAX_CHANNELS]; // Only written by dispatcher thread
// Carla host
CarlaHost carla_host;
// 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_RACK_ADD_PLUGIN,
ACTION_RACK_REMOVE_PLUGIN,
ACTION_RACK_SET_PARAMETER,
ACTION_RACK_SET_VOLUME,
ACTION_RACK_BYPASS,
ACTION_SAVE_PROJECT,
ACTION_LOAD_PROJECT,
ACTION_PROCESS_AUDIO,
ACTION_QUIT,
ACTION_SET_SHOW_MIDI_GRID,
ACTION_MIDI_CLIP_TRIGGER,
ACTION_MIDI_CLIP_RESET,
ACTION_SET_CHANNEL_NAME
} 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 { int channel; char uri[512]; PluginType type; } rack_add_plugin;
struct { int channel; int plugin_index; } rack_remove_plugin;
struct { int channel; int plugin_index; int param_index; float value; } rack_set_parameter;
struct { int channel; float volume; } rack_set_volume;
struct { int channel; bool bypass; } rack_bypass;
struct { char filename[512]; } save_project;
struct { char filename[512]; } load_project;
struct { jack_nframes_t nframes; } process_audio;
struct { bool show; } set_show_midi_grid;
struct { int clip_index; } midi_clip_trigger;
struct { int clip_index; } midi_clip_reset;
struct { int channel; char name[64]; } set_channel_name;
} 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)
void dispatcher_get_state(AppState *out);
// Get pointer to dispatcher's state (for real-time threads that can't block)
// WARNING: This is NOT thread-safe. Only use in JACK process callback
// which is serialized per client.
AppState* dispatcher_get_state_ptr(void);
// ============================================================
// Reducer - pure function (takes pointer to avoid stack copy)
// ============================================================
void reducer(AppState *state, Action action);
#endif // DISPATCHER_H