340 lines
13 KiB
C
340 lines
13 KiB
C
#include "engine.h"
|
|
#include "carla.h"
|
|
#include "logging.h"
|
|
|
|
#define MAX_NFRAMES 8192
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <stdatomic.h>
|
|
|
|
static int process_callback(jack_nframes_t nframes, void *arg) {
|
|
Engine *engine = (Engine *)arg;
|
|
if (!engine || !engine->dispatch) return 0;
|
|
|
|
// Debug: print every 50th callback
|
|
static int cb_count = 0;
|
|
if (cb_count++ % 50 == 0) {
|
|
fprintf(stdout, "LOOPER_CB: nframes=%u, dispatch=%p, state=%p\n",
|
|
nframes, (void*)engine->dispatch, (void*)engine->state);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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)jack_port_get_buffer(engine->midi_clock_in_port, nframes);
|
|
void *midi_out_buf = jack_port_get_buffer(engine->midi_out_port, nframes);
|
|
|
|
jack_midi_clear_buffer(midi_out_buf);
|
|
|
|
// Get pointer to dispatcher's state for real-time access
|
|
AppState *state = engine->state;
|
|
|
|
// Process MIDI input
|
|
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];
|
|
|
|
if (status == 0x90 && channel == 0 && velocity > 0) {
|
|
LOG_DEBUG("MIDI Note On: note=%d velocity=%d channel=%d", note, velocity, channel);
|
|
|
|
// Trigger audio clip
|
|
Action action = {
|
|
.type = ACTION_MIDI_NOTE_ON,
|
|
.data.midi_note_on = { .note = note, .velocity = velocity,
|
|
.channel = channel, .time = midi_event.time }
|
|
};
|
|
engine->dispatch(action);
|
|
|
|
// Also trigger MIDI clip
|
|
int clip_idx = note % MAX_CLIPS;
|
|
Action midi_action = {
|
|
.type = ACTION_MIDI_CLIP_TRIGGER,
|
|
.data.midi_clip_trigger = { .clip_index = clip_idx }
|
|
};
|
|
engine->dispatch(midi_action);
|
|
|
|
ClipState note_state = (ClipState)atomic_load(&state->clips[note % MAX_CLIPS].state);
|
|
LOG_DEBUG("Clip %d state after dispatch: %d", note % MAX_CLIPS, note_state);
|
|
uint8_t out_velocity = clip_state_to_velocity(note_state);
|
|
uint8_t out_msg[3] = {0x90 | channel, note, out_velocity};
|
|
jack_midi_event_write(midi_out_buf, midi_event.time, out_msg, 3);
|
|
} else {
|
|
jack_midi_event_write(midi_out_buf, midi_event.time,
|
|
midi_event.buffer, midi_event.size);
|
|
}
|
|
}
|
|
|
|
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];
|
|
|
|
if (status == 0x90 && velocity > 0) {
|
|
LOG_DEBUG("MIDI Scene Launch: note=%d", note);
|
|
Action action = {
|
|
.type = ACTION_MIDI_SCENE_LAUNCH,
|
|
.data.midi_scene_launch = { .scene_index = note % MAX_SCENES, .time = midi_event.time }
|
|
};
|
|
engine->dispatch(action);
|
|
}
|
|
}
|
|
|
|
// MIDI clip playback
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
for (int s = 0; s < MAX_SCENES; s++) {
|
|
for (int g = 0; g < 8; g++) {
|
|
int clip_idx = g * GRID_ROWS * GRID_COLS + s * GRID_COLS + ch;
|
|
MidiClip *mclip = &state->midi_clips[clip_idx];
|
|
|
|
ClipState mclip_state = (ClipState)atomic_load(&mclip->state);
|
|
if (mclip_state == CLIP_LOOPING && mclip->event_count > 0 && mclip->events != NULL) {
|
|
if (mclip->read_index < mclip->event_count) {
|
|
int idx = mclip->read_index;
|
|
uint8_t msg[3] = {
|
|
0x90 | ch,
|
|
mclip->events[idx].note,
|
|
mclip->events[idx].velocity
|
|
};
|
|
jack_midi_event_write(midi_out_buf,
|
|
mclip->events[idx].timestamp, msg, 3);
|
|
mclip->read_index++;
|
|
|
|
if (mclip->read_index >= mclip->event_count) {
|
|
mclip->read_index = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process audio per-channel
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
memset(audio_out[ch], 0, sizeof(jack_default_audio_sample_t) * nframes);
|
|
|
|
// Use stack-allocated buffers (max MAX_NFRAMES samples)
|
|
float rack_in[MAX_NFRAMES];
|
|
float rack_out[MAX_NFRAMES];
|
|
if (nframes > MAX_NFRAMES) {
|
|
// Should never happen with JACK, but guard
|
|
continue;
|
|
}
|
|
|
|
// Track if any clip is active on this channel
|
|
bool any_recording = false;
|
|
bool any_looping = false;
|
|
|
|
// Debug: check if we're receiving audio
|
|
static int debug_counter = 0;
|
|
if (debug_counter++ % 50 == 0) {
|
|
fprintf(stderr, "LOOPER DEBUG: Channel %d audio_in[0] = %f, nframes = %u\n",
|
|
ch, audio_in[ch][0], nframes);
|
|
}
|
|
|
|
for (jack_nframes_t i = 0; i < nframes; i++) {
|
|
// Start with live audio input
|
|
rack_in[i] = audio_in[ch][i];
|
|
|
|
// Check if input has signal
|
|
if (i == 0 && fabsf(audio_in[ch][i]) > 0.001f) {
|
|
LOG_TRACE("Channel %d has audio input at sample %d: %f", ch, i, audio_in[ch][i]);
|
|
}
|
|
|
|
for (int s = 0; s < MAX_SCENES; s++) {
|
|
// Iterate over all grids for this scene and channel
|
|
for (int g = 0; g < 8; g++) {
|
|
int clip_idx = g * GRID_ROWS * GRID_COLS + s * GRID_COLS + ch;
|
|
Clip *clip = &state->clips[clip_idx];
|
|
|
|
ClipState clip_state = (ClipState)atomic_load(&clip->state);
|
|
if (clip_state == CLIP_RECORDING) {
|
|
any_recording = true;
|
|
// Write to lock-free ring buffer instead of clip buffer directly
|
|
size_t wp = atomic_load(&state->record_write_pos[ch]);
|
|
state->record_buffer[ch][wp % MAX_BUFFER_SIZE] = audio_in[ch][i];
|
|
atomic_store(&state->record_write_pos[ch], wp + 1);
|
|
}
|
|
|
|
if (clip_state == CLIP_LOOPING && clip->buffer_size > 0 && clip->buffer != NULL) {
|
|
any_looping = true;
|
|
size_t rp = atomic_load(&clip->read_position);
|
|
rack_in[i] += clip->buffer[rp];
|
|
atomic_store(&clip->read_position, (rp + 1) % clip->buffer_size);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log channel state once per callback
|
|
static int log_counter = 0;
|
|
if (log_counter++ % 100 == 0) {
|
|
LOG_DEBUG("Channel %d: nframes=%d any_recording=%d any_looping=%d rack_in[0]=%f",
|
|
ch, nframes, any_recording, any_looping, rack_in[0]);
|
|
}
|
|
|
|
// Process through Carla rack
|
|
carla_process(&engine->carla_host, ch, rack_in, rack_out, nframes);
|
|
|
|
// Copy to output
|
|
memcpy(audio_out[ch], rack_out, nframes * sizeof(jack_default_audio_sample_t));
|
|
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void shutdown_callback(void *arg) {
|
|
Engine *engine = (Engine *)arg;
|
|
engine->running = false;
|
|
fprintf(stderr, "JACK shutdown\n");
|
|
}
|
|
|
|
int engine_init(Engine *engine, const char *client_name, DispatchFn dispatch) {
|
|
if (!engine || !client_name || !dispatch) return -1;
|
|
|
|
memset(engine, 0, sizeof(Engine));
|
|
engine->dispatch = dispatch;
|
|
engine->running = false;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
jack_set_process_callback(engine->client, process_callback, engine);
|
|
jack_on_shutdown(engine->client, shutdown_callback, engine);
|
|
|
|
engine->sample_rate = jack_get_sample_rate(engine->client);
|
|
|
|
// Initialize Carla host
|
|
carla_init(&engine->carla_host, engine->client);
|
|
carla_scan_plugins(&engine->carla_host);
|
|
|
|
// Get pointer to dispatcher's state for real-time access
|
|
engine->state = dispatcher_get_state_ptr();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void engine_cleanup(Engine *engine) {
|
|
if (!engine) return;
|
|
|
|
if (engine->client) {
|
|
jack_client_close(engine->client);
|
|
engine->client = 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);
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|