1227 lines
48 KiB
C
1227 lines
48 KiB
C
#include "engine.h"
|
|
#include "wav_io.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <stdatomic.h>
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
|
|
static atomic_bool save_load_running_atomic = false;
|
|
|
|
// Forward declarations
|
|
static void process_queued_triggers(Engine *engine, jack_nframes_t current_frame);
|
|
static jack_nframes_t get_next_quantize_frame(Engine *engine, jack_nframes_t current_frame);
|
|
|
|
// JACK process callback
|
|
static int process_callback(jack_nframes_t nframes, void *arg) {
|
|
Engine *engine = (Engine *)arg;
|
|
|
|
// Process commands from frontend threads
|
|
engine_process_commands(engine);
|
|
|
|
// Get per-channel audio buffers
|
|
jack_default_audio_sample_t *audio_in[MAX_CHANNELS];
|
|
jack_default_audio_sample_t *audio_out[MAX_CHANNELS];
|
|
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
audio_in[ch] = (jack_default_audio_sample_t *)
|
|
jack_port_get_buffer(engine->audio_in_ports[ch], nframes);
|
|
audio_out[ch] = (jack_default_audio_sample_t *)
|
|
jack_port_get_buffer(engine->audio_out_ports[ch], nframes);
|
|
}
|
|
|
|
// Get MIDI buffers
|
|
void *midi_in_buf = jack_port_get_buffer(engine->midi_in_port, nframes);
|
|
void *midi_scene_buf = jack_port_get_buffer(engine->midi_scene_in_port, nframes);
|
|
void *midi_clock_buf = jack_port_get_buffer(engine->midi_clock_in_port, nframes);
|
|
void *midi_out_buf = jack_port_get_buffer(engine->midi_out_port, nframes);
|
|
|
|
// Clear output MIDI buffer
|
|
jack_midi_clear_buffer(midi_out_buf);
|
|
|
|
// Process transport (handles both master and slave clock)
|
|
transport_process(engine->transport, nframes, midi_clock_buf, midi_out_buf);
|
|
|
|
// Process control channel MIDI input (clip triggers)
|
|
int event_index;
|
|
jack_midi_event_t midi_event;
|
|
event_index = 0;
|
|
while (jack_midi_event_get(&midi_event, midi_in_buf, event_index) == 0) {
|
|
event_index++;
|
|
|
|
uint8_t *data = midi_event.buffer;
|
|
uint8_t status = data[0] & 0xF0;
|
|
uint8_t channel = data[0] & 0x0F;
|
|
uint8_t note = data[1];
|
|
uint8_t velocity = data[2];
|
|
|
|
// Only process note on messages on the control channel
|
|
if (status == 0x90 && channel == engine->control_channel && velocity > 0) {
|
|
int clip_index = note % MAX_CLIPS;
|
|
|
|
// Read quantize mode atomically (frontend may update it)
|
|
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic);
|
|
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
|
|
|
|
if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) {
|
|
// Queue for quantization
|
|
jack_nframes_t trigger_time = midi_event.time;
|
|
queue_trigger(engine, clip_index, false, trigger_time);
|
|
} else {
|
|
// Trigger immediately
|
|
engine_trigger_clip(engine, clip_index);
|
|
}
|
|
|
|
// Send note with velocity representing state
|
|
uint8_t out_velocity = clip_state_to_velocity(engine->clips[clip_index].state);
|
|
uint8_t out_msg[3] = {0x90 | channel, note, out_velocity};
|
|
|
|
if (jack_midi_event_write(midi_out_buf, midi_event.time, out_msg, 3) != 0) {
|
|
fprintf(stderr, "Failed to write MIDI event\n");
|
|
}
|
|
} else {
|
|
// Pass through all other MIDI messages
|
|
if (jack_midi_event_write(midi_out_buf, midi_event.time,
|
|
midi_event.buffer, midi_event.size) != 0) {
|
|
fprintf(stderr, "Failed to write MIDI event\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process scene launch MIDI input
|
|
event_index = 0;
|
|
while (jack_midi_event_get(&midi_event, midi_scene_buf, event_index) == 0) {
|
|
event_index++;
|
|
|
|
uint8_t *data = midi_event.buffer;
|
|
uint8_t status = data[0] & 0xF0;
|
|
uint8_t note = data[1];
|
|
uint8_t velocity = data[2];
|
|
|
|
// Process note on messages (any channel) for scene launch
|
|
if (status == 0x90 && velocity > 0) {
|
|
int scene_index = note % MAX_SCENES;
|
|
|
|
// Read quantize mode atomically (frontend may update it)
|
|
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic);
|
|
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
|
|
|
|
if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) {
|
|
// Queue for quantization
|
|
jack_nframes_t trigger_time = midi_event.time;
|
|
queue_trigger(engine, scene_index, true, trigger_time);
|
|
} else {
|
|
// Trigger immediately
|
|
engine_trigger_scene(engine, scene_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process queued triggers at quantization boundaries
|
|
process_queued_triggers(engine, nframes);
|
|
|
|
// Process audio per-channel
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
memset(audio_out[ch], 0, sizeof(jack_default_audio_sample_t) * nframes);
|
|
|
|
for (jack_nframes_t i = 0; i < nframes; i++) {
|
|
// Record input to recording clips in this channel
|
|
for (int s = 0; s < MAX_SCENES; s++) {
|
|
int clip_idx = CLIP_INDEX(s, ch);
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
|
|
if ((ClipState)atomic_load(&clip->state) == CLIP_RECORDING) {
|
|
if (clip->write_position < MAX_BUFFER_SIZE) {
|
|
clip->buffer[clip->write_position++] = audio_in[ch][i];
|
|
} else {
|
|
// Buffer full, stop recording
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
atomic_store(&clip->buffer_size, clip->write_position);
|
|
clip->read_position = 0;
|
|
}
|
|
}
|
|
|
|
// Play looping clips to this channel's output
|
|
if ((ClipState)atomic_load(&clip->state) == CLIP_LOOPING && atomic_load(&clip->buffer_size) > 0) {
|
|
audio_out[ch][i] += clip->buffer[clip->read_position];
|
|
clip->read_position = (clip->read_position + 1) % clip->buffer_size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// JACK shutdown callback
|
|
static void shutdown_callback(void *arg) {
|
|
Engine *engine = (Engine *)arg;
|
|
engine->running = false;
|
|
fprintf(stderr, "JACK shutdown\n");
|
|
}
|
|
|
|
// Get the next quantization boundary frame
|
|
static jack_nframes_t get_next_quantize_frame(Engine *engine, jack_nframes_t current_frame) {
|
|
if (!engine->transport || engine->transport->state != TRANSPORT_PLAYING ||
|
|
engine->quantize_mode == QUANTIZE_OFF) {
|
|
return current_frame;
|
|
}
|
|
|
|
// Calculate frames per beat
|
|
jack_nframes_t frames_per_beat = (jack_nframes_t)engine->transport->samples_per_beat;
|
|
jack_nframes_t frames_per_bar = frames_per_beat * BEATS_PER_BAR;
|
|
|
|
// Current position in frames
|
|
jack_nframes_t current_pos = engine->transport->sample_position + current_frame;
|
|
|
|
if (engine->quantize_mode == QUANTIZE_BEAT) {
|
|
// Next beat boundary
|
|
jack_nframes_t beat_frames = frames_per_beat;
|
|
jack_nframes_t next_beat = ((current_pos / beat_frames) + 1) * beat_frames;
|
|
return next_beat - engine->transport->sample_position;
|
|
} else { // QUANTIZE_BAR
|
|
// Next bar boundary
|
|
jack_nframes_t bar_frames = frames_per_bar;
|
|
jack_nframes_t next_bar = ((current_pos / bar_frames) + 1) * bar_frames;
|
|
return next_bar - engine->transport->sample_position;
|
|
}
|
|
}
|
|
|
|
// Queue a trigger for quantization
|
|
void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time) {
|
|
if (!engine) return;
|
|
|
|
QueuedTrigger *qt = (QueuedTrigger *)malloc(sizeof(QueuedTrigger));
|
|
if (!qt) return;
|
|
|
|
qt->clip_index = clip_index;
|
|
qt->is_scene = is_scene;
|
|
qt->trigger_time = time;
|
|
qt->next = NULL;
|
|
|
|
// Add to end of queue
|
|
if (!engine->queued_triggers) {
|
|
engine->queued_triggers = qt;
|
|
} else {
|
|
QueuedTrigger *last = engine->queued_triggers;
|
|
while (last && last->next) last = last->next; // FIX: add null check for last
|
|
if (last) { // ADD THIS
|
|
last->next = qt;
|
|
} else {
|
|
engine->queued_triggers = qt; // Fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process queued triggers at quantization boundaries
|
|
static void process_queued_triggers(Engine *engine, jack_nframes_t nframes) {
|
|
if (!engine || !engine->queued_triggers) return; // FIX: add engine null check
|
|
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
|
|
if (transport_state != TRANSPORT_PLAYING) return;
|
|
|
|
jack_nframes_t quantize_frame = get_next_quantize_frame(engine, 0);
|
|
|
|
// Check if we've reached the quantization boundary
|
|
if (quantize_frame <= nframes) {
|
|
QueuedTrigger *qt = engine->queued_triggers;
|
|
engine->queued_triggers = NULL;
|
|
|
|
while (qt) {
|
|
if (qt->is_scene) {
|
|
engine_trigger_scene(engine, qt->clip_index);
|
|
} else {
|
|
engine_trigger_clip(engine, qt->clip_index);
|
|
}
|
|
|
|
QueuedTrigger *next = qt->next;
|
|
free(qt);
|
|
qt = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize command queue
|
|
void command_queue_init(CommandQueue *q) {
|
|
atomic_store(&q->write_index, 0);
|
|
atomic_store(&q->read_index, 0);
|
|
}
|
|
|
|
// Initialize save/load queue
|
|
void save_load_queue_init(SaveLoadQueue *q) {
|
|
atomic_store(&q->write_index, 0);
|
|
atomic_store(&q->read_index, 0);
|
|
}
|
|
|
|
// Push a save/load request (called from audio thread)
|
|
int save_load_queue_push(SaveLoadQueue *q, SaveLoadType type, int clip_index, const char *filename) {
|
|
if (!q || !filename) return -1;
|
|
|
|
unsigned int write = atomic_load(&q->write_index);
|
|
unsigned int read = atomic_load(&q->read_index);
|
|
|
|
if ((write - read) >= MAX_QUEUED_COMMANDS) {
|
|
fprintf(stderr, "Save/Load queue full, dropping request\n");
|
|
return -1;
|
|
}
|
|
|
|
unsigned int slot = write % MAX_QUEUED_COMMANDS;
|
|
q->buffer[slot].type = type;
|
|
q->buffer[slot].clip_index = clip_index;
|
|
strncpy(q->buffer[slot].filename, filename, sizeof(q->buffer[slot].filename) - 1);
|
|
q->buffer[slot].filename[sizeof(q->buffer[slot].filename) - 1] = '\0';
|
|
|
|
atomic_store(&q->write_index, write + 1);
|
|
return 0;
|
|
}
|
|
|
|
// Pop a save/load request (called from save/load thread)
|
|
int save_load_queue_pop(SaveLoadQueue *q, SaveLoadRequest *req) {
|
|
if (!q || !req) return -1;
|
|
|
|
unsigned int read = atomic_load(&q->read_index);
|
|
unsigned int write = atomic_load(&q->write_index);
|
|
|
|
if (read >= write) return 0; // Empty
|
|
|
|
unsigned int slot = read % MAX_QUEUED_COMMANDS;
|
|
*req = q->buffer[slot];
|
|
|
|
atomic_store(&q->read_index, read + 1);
|
|
return 1;
|
|
}
|
|
|
|
// Save/Load thread function
|
|
void* save_load_thread_func(void *arg) {
|
|
Engine *engine = (Engine *)arg;
|
|
if (!engine) return NULL;
|
|
|
|
// Create samples directory if it doesn't exist
|
|
mkdir("samples", 0755);
|
|
|
|
while (atomic_load(&save_load_running_atomic)) {
|
|
SaveLoadRequest req;
|
|
int ret = save_load_queue_pop(&engine->save_load_queue, &req);
|
|
|
|
if (ret == 1) {
|
|
char filepath[512];
|
|
|
|
switch (req.type) {
|
|
case REQ_SAVE_CLIP: {
|
|
if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[req.clip_index];
|
|
|
|
// Build filename: samples/clip_<index>.wav
|
|
snprintf(filepath, sizeof(filepath), "samples/clip_%d.wav", req.clip_index);
|
|
|
|
if (clip->buffer && clip->buffer_size > 0) {
|
|
int result = save_wav_float(filepath, clip->buffer, clip->buffer_size, engine->sample_rate);
|
|
if (result == 0) {
|
|
printf("Saved clip %d to %s (%zu samples)\n", req.clip_index, filepath, clip->buffer_size);
|
|
} else {
|
|
fprintf(stderr, "Failed to save clip %d to %s\n", req.clip_index, filepath);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case REQ_LOAD_CLIP: {
|
|
if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[req.clip_index];
|
|
|
|
float *new_buffer = NULL;
|
|
size_t num_samples = 0;
|
|
unsigned int file_sample_rate = 0;
|
|
|
|
int result = load_wav_float(req.filename, &new_buffer, &num_samples, &file_sample_rate);
|
|
if (result == 0 && new_buffer && num_samples > 0) {
|
|
// Allocate a new buffer for the clip
|
|
float *clip_buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
|
if (!clip_buffer) {
|
|
free(new_buffer);
|
|
break;
|
|
}
|
|
|
|
// Copy samples (truncate if too long)
|
|
size_t copy_size = (num_samples < MAX_BUFFER_SIZE) ? num_samples : MAX_BUFFER_SIZE;
|
|
memcpy(clip_buffer, new_buffer, copy_size * sizeof(float));
|
|
|
|
// Atomically swap the clip's buffer
|
|
float *old_buffer = atomic_exchange(&clip->buffer, clip_buffer);
|
|
|
|
// Update clip state atomically
|
|
clip->state = CLIP_LOOPING;
|
|
clip->buffer_size = copy_size;
|
|
clip->write_position = copy_size;
|
|
clip->read_position = 0;
|
|
|
|
// Free old buffer and temporary buffer
|
|
if (old_buffer) free(old_buffer);
|
|
free(new_buffer);
|
|
|
|
printf("Loaded clip %d from %s (%zu samples, %u Hz)\n",
|
|
req.clip_index, req.filename, num_samples, file_sample_rate);
|
|
} else {
|
|
fprintf(stderr, "Failed to load %s into clip %d\n", req.filename, req.clip_index);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// No requests, sleep a bit to avoid busy-waiting
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 }; // 1ms
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Start the save/load thread
|
|
int engine_start_save_load_thread(Engine *engine) {
|
|
if (!engine) return -1;
|
|
|
|
atomic_store(&save_load_running_atomic, true);
|
|
save_load_queue_init(&engine->save_load_queue);
|
|
|
|
if (pthread_create(&engine->save_load_thread, NULL, save_load_thread_func, engine) != 0) {
|
|
engine->save_load_running = false;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Stop the save/load thread
|
|
void engine_stop_save_load_thread(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
atomic_store(&save_load_running_atomic, false);
|
|
pthread_join(engine->save_load_thread, NULL);
|
|
}
|
|
|
|
// Submit command from frontend thread (non-blocking)
|
|
int engine_submit_command(Engine *engine, CommandType type, int index, jack_nframes_t value) {
|
|
if (!engine) return -1;
|
|
|
|
CommandQueue *q = &engine->command_queue;
|
|
|
|
// Use CAS to atomically claim a slot
|
|
unsigned int write, next_write, read;
|
|
do {
|
|
write = atomic_load(&q->write_index);
|
|
read = atomic_load(&q->read_index);
|
|
|
|
// Check if queue is full
|
|
if ((write - read) >= MAX_QUEUED_COMMANDS) {
|
|
fprintf(stderr, "Command queue full, dropping command\n");
|
|
return -1;
|
|
}
|
|
|
|
next_write = write + 1;
|
|
} while (!atomic_compare_exchange_weak(&q->write_index, &write, next_write));
|
|
|
|
// We now own this slot exclusively
|
|
unsigned int slot = write % MAX_QUEUED_COMMANDS;
|
|
q->buffer[slot].type = type;
|
|
q->buffer[slot].index = index;
|
|
q->buffer[slot].value = value;
|
|
|
|
// Release fence ensures the buffer write is visible before any consumer reads it
|
|
atomic_thread_fence(memory_order_release);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Process pending commands (called from audio thread only)
|
|
void engine_process_commands(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
CommandQueue *q = &engine->command_queue;
|
|
unsigned int read = atomic_load(&q->read_index);
|
|
unsigned int write = atomic_load(&q->write_index);
|
|
|
|
while (read < write) {
|
|
unsigned int slot = read % MAX_QUEUED_COMMANDS;
|
|
|
|
// Acquire fence ensures we see the fully written command data
|
|
atomic_thread_fence(memory_order_acquire);
|
|
|
|
Command cmd = q->buffer[slot];
|
|
|
|
// Process the command directly (we're in the audio thread)
|
|
switch (cmd.type) {
|
|
case CMD_TRIGGER_CLIP: {
|
|
if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[cmd.index];
|
|
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_TRIGGER_CLIP;
|
|
action.index = cmd.index;
|
|
action.value = 0;
|
|
action.previous_state = clip->state;
|
|
action.previous_buffer_size = clip->buffer_size;
|
|
action.previous_write_position = clip->write_position;
|
|
action.previous_read_position = clip->read_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
ClipState prev_state = (ClipState)atomic_load(&clip->state);
|
|
|
|
switch (prev_state) {
|
|
case CLIP_EMPTY:
|
|
atomic_store(&clip->state, CLIP_RECORDING);
|
|
clip->write_position = 0;
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_RECORDING:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
atomic_store(&clip->buffer_size, clip->write_position);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_LOOPING:
|
|
atomic_store(&clip->state, CLIP_STOPPED);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_STOPPED:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
clip->read_position = 0;
|
|
break;
|
|
}
|
|
|
|
// Auto-save when recording finishes (RECORDING -> LOOPING)
|
|
if (prev_state == CLIP_RECORDING && (ClipState)atomic_load(&clip->state) == CLIP_LOOPING) {
|
|
save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, cmd.index, "");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CMD_TRIGGER_SCENE: {
|
|
if (cmd.index < 0 || cmd.index >= MAX_SCENES) break;
|
|
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_TRIGGER_SCENE;
|
|
action.index = cmd.index;
|
|
action.value = 0;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
int clip_idx = CLIP_INDEX(cmd.index, ch);
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
|
|
ClipState prev_state = (ClipState)atomic_load(&clip->state);
|
|
|
|
switch (prev_state) {
|
|
case CLIP_EMPTY:
|
|
atomic_store(&clip->state, CLIP_RECORDING);
|
|
clip->write_position = 0;
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_RECORDING:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
atomic_store(&clip->buffer_size, clip->write_position);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_LOOPING:
|
|
atomic_store(&clip->state, CLIP_STOPPED);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_STOPPED:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
clip->read_position = 0;
|
|
break;
|
|
}
|
|
|
|
// Auto-save when recording finishes
|
|
if (prev_state == CLIP_RECORDING && (ClipState)atomic_load(&clip->state) == CLIP_LOOPING) {
|
|
save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, "");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CMD_RESET_CLIP: {
|
|
if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[cmd.index];
|
|
if (!clip->buffer) break;
|
|
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_RESET_CLIP;
|
|
action.index = cmd.index;
|
|
action.value = 0;
|
|
action.previous_state = clip->state;
|
|
action.previous_buffer_size = clip->buffer_size;
|
|
action.previous_write_position = clip->write_position;
|
|
action.previous_read_position = clip->read_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
atomic_store(&clip->state, CLIP_EMPTY);
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->write_position = 0;
|
|
clip->read_position = 0;
|
|
memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float));
|
|
break;
|
|
}
|
|
|
|
case CMD_SET_QUANTIZE_MODE: {
|
|
QuantizeMode new_mode = (QuantizeMode)cmd.index;
|
|
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_SET_QUANTIZE_MODE;
|
|
action.index = (int)new_mode;
|
|
action.value = 0;
|
|
action.previous_quantize_mode = engine->quantize_mode;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
engine->quantize_mode = new_mode;
|
|
atomic_store(&engine->quantize_mode_atomic, (int)new_mode);
|
|
break;
|
|
}
|
|
|
|
case CMD_SET_QUANTIZE_THRESHOLD: {
|
|
jack_nframes_t new_threshold = cmd.value;
|
|
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_SET_QUANTIZE_THRESHOLD;
|
|
action.index = 0;
|
|
action.value = new_threshold;
|
|
action.previous_quantize_threshold = engine->quantize_threshold;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
engine->quantize_threshold = new_threshold;
|
|
atomic_store(&engine->quantize_threshold_atomic, new_threshold);
|
|
break;
|
|
}
|
|
|
|
case CMD_RESET_TRANSPORT: {
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_RESET_TRANSPORT;
|
|
action.index = 0;
|
|
action.value = 0;
|
|
action.previous_rolling = (engine->transport->state == TRANSPORT_PLAYING);
|
|
action.previous_clock_count = engine->transport->clock_count;
|
|
action.previous_beat_position = engine->transport->beat_position;
|
|
action.previous_bar_position = engine->transport->bar_position;
|
|
action.previous_sample_position = engine->transport->sample_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
transport_reset(engine->transport);
|
|
break;
|
|
}
|
|
|
|
case CMD_UNDO:
|
|
engine_undo(engine);
|
|
break;
|
|
|
|
case CMD_REDO:
|
|
engine_redo(engine);
|
|
break;
|
|
|
|
case CMD_TRANSPORT_PLAY: {
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_TRANSPORT_STATE_CHANGE;
|
|
action.index = 0;
|
|
action.value = (jack_nframes_t)engine->transport->state;
|
|
action.previous_clock_count = engine->transport->clock_count;
|
|
action.previous_beat_position = engine->transport->beat_position;
|
|
action.previous_bar_position = engine->transport->bar_position;
|
|
action.previous_sample_position = engine->transport->sample_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
transport_play(engine->transport);
|
|
break;
|
|
}
|
|
|
|
case CMD_TRANSPORT_PAUSE: {
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_TRANSPORT_STATE_CHANGE;
|
|
action.index = 0;
|
|
action.value = (jack_nframes_t)engine->transport->state;
|
|
action.previous_clock_count = engine->transport->clock_count;
|
|
action.previous_beat_position = engine->transport->beat_position;
|
|
action.previous_bar_position = engine->transport->bar_position;
|
|
action.previous_sample_position = engine->transport->sample_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
transport_pause(engine->transport);
|
|
break;
|
|
}
|
|
|
|
case CMD_TRANSPORT_STOP: {
|
|
// Record undo action
|
|
UndoAction action;
|
|
action.type = ACTION_TRANSPORT_STATE_CHANGE;
|
|
action.index = 0;
|
|
action.value = (jack_nframes_t)engine->transport->state;
|
|
action.previous_clock_count = engine->transport->clock_count;
|
|
action.previous_beat_position = engine->transport->beat_position;
|
|
action.previous_bar_position = engine->transport->bar_position;
|
|
action.previous_sample_position = engine->transport->sample_position;
|
|
engine_push_undo_action(engine, &action);
|
|
|
|
transport_stop(engine->transport);
|
|
break;
|
|
}
|
|
|
|
case CMD_TRANSPORT_TOGGLE_PLAY:
|
|
transport_toggle_play(engine->transport);
|
|
break;
|
|
|
|
case CMD_SET_CLOCK_SOURCE:
|
|
transport_set_clock_source(engine->transport, (ClockSource)cmd.index);
|
|
break;
|
|
|
|
case CMD_SET_BPM:
|
|
transport_set_bpm(engine->transport, (double)cmd.value / 100.0);
|
|
break;
|
|
}
|
|
|
|
read++;
|
|
// Store read_index after processing each command
|
|
atomic_store(&q->read_index, read);
|
|
}
|
|
}
|
|
|
|
// Push an action to the undo history
|
|
void engine_push_undo_action(Engine *engine, UndoAction *action) {
|
|
if (!engine || !action) return;
|
|
|
|
UndoHistory *history = &engine->undo_history;
|
|
|
|
// If we've undone some actions, clear the redo history
|
|
if (history->redo_index > history->undo_index) {
|
|
history->redo_index = history->undo_index;
|
|
}
|
|
|
|
// Add action at current undo position
|
|
int slot = history->undo_index % MAX_UNDO_HISTORY;
|
|
history->actions[slot] = *action;
|
|
history->undo_index++;
|
|
history->redo_index = history->undo_index;
|
|
|
|
if (history->count < MAX_UNDO_HISTORY) {
|
|
history->count++;
|
|
}
|
|
}
|
|
|
|
// Undo the last action
|
|
void engine_undo(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
UndoHistory *history = &engine->undo_history;
|
|
if (history->undo_index <= 0) return; // Nothing to undo
|
|
|
|
int slot = (history->undo_index - 1) % MAX_UNDO_HISTORY;
|
|
UndoAction *action = &history->actions[slot];
|
|
|
|
switch (action->type) {
|
|
case ACTION_TRIGGER_CLIP: {
|
|
int clip_idx = action->index;
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) break; // ADD THIS
|
|
atomic_store(&clip->state, action->previous_state);
|
|
atomic_store(&clip->buffer_size, action->previous_buffer_size);
|
|
clip->write_position = action->previous_write_position;
|
|
clip->read_position = action->previous_read_position;
|
|
break;
|
|
}
|
|
|
|
case ACTION_TRIGGER_SCENE: {
|
|
int scene_idx = action->index;
|
|
if (scene_idx < 0 || scene_idx >= MAX_SCENES) break;
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
int clip_idx = CLIP_INDEX(scene_idx, ch);
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) continue;
|
|
atomic_store(&clip->state, CLIP_EMPTY);
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->write_position = 0;
|
|
clip->read_position = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ACTION_RESET_CLIP: {
|
|
int clip_idx = action->index;
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) break;
|
|
atomic_store(&clip->state, action->previous_state);
|
|
atomic_store(&clip->buffer_size, action->previous_buffer_size);
|
|
clip->write_position = action->previous_write_position;
|
|
clip->read_position = action->previous_read_position;
|
|
break;
|
|
}
|
|
|
|
case ACTION_SET_QUANTIZE_MODE: {
|
|
engine->quantize_mode = action->previous_quantize_mode;
|
|
atomic_store(&engine->quantize_mode_atomic, (int)action->previous_quantize_mode);
|
|
break;
|
|
}
|
|
|
|
case ACTION_SET_QUANTIZE_THRESHOLD: {
|
|
engine->quantize_threshold = action->previous_quantize_threshold;
|
|
atomic_store(&engine->quantize_threshold_atomic, action->previous_quantize_threshold);
|
|
break;
|
|
}
|
|
|
|
case ACTION_RESET_TRANSPORT: {
|
|
engine->transport->state = action->previous_rolling ? TRANSPORT_PLAYING : TRANSPORT_STOPPED;
|
|
engine->transport->clock_count = action->previous_clock_count;
|
|
engine->transport->beat_position = action->previous_beat_position;
|
|
engine->transport->bar_position = action->previous_bar_position;
|
|
engine->transport->sample_position = action->previous_sample_position;
|
|
atomic_store(&engine->transport->state_atomic, engine->transport->state);
|
|
atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count);
|
|
atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position);
|
|
atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position);
|
|
atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position);
|
|
break;
|
|
}
|
|
|
|
case ACTION_TRANSPORT_STATE_CHANGE: {
|
|
TransportState prev_state = (TransportState)action->value;
|
|
engine->transport->state = prev_state;
|
|
engine->transport->clock_count = action->previous_clock_count;
|
|
engine->transport->beat_position = action->previous_beat_position;
|
|
engine->transport->bar_position = action->previous_bar_position;
|
|
engine->transport->sample_position = action->previous_sample_position;
|
|
atomic_store(&engine->transport->state_atomic, prev_state);
|
|
atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count);
|
|
atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position);
|
|
atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position);
|
|
atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position);
|
|
break;
|
|
}
|
|
}
|
|
|
|
history->undo_index--;
|
|
}
|
|
|
|
// Redo the last undone action
|
|
void engine_redo(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
UndoHistory *history = &engine->undo_history;
|
|
if (history->redo_index <= history->undo_index) return; // Nothing to redo
|
|
|
|
int slot = history->undo_index % MAX_UNDO_HISTORY;
|
|
UndoAction *action = &history->actions[slot];
|
|
|
|
switch (action->type) {
|
|
case ACTION_TRIGGER_CLIP: {
|
|
int clip_idx = action->index;
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) break; // ADD THIS
|
|
// Re-apply the trigger by directly manipulating state
|
|
switch ((ClipState)atomic_load(&clip->state)) {
|
|
case CLIP_EMPTY:
|
|
atomic_store(&clip->state, CLIP_RECORDING);
|
|
clip->write_position = 0;
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_RECORDING:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
atomic_store(&clip->buffer_size, clip->write_position);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_LOOPING:
|
|
atomic_store(&clip->state, CLIP_STOPPED);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_STOPPED:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
clip->read_position = 0;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ACTION_TRIGGER_SCENE: {
|
|
int scene_idx = action->index;
|
|
if (scene_idx < 0 || scene_idx >= MAX_SCENES) break;
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
int clip_idx = CLIP_INDEX(scene_idx, ch);
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) continue;
|
|
switch ((ClipState)atomic_load(&clip->state)) {
|
|
case CLIP_EMPTY:
|
|
atomic_store(&clip->state, CLIP_RECORDING);
|
|
clip->write_position = 0;
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_RECORDING:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
atomic_store(&clip->buffer_size, clip->write_position);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_LOOPING:
|
|
atomic_store(&clip->state, CLIP_STOPPED);
|
|
clip->read_position = 0;
|
|
break;
|
|
case CLIP_STOPPED:
|
|
atomic_store(&clip->state, CLIP_LOOPING);
|
|
clip->read_position = 0;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ACTION_RESET_CLIP: {
|
|
int clip_idx = action->index;
|
|
if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break;
|
|
Clip *clip = &engine->clips[clip_idx];
|
|
if (!clip->buffer) break;
|
|
atomic_store(&clip->state, CLIP_EMPTY);
|
|
atomic_store(&clip->buffer_size, 0);
|
|
clip->write_position = 0;
|
|
clip->read_position = 0;
|
|
memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float));
|
|
break;
|
|
}
|
|
|
|
case ACTION_SET_QUANTIZE_MODE: {
|
|
engine->quantize_mode = (QuantizeMode)action->index;
|
|
atomic_store(&engine->quantize_mode_atomic, action->index);
|
|
break;
|
|
}
|
|
|
|
case ACTION_SET_QUANTIZE_THRESHOLD: {
|
|
engine->quantize_threshold = action->value;
|
|
atomic_store(&engine->quantize_threshold_atomic, action->value);
|
|
break;
|
|
}
|
|
|
|
case ACTION_RESET_TRANSPORT: {
|
|
engine->transport->state = TRANSPORT_PLAYING;
|
|
engine->transport->clock_count = 0;
|
|
engine->transport->beat_position = 0;
|
|
engine->transport->bar_position = 0;
|
|
engine->transport->sample_position = 0;
|
|
atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING);
|
|
atomic_store(&engine->transport->clock_count_atomic, 0);
|
|
atomic_store(&engine->transport->beat_position_atomic, 0);
|
|
atomic_store(&engine->transport->bar_position_atomic, 0);
|
|
atomic_store(&engine->transport->sample_position_atomic, 0);
|
|
break;
|
|
}
|
|
|
|
case ACTION_TRANSPORT_STATE_CHANGE: {
|
|
TransportState prev_state = (TransportState)action->value;
|
|
engine->transport->state = prev_state;
|
|
engine->transport->clock_count = action->previous_clock_count;
|
|
engine->transport->beat_position = action->previous_beat_position;
|
|
engine->transport->bar_position = action->previous_bar_position;
|
|
engine->transport->sample_position = action->previous_sample_position;
|
|
atomic_store(&engine->transport->state_atomic, prev_state);
|
|
atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count);
|
|
atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position);
|
|
atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position);
|
|
atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position);
|
|
break;
|
|
}
|
|
}
|
|
|
|
history->undo_index++;
|
|
}
|
|
|
|
int engine_init(Engine *engine, const char *client_name) {
|
|
if (!engine || !client_name) return -1;
|
|
|
|
memset(engine, 0, sizeof(Engine));
|
|
engine->control_channel = 0;
|
|
engine->running = false;
|
|
engine->quantize_mode = QUANTIZE_OFF;
|
|
engine->quantize_threshold = 0;
|
|
engine->queued_triggers = NULL;
|
|
|
|
// Initialize undo history
|
|
engine->undo_history.undo_index = 0;
|
|
engine->undo_history.redo_index = 0;
|
|
engine->undo_history.count = 0;
|
|
|
|
// Initialize command queue
|
|
command_queue_init(&engine->command_queue);
|
|
|
|
// Initialize save/load queue
|
|
save_load_queue_init(&engine->save_load_queue);
|
|
|
|
// Initialize atomic state mirrors
|
|
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
|
|
atomic_store(&engine->quantize_threshold_atomic, 0);
|
|
|
|
// Initialize transport
|
|
engine->transport = (Transport *)calloc(1, sizeof(Transport));
|
|
if (!engine->transport) {
|
|
// Cleanup on allocation failure
|
|
for (int j = 0; j < MAX_CLIPS; j++) {
|
|
free(engine->clips[j].buffer);
|
|
engine->clips[j].buffer = NULL; // ADD THIS
|
|
}
|
|
return -1;
|
|
}
|
|
transport_init(engine->transport, engine->sample_rate);
|
|
|
|
// Initialize clips
|
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
|
engine->clips[i].state = CLIP_EMPTY;
|
|
engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
|
if (!engine->clips[i].buffer) {
|
|
// Cleanup on allocation failure
|
|
for (int j = 0; j < i; j++) {
|
|
free(engine->clips[j].buffer);
|
|
engine->clips[j].buffer = NULL; // ADD THIS
|
|
}
|
|
free(engine->transport);
|
|
engine->transport = NULL; // ADD THIS
|
|
return -1;
|
|
}
|
|
engine->clips[i].buffer_size = 0;
|
|
engine->clips[i].write_position = 0;
|
|
engine->clips[i].read_position = 0;
|
|
}
|
|
|
|
// Open JACK client
|
|
jack_status_t status;
|
|
engine->client = jack_client_open(client_name, JackNullOption, &status, NULL);
|
|
if (!engine->client) {
|
|
fprintf(stderr, "Failed to open JACK client, status = 0x%2.0x\n", status);
|
|
return -1;
|
|
}
|
|
|
|
// Register per-channel audio ports
|
|
char port_name[32];
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
snprintf(port_name, sizeof(port_name), "audio_in_%d", ch);
|
|
engine->audio_in_ports[ch] = jack_port_register(engine->client, port_name,
|
|
JACK_DEFAULT_AUDIO_TYPE,
|
|
JackPortIsInput, 0);
|
|
|
|
snprintf(port_name, sizeof(port_name), "audio_out_%d", ch);
|
|
engine->audio_out_ports[ch] = jack_port_register(engine->client, port_name,
|
|
JACK_DEFAULT_AUDIO_TYPE,
|
|
JackPortIsOutput, 0);
|
|
|
|
if (!engine->audio_in_ports[ch] || !engine->audio_out_ports[ch]) {
|
|
fprintf(stderr, "Failed to register audio port %d\n", ch);
|
|
engine_cleanup(engine);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Register MIDI ports
|
|
engine->midi_in_port = jack_port_register(engine->client, "midi_control_in",
|
|
JACK_DEFAULT_MIDI_TYPE,
|
|
JackPortIsInput, 0);
|
|
engine->midi_scene_in_port = jack_port_register(engine->client, "midi_scene_in",
|
|
JACK_DEFAULT_MIDI_TYPE,
|
|
JackPortIsInput, 0);
|
|
engine->midi_clock_in_port = jack_port_register(engine->client, "midi_clock_in",
|
|
JACK_DEFAULT_MIDI_TYPE,
|
|
JackPortIsInput, 0);
|
|
engine->midi_out_port = jack_port_register(engine->client, "midi_out",
|
|
JACK_DEFAULT_MIDI_TYPE,
|
|
JackPortIsOutput, 0);
|
|
|
|
if (!engine->midi_in_port || !engine->midi_scene_in_port ||
|
|
!engine->midi_clock_in_port || !engine->midi_out_port) {
|
|
fprintf(stderr, "Failed to register MIDI ports\n");
|
|
engine_cleanup(engine);
|
|
return -1;
|
|
}
|
|
|
|
// Set callbacks
|
|
jack_set_process_callback(engine->client, process_callback, engine);
|
|
jack_on_shutdown(engine->client, shutdown_callback, engine);
|
|
|
|
// Get sample rate
|
|
engine->sample_rate = jack_get_sample_rate(engine->client);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void engine_cleanup(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
// Free any queued triggers
|
|
QueuedTrigger *qt = engine->queued_triggers;
|
|
while (qt) {
|
|
QueuedTrigger *next = qt->next;
|
|
free(qt);
|
|
qt = next;
|
|
}
|
|
engine->queued_triggers = NULL;
|
|
|
|
// Free transport
|
|
if (engine->transport) {
|
|
transport_cleanup(engine->transport);
|
|
free(engine->transport);
|
|
engine->transport = NULL;
|
|
}
|
|
|
|
if (engine->client) {
|
|
jack_client_close(engine->client);
|
|
engine->client = NULL;
|
|
}
|
|
|
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
|
free(engine->clips[i].buffer);
|
|
engine->clips[i].buffer = NULL;
|
|
}
|
|
}
|
|
|
|
int engine_start(Engine *engine) {
|
|
if (!engine || !engine->client) return -1;
|
|
|
|
if (jack_activate(engine->client) != 0) {
|
|
fprintf(stderr, "Failed to activate JACK client\n");
|
|
return -1;
|
|
}
|
|
|
|
engine->running = true;
|
|
|
|
// Start save/load thread
|
|
if (engine_start_save_load_thread(engine) != 0) {
|
|
fprintf(stderr, "Failed to start save/load thread\n");
|
|
jack_deactivate(engine->client);
|
|
engine->running = false;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void engine_stop(Engine *engine) {
|
|
if (!engine || !engine->client) return;
|
|
|
|
engine->running = false;
|
|
engine_stop_save_load_thread(engine);
|
|
jack_deactivate(engine->client);
|
|
}
|
|
|
|
void engine_trigger_clip(Engine *engine, int clip_index) {
|
|
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
|
|
|
// Queue command for audio thread processing
|
|
engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_index, 0);
|
|
}
|
|
|
|
void engine_trigger_scene(Engine *engine, int scene_index) {
|
|
if (!engine || scene_index < 0 || scene_index >= MAX_SCENES) return;
|
|
|
|
engine_submit_command(engine, CMD_TRIGGER_SCENE, scene_index, 0);
|
|
}
|
|
|
|
void engine_reset_clip(Engine *engine, int clip_index) {
|
|
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
|
|
|
engine_submit_command(engine, CMD_RESET_CLIP, clip_index, 0);
|
|
}
|
|
|
|
void engine_set_quantize_mode(Engine *engine, QuantizeMode mode) {
|
|
if (!engine) return;
|
|
|
|
// Atomically update the mode so audio thread sees it immediately
|
|
atomic_store(&engine->quantize_mode_atomic, (int)mode);
|
|
|
|
// Also queue for any additional processing
|
|
engine_submit_command(engine, CMD_SET_QUANTIZE_MODE, (int)mode, 0);
|
|
|
|
printf("Quantize mode set to: %s\n", quantize_mode_to_string(mode));
|
|
}
|
|
|
|
void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples) {
|
|
if (!engine) return;
|
|
|
|
atomic_store(&engine->quantize_threshold_atomic, samples);
|
|
engine_submit_command(engine, CMD_SET_QUANTIZE_THRESHOLD, 0, samples);
|
|
}
|
|
|
|
void engine_transport_play(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_TRANSPORT_PLAY, 0, 0);
|
|
}
|
|
|
|
void engine_transport_pause(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_TRANSPORT_PAUSE, 0, 0);
|
|
}
|
|
|
|
void engine_transport_stop(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_TRANSPORT_STOP, 0, 0);
|
|
}
|
|
|
|
void engine_transport_toggle_play(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_TRANSPORT_TOGGLE_PLAY, 0, 0);
|
|
}
|
|
|
|
void engine_set_clock_source(Engine *engine, ClockSource source) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_SET_CLOCK_SOURCE, (int)source, 0);
|
|
}
|
|
|
|
void engine_set_bpm(Engine *engine, double bpm) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_SET_BPM, 0, (jack_nframes_t)(bpm * 100.0));
|
|
}
|
|
|
|
const char* clip_state_to_string(ClipState state) {
|
|
switch (state) {
|
|
case CLIP_EMPTY: return "Empty";
|
|
case CLIP_RECORDING: return "Recording";
|
|
case CLIP_LOOPING: return "Looping";
|
|
case CLIP_STOPPED: return "Stopped";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
uint8_t clip_state_to_velocity(ClipState state) {
|
|
switch (state) {
|
|
case CLIP_EMPTY: return 0;
|
|
case CLIP_RECORDING: return 64;
|
|
case CLIP_LOOPING: return 127;
|
|
case CLIP_STOPPED: return 32;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
const char* quantize_mode_to_string(QuantizeMode mode) {
|
|
switch (mode) {
|
|
case QUANTIZE_OFF: return "Off";
|
|
case QUANTIZE_BEAT: return "Beat";
|
|
case QUANTIZE_BAR: return "Bar";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
void engine_undo_action(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_UNDO, 0, 0);
|
|
}
|
|
|
|
void engine_redo_action(Engine *engine) {
|
|
if (!engine) return;
|
|
engine_submit_command(engine, CMD_REDO, 0, 0);
|
|
}
|