#include "engine.h" #include "carla.h" #define MAX_NFRAMES 8192 #include #include #include #include #include static int process_callback(jack_nframes_t nframes, void *arg) { Engine *engine = (Engine *)arg; if (!engine || !engine->dispatch) return 0; 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) { // 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); // Record MIDI event into MIDI clip if recording // Note: we modify the local copy, not the actual state. // The actual recording should be done via an action. // For now, we just dispatch the trigger and let the reducer handle it. ClipState note_state = (ClipState)atomic_load(&state->clips[note % MAX_CLIPS].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) { 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; } for (jack_nframes_t i = 0; i < nframes; i++) { rack_in[i] = 0.0f; 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) { // 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) { size_t rp = atomic_load(&clip->read_position); rack_in[i] += clip->buffer[rp]; atomic_store(&clip->read_position, (rp + 1) % clip->buffer_size); } } } } // 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"; } }