feat: add 8x8 scene/channel grid, MIDI clock sync, and quantization engine
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
339
engine.c
339
engine.c
@@ -4,25 +4,78 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
// 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
|
// JACK process callback
|
||||||
static int process_callback(jack_nframes_t nframes, void *arg) {
|
static int process_callback(jack_nframes_t nframes, void *arg) {
|
||||||
Engine *engine = (Engine *)arg;
|
Engine *engine = (Engine *)arg;
|
||||||
|
|
||||||
// Get ports
|
// Get per-channel audio buffers
|
||||||
jack_default_audio_sample_t *audio_in = (jack_default_audio_sample_t *)
|
jack_default_audio_sample_t *audio_in[MAX_CHANNELS];
|
||||||
jack_port_get_buffer(engine->audio_in_port, nframes);
|
jack_default_audio_sample_t *audio_out[MAX_CHANNELS];
|
||||||
jack_default_audio_sample_t *audio_out = (jack_default_audio_sample_t *)
|
|
||||||
jack_port_get_buffer(engine->audio_out_port, nframes);
|
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_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);
|
void *midi_out_buf = jack_port_get_buffer(engine->midi_out_port, nframes);
|
||||||
|
|
||||||
// Clear output MIDI buffer
|
// Clear output MIDI buffer
|
||||||
jack_midi_clear_buffer(midi_out_buf);
|
jack_midi_clear_buffer(midi_out_buf);
|
||||||
|
|
||||||
// Process MIDI input
|
// Process MIDI clock input
|
||||||
jack_midi_event_t midi_event;
|
jack_midi_event_t midi_event;
|
||||||
jack_nframes_t event_index = 0;
|
jack_nframes_t event_index = 0;
|
||||||
|
|
||||||
|
while (jack_midi_event_get(&midi_event, midi_clock_buf, event_index) == 0) {
|
||||||
|
event_index++;
|
||||||
|
|
||||||
|
uint8_t *data = midi_event.buffer;
|
||||||
|
uint8_t status = data[0];
|
||||||
|
|
||||||
|
if (status == 0xF8) { // MIDI Clock
|
||||||
|
engine->transport.clock_count++;
|
||||||
|
engine->transport.sample_position =
|
||||||
|
(engine->transport.clock_count * engine->sample_rate * 4) /
|
||||||
|
(MIDI_CLOCKS_PER_BEAT * BEATS_PER_BAR);
|
||||||
|
|
||||||
|
if (engine->transport.clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
|
||||||
|
engine->transport.beat_position =
|
||||||
|
(engine->transport.beat_position + 1) % BEATS_PER_BAR;
|
||||||
|
if (engine->transport.beat_position == 0) {
|
||||||
|
engine->transport.bar_position++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (status == 0xFA) { // MIDI Start
|
||||||
|
engine->transport.rolling = true;
|
||||||
|
engine->transport.clock_count = 0;
|
||||||
|
engine->transport.beat_position = 0;
|
||||||
|
engine->transport.bar_position = 0;
|
||||||
|
engine->transport.sample_position = 0;
|
||||||
|
} else if (status == 0xFC) { // MIDI Stop
|
||||||
|
engine->transport.rolling = false;
|
||||||
|
} else if (status == 0xFB) { // MIDI Continue
|
||||||
|
engine->transport.rolling = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through clock 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 control channel MIDI input (clip triggers)
|
||||||
|
event_index = 0;
|
||||||
while (jack_midi_event_get(&midi_event, midi_in_buf, event_index) == 0) {
|
while (jack_midi_event_get(&midi_event, midi_in_buf, event_index) == 0) {
|
||||||
event_index++;
|
event_index++;
|
||||||
|
|
||||||
@@ -35,7 +88,15 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
// Only process note on messages on the control channel
|
// Only process note on messages on the control channel
|
||||||
if (status == 0x90 && channel == engine->control_channel && velocity > 0) {
|
if (status == 0x90 && channel == engine->control_channel && velocity > 0) {
|
||||||
int clip_index = note % MAX_CLIPS;
|
int clip_index = note % MAX_CLIPS;
|
||||||
engine_trigger_clip(engine, clip_index);
|
|
||||||
|
if (engine->quantize_mode != QUANTIZE_OFF && engine->transport.rolling) {
|
||||||
|
// 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
|
// Send note with velocity representing state
|
||||||
uint8_t out_velocity = clip_state_to_velocity(engine->clips[clip_index].state);
|
uint8_t out_velocity = clip_state_to_velocity(engine->clips[clip_index].state);
|
||||||
@@ -53,29 +114,60 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process audio
|
// Process scene launch MIDI input
|
||||||
memset(audio_out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
event_index = 0;
|
||||||
|
while (jack_midi_event_get(&midi_event, midi_scene_buf, event_index) == 0) {
|
||||||
|
event_index++;
|
||||||
|
|
||||||
for (jack_nframes_t i = 0; i < nframes; i++) {
|
uint8_t *data = midi_event.buffer;
|
||||||
// Record input to recording clips
|
uint8_t status = data[0] & 0xF0;
|
||||||
for (int c = 0; c < MAX_CLIPS; c++) {
|
uint8_t note = data[1];
|
||||||
Clip *clip = &engine->clips[c];
|
uint8_t velocity = data[2];
|
||||||
|
|
||||||
if (clip->state == CLIP_RECORDING) {
|
// Process note on messages (any channel) for scene launch
|
||||||
if (clip->write_position < MAX_BUFFER_SIZE) {
|
if (status == 0x90 && velocity > 0) {
|
||||||
clip->buffer[clip->write_position++] = audio_in[i];
|
int scene_index = note % MAX_SCENES;
|
||||||
} else {
|
|
||||||
// Buffer full, stop recording
|
if (engine->quantize_mode != QUANTIZE_OFF && engine->transport.rolling) {
|
||||||
clip->state = CLIP_LOOPING;
|
// Queue for quantization
|
||||||
clip->buffer_size = clip->write_position;
|
jack_nframes_t trigger_time = midi_event.time;
|
||||||
clip->read_position = 0;
|
queue_trigger(engine, scene_index, true, trigger_time);
|
||||||
}
|
} else {
|
||||||
|
// Trigger immediately
|
||||||
|
engine_trigger_scene(engine, scene_index);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Play looping clips
|
// Process queued triggers at quantization boundaries
|
||||||
if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) {
|
process_queued_triggers(engine, nframes);
|
||||||
audio_out[i] += clip->buffer[clip->read_position];
|
|
||||||
clip->read_position = (clip->read_position + 1) % clip->buffer_size;
|
// 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 (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
|
||||||
|
clip->state = CLIP_LOOPING;
|
||||||
|
clip->buffer_size = clip->write_position;
|
||||||
|
clip->read_position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play looping clips to this channel's output
|
||||||
|
if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) {
|
||||||
|
audio_out[ch][i] += clip->buffer[clip->read_position];
|
||||||
|
clip->read_position = (clip->read_position + 1) % clip->buffer_size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,12 +182,101 @@ static void shutdown_callback(void *arg) {
|
|||||||
fprintf(stderr, "JACK shutdown\n");
|
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.rolling || engine->quantize_mode == QUANTIZE_OFF) {
|
||||||
|
return current_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate frames per beat
|
||||||
|
jack_nframes_t frames_per_beat = engine->sample_rate * 60 / 120; // Assume 120 BPM from clock
|
||||||
|
if (engine->transport.clock_count > 0) {
|
||||||
|
// Derive from actual clock
|
||||||
|
frames_per_beat = (engine->transport.sample_position * MIDI_CLOCKS_PER_BEAT) /
|
||||||
|
(engine->transport.clock_count / MIDI_CLOCKS_PER_BEAT + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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->next) last = last->next;
|
||||||
|
last->next = qt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process queued triggers at quantization boundaries
|
||||||
|
static void process_queued_triggers(Engine *engine, jack_nframes_t nframes) {
|
||||||
|
if (!engine->queued_triggers || !engine->transport.rolling) 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int engine_init(Engine *engine, const char *client_name) {
|
int engine_init(Engine *engine, const char *client_name) {
|
||||||
if (!engine || !client_name) return -1;
|
if (!engine || !client_name) return -1;
|
||||||
|
|
||||||
memset(engine, 0, sizeof(Engine));
|
memset(engine, 0, sizeof(Engine));
|
||||||
engine->control_channel = 0;
|
engine->control_channel = 0;
|
||||||
engine->running = false;
|
engine->running = false;
|
||||||
|
engine->quantize_mode = QUANTIZE_OFF;
|
||||||
|
engine->quantize_threshold = 0;
|
||||||
|
engine->queued_triggers = NULL;
|
||||||
|
|
||||||
|
// Initialize transport
|
||||||
|
engine->transport.rolling = false;
|
||||||
|
engine->transport.clock_count = 0;
|
||||||
|
engine->transport.beat_position = 0;
|
||||||
|
engine->transport.bar_position = 0;
|
||||||
|
engine->transport.sample_position = 0;
|
||||||
|
|
||||||
// Initialize clips
|
// Initialize clips
|
||||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
@@ -121,23 +302,43 @@ int engine_init(Engine *engine, const char *client_name) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register ports
|
// Register per-channel audio ports
|
||||||
engine->audio_in_port = jack_port_register(engine->client, "audio_in",
|
char port_name[32];
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||||
JackPortIsInput, 0);
|
snprintf(port_name, sizeof(port_name), "audio_in_%d", ch);
|
||||||
engine->audio_out_port = jack_port_register(engine->client, "audio_out",
|
engine->audio_in_ports[ch] = jack_port_register(engine->client, port_name,
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
JackPortIsOutput, 0);
|
JackPortIsInput, 0);
|
||||||
engine->midi_in_port = jack_port_register(engine->client, "midi_in",
|
|
||||||
|
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,
|
JACK_DEFAULT_MIDI_TYPE,
|
||||||
JackPortIsInput, 0);
|
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",
|
engine->midi_out_port = jack_port_register(engine->client, "midi_out",
|
||||||
JACK_DEFAULT_MIDI_TYPE,
|
JACK_DEFAULT_MIDI_TYPE,
|
||||||
JackPortIsOutput, 0);
|
JackPortIsOutput, 0);
|
||||||
|
|
||||||
if (!engine->audio_in_port || !engine->audio_out_port ||
|
if (!engine->midi_in_port || !engine->midi_scene_in_port ||
|
||||||
!engine->midi_in_port || !engine->midi_out_port) {
|
!engine->midi_clock_in_port || !engine->midi_out_port) {
|
||||||
fprintf(stderr, "Failed to register ports\n");
|
fprintf(stderr, "Failed to register MIDI ports\n");
|
||||||
engine_cleanup(engine);
|
engine_cleanup(engine);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -155,6 +356,15 @@ int engine_init(Engine *engine, const char *client_name) {
|
|||||||
void engine_cleanup(Engine *engine) {
|
void engine_cleanup(Engine *engine) {
|
||||||
if (!engine) return;
|
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;
|
||||||
|
|
||||||
if (engine->client) {
|
if (engine->client) {
|
||||||
jack_client_close(engine->client);
|
jack_client_close(engine->client);
|
||||||
engine->client = NULL;
|
engine->client = NULL;
|
||||||
@@ -197,7 +407,8 @@ void engine_trigger_clip(Engine *engine, int clip_index) {
|
|||||||
clip->write_position = 0;
|
clip->write_position = 0;
|
||||||
clip->buffer_size = 0;
|
clip->buffer_size = 0;
|
||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
printf("Clip %d: Recording started\n", clip_index);
|
printf("Clip %d (scene %d, channel %d): Recording started\n",
|
||||||
|
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CLIP_RECORDING:
|
case CLIP_RECORDING:
|
||||||
@@ -205,26 +416,40 @@ void engine_trigger_clip(Engine *engine, int clip_index) {
|
|||||||
clip->state = CLIP_LOOPING;
|
clip->state = CLIP_LOOPING;
|
||||||
clip->buffer_size = clip->write_position;
|
clip->buffer_size = clip->write_position;
|
||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
printf("Clip %d: Recording stopped, looping %zu samples\n",
|
printf("Clip %d (scene %d, channel %d): Recording stopped, looping %zu samples\n",
|
||||||
clip_index, clip->buffer_size);
|
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS,
|
||||||
|
clip->buffer_size);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CLIP_LOOPING:
|
case CLIP_LOOPING:
|
||||||
// Stop looping
|
// Stop looping
|
||||||
clip->state = CLIP_STOPPED;
|
clip->state = CLIP_STOPPED;
|
||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
printf("Clip %d: Looping stopped\n", clip_index);
|
printf("Clip %d (scene %d, channel %d): Looping stopped\n",
|
||||||
|
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CLIP_STOPPED:
|
case CLIP_STOPPED:
|
||||||
// Start looping again
|
// Start looping again
|
||||||
clip->state = CLIP_LOOPING;
|
clip->state = CLIP_LOOPING;
|
||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
printf("Clip %d: Looping resumed\n", clip_index);
|
printf("Clip %d (scene %d, channel %d): Looping resumed\n",
|
||||||
|
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void engine_trigger_scene(Engine *engine, int scene_index) {
|
||||||
|
if (!engine || scene_index < 0 || scene_index >= MAX_SCENES) return;
|
||||||
|
|
||||||
|
printf("Scene %d: Triggering all clips\n", scene_index);
|
||||||
|
|
||||||
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||||
|
int clip_idx = CLIP_INDEX(scene_index, ch);
|
||||||
|
engine_trigger_clip(engine, clip_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void engine_reset_clip(Engine *engine, int clip_index) {
|
void engine_reset_clip(Engine *engine, int clip_index) {
|
||||||
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
|
||||||
|
|
||||||
@@ -236,6 +461,27 @@ void engine_reset_clip(Engine *engine, int clip_index) {
|
|||||||
memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float));
|
memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void engine_set_quantize_mode(Engine *engine, QuantizeMode mode) {
|
||||||
|
if (!engine) return;
|
||||||
|
engine->quantize_mode = mode;
|
||||||
|
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;
|
||||||
|
engine->quantize_threshold = samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
void engine_reset_transport(Engine *engine) {
|
||||||
|
if (!engine) return;
|
||||||
|
engine->transport.rolling = false;
|
||||||
|
engine->transport.clock_count = 0;
|
||||||
|
engine->transport.beat_position = 0;
|
||||||
|
engine->transport.bar_position = 0;
|
||||||
|
engine->transport.sample_position = 0;
|
||||||
|
printf("Transport reset\n");
|
||||||
|
}
|
||||||
|
|
||||||
const char* clip_state_to_string(ClipState state) {
|
const char* clip_state_to_string(ClipState state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CLIP_EMPTY: return "Empty";
|
case CLIP_EMPTY: return "Empty";
|
||||||
@@ -255,3 +501,12 @@ uint8_t clip_state_to_velocity(ClipState state) {
|
|||||||
default: return 0;
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
58
engine.h
58
engine.h
@@ -6,8 +6,15 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#define MAX_CLIPS 128
|
#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_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz
|
||||||
|
#define MIDI_CLOCKS_PER_BEAT 24
|
||||||
|
#define BEATS_PER_BAR 4
|
||||||
|
|
||||||
|
// Convert scene/channel to flat clip index
|
||||||
|
#define CLIP_INDEX(scene, channel) ((scene) * MAX_CHANNELS + (channel))
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CLIP_EMPTY,
|
CLIP_EMPTY,
|
||||||
@@ -16,6 +23,20 @@ typedef enum {
|
|||||||
CLIP_STOPPED
|
CLIP_STOPPED
|
||||||
} ClipState;
|
} ClipState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
QUANTIZE_OFF,
|
||||||
|
QUANTIZE_BEAT,
|
||||||
|
QUANTIZE_BAR
|
||||||
|
} QuantizeMode;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool rolling;
|
||||||
|
uint32_t clock_count;
|
||||||
|
uint32_t beat_position; // 0-3
|
||||||
|
uint32_t bar_position;
|
||||||
|
uint32_t sample_position; // derived from clock at current sample rate
|
||||||
|
} TransportState;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
ClipState state;
|
ClipState state;
|
||||||
float *buffer;
|
float *buffer;
|
||||||
@@ -25,17 +46,36 @@ typedef struct {
|
|||||||
bool is_playing;
|
bool is_playing;
|
||||||
} Clip;
|
} Clip;
|
||||||
|
|
||||||
|
// 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_port;
|
jack_port_t *audio_in_ports[MAX_CHANNELS];
|
||||||
jack_port_t *audio_out_port;
|
jack_port_t *audio_out_ports[MAX_CHANNELS];
|
||||||
jack_port_t *midi_in_port;
|
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_out_port;
|
jack_port_t *midi_out_port;
|
||||||
|
|
||||||
Clip clips[MAX_CLIPS];
|
Clip clips[MAX_CLIPS];
|
||||||
int control_channel;
|
int control_channel;
|
||||||
jack_nframes_t sample_rate;
|
jack_nframes_t sample_rate;
|
||||||
|
|
||||||
|
// Transport and clock
|
||||||
|
TransportState transport;
|
||||||
|
bool clock_sync_enabled;
|
||||||
|
|
||||||
|
// Quantization
|
||||||
|
QuantizeMode quantize_mode;
|
||||||
|
jack_nframes_t quantize_threshold; // in samples (lookahead)
|
||||||
|
QueuedTrigger *queued_triggers;
|
||||||
|
|
||||||
bool running;
|
bool running;
|
||||||
} Engine;
|
} Engine;
|
||||||
|
|
||||||
@@ -47,10 +87,20 @@ void engine_stop(Engine *engine);
|
|||||||
|
|
||||||
// Clip management
|
// Clip management
|
||||||
void engine_trigger_clip(Engine *engine, int clip_index);
|
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);
|
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_reset_transport(Engine *engine);
|
||||||
|
|
||||||
|
// Queue management (exposed for testing)
|
||||||
|
void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time);
|
||||||
|
|
||||||
// Utility
|
// 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);
|
||||||
|
|
||||||
#endif // ENGINE_H
|
#endif // ENGINE_H
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ static Engine *create_test_engine(void) {
|
|||||||
|
|
||||||
engine->control_channel = 0;
|
engine->control_channel = 0;
|
||||||
engine->sample_rate = 48000;
|
engine->sample_rate = 48000;
|
||||||
|
engine->quantize_mode = QUANTIZE_OFF;
|
||||||
|
engine->quantize_threshold = 0;
|
||||||
|
engine->queued_triggers = NULL;
|
||||||
|
|
||||||
|
// Initialize transport
|
||||||
|
engine->transport.rolling = false;
|
||||||
|
engine->transport.clock_count = 0;
|
||||||
|
engine->transport.beat_position = 0;
|
||||||
|
engine->transport.bar_position = 0;
|
||||||
|
engine->transport.sample_position = 0;
|
||||||
|
|
||||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
engine->clips[i].state = CLIP_EMPTY;
|
engine->clips[i].state = CLIP_EMPTY;
|
||||||
@@ -26,6 +36,14 @@ static Engine *create_test_engine(void) {
|
|||||||
|
|
||||||
static void destroy_test_engine(Engine *engine) {
|
static void destroy_test_engine(Engine *engine) {
|
||||||
if (engine) {
|
if (engine) {
|
||||||
|
// Free queued triggers
|
||||||
|
QueuedTrigger *qt = engine->queued_triggers;
|
||||||
|
while (qt) {
|
||||||
|
QueuedTrigger *next = qt->next;
|
||||||
|
free(qt);
|
||||||
|
qt = next;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||||
free(engine->clips[i].buffer);
|
free(engine->clips[i].buffer);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user