Files
jack-looper/engine.c
Loic Coenen 05c6f34b8f feat: add microui-based GUI with transport controls and progress bar
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-01 13:02:39 +00:00

624 lines
21 KiB
C

#include "engine.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.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
static int process_callback(jack_nframes_t nframes, void *arg) {
Engine *engine = (Engine *)arg;
// 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 MIDI clock input
jack_midi_event_t midi_event;
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) {
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;
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
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;
if (engine->quantize_mode != QUANTIZE_OFF && engine->transport.rolling) {
// 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 (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;
}
}
}
}
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.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) {
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 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
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);
}
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;
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;
return 0;
}
void engine_stop(Engine *engine) {
if (!engine || !engine->client) return;
engine->running = false;
jack_deactivate(engine->client);
}
void engine_trigger_clip(Engine *engine, int clip_index) {
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
Clip *clip = &engine->clips[clip_index];
switch (clip->state) {
case CLIP_EMPTY:
// Start recording
clip->state = CLIP_RECORDING;
clip->write_position = 0;
clip->buffer_size = 0;
clip->read_position = 0;
printf("Clip %d (scene %d, channel %d): Recording started\n",
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
break;
case CLIP_RECORDING:
// Stop recording, start looping
clip->state = CLIP_LOOPING;
clip->buffer_size = clip->write_position;
clip->read_position = 0;
printf("Clip %d (scene %d, channel %d): Recording stopped, looping %zu samples\n",
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS,
clip->buffer_size);
break;
case CLIP_LOOPING:
// Stop looping
clip->state = CLIP_STOPPED;
clip->read_position = 0;
printf("Clip %d (scene %d, channel %d): Looping stopped\n",
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
break;
case CLIP_STOPPED:
// Start looping again
clip->state = CLIP_LOOPING;
clip->read_position = 0;
printf("Clip %d (scene %d, channel %d): Looping resumed\n",
clip_index, clip_index / MAX_CHANNELS, clip_index % MAX_CHANNELS);
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) {
if (!engine || clip_index < 0 || clip_index >= MAX_CLIPS) return;
Clip *clip = &engine->clips[clip_index];
clip->state = CLIP_EMPTY;
clip->buffer_size = 0;
clip->write_position = 0;
clip->read_position = 0;
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) {
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";
}
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <jack/jack.h>
#include "engine.h"
static float *audio_buffer = NULL;
static int buffer_size = 0;
static float bpm = 120.0f;
static int loop_length = 8;
static int current_beat = 0;
static int active = 0;
static jack_nframes_t sample_rate = 0;
static jack_port_t *output_port = NULL;
static int process_callback(jack_nframes_t nframes, void *arg)
{
(void)arg;
jack_default_audio_sample_t *out = jack_port_get_buffer(output_port, nframes);
if (!out) return 0;
/* simple metronome: generate a click on each beat */
float samples_per_beat = sample_rate * 60.0f / bpm;
static float phase = 0.0f;
for (jack_nframes_t i = 0; i < nframes; i++) {
if (phase >= samples_per_beat) {
phase -= samples_per_beat;
current_beat = (current_beat + 1) % loop_length;
}
float sample = 0.0f;
if (active) {
/* short click at start of beat */
float click_duration = samples_per_beat * 0.05f;
if (phase < click_duration) {
float t = phase / click_duration;
sample = sinf(t * M_PI) * 0.3f;
}
}
out[i] = sample;
phase += 1.0f;
}
return 0;
}
int engine_init(jack_client_t *client, float *buffer, int buf_size)
{
audio_buffer = buffer;
buffer_size = buf_size;
sample_rate = jack_get_sample_rate(client);
output_port = jack_port_register(client, "output",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
if (!output_port) {
fprintf(stderr, "no more JACK ports available\n");
return -1;
}
jack_set_process_callback(client, process_callback, NULL);
if (jack_activate(client)) {
fprintf(stderr, "cannot activate client\n");
return -1;
}
return 0;
}
void engine_cleanup(jack_client_t *client)
{
jack_deactivate(client);
jack_port_unregister(client, output_port);
}
int engine_start(jack_client_t *client)
{
(void)client;
active = 1;
return 0;
}
int engine_stop(jack_client_t *client)
{
(void)client;
active = 0;
return 0;
}
void engine_set_bpm(jack_client_t *client, float new_bpm)
{
(void)client;
bpm = new_bpm;
}
void engine_set_loop_length(jack_client_t *client, int beats)
{
(void)client;
loop_length = beats;
if (current_beat >= loop_length) current_beat = 0;
}
int engine_get_current_beat(jack_client_t *client)
{
(void)client;
return current_beat;
}