diff --git a/engine.c b/engine.c index 5a05ba1..9de91f2 100644 --- a/engine.c +++ b/engine.c @@ -4,25 +4,78 @@ #include #include +// 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 ports - jack_default_audio_sample_t *audio_in = (jack_default_audio_sample_t *) - jack_port_get_buffer(engine->audio_in_port, nframes); - jack_default_audio_sample_t *audio_out = (jack_default_audio_sample_t *) - jack_port_get_buffer(engine->audio_out_port, nframes); + // 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 input + // 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++; @@ -35,7 +88,15 @@ static int process_callback(jack_nframes_t nframes, void *arg) { // Only process note on messages on the control channel if (status == 0x90 && channel == engine->control_channel && velocity > 0) { 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 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 - memset(audio_out, 0, sizeof(jack_default_audio_sample_t) * nframes); - - for (jack_nframes_t i = 0; i < nframes; i++) { - // Record input to recording clips - for (int c = 0; c < MAX_CLIPS; c++) { - Clip *clip = &engine->clips[c]; + // 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 (clip->state == CLIP_RECORDING) { - if (clip->write_position < MAX_BUFFER_SIZE) { - clip->buffer[clip->write_position++] = audio_in[i]; - } else { - // Buffer full, stop recording - clip->state = CLIP_LOOPING; - clip->buffer_size = clip->write_position; - clip->read_position = 0; - } + 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); } - - // Play looping clips - if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) { - audio_out[i] += clip->buffer[clip->read_position]; - clip->read_position = (clip->read_position + 1) % clip->buffer_size; + } + } + + // 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; + } } } } @@ -90,12 +182,101 @@ static void shutdown_callback(void *arg) { 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++) { @@ -121,23 +302,43 @@ int engine_init(Engine *engine, const char *client_name) { return -1; } - // Register ports - engine->audio_in_port = jack_port_register(engine->client, "audio_in", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - engine->audio_out_port = jack_port_register(engine->client, "audio_out", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - engine->midi_in_port = jack_port_register(engine->client, "midi_in", + // 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->audio_in_port || !engine->audio_out_port || - !engine->midi_in_port || !engine->midi_out_port) { - fprintf(stderr, "Failed to register ports\n"); + 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; } @@ -155,6 +356,15 @@ int engine_init(Engine *engine, const char *client_name) { 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; @@ -197,7 +407,8 @@ void engine_trigger_clip(Engine *engine, int clip_index) { clip->write_position = 0; clip->buffer_size = 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; case CLIP_RECORDING: @@ -205,26 +416,40 @@ void engine_trigger_clip(Engine *engine, int clip_index) { clip->state = CLIP_LOOPING; clip->buffer_size = clip->write_position; clip->read_position = 0; - printf("Clip %d: Recording stopped, looping %zu samples\n", - clip_index, clip->buffer_size); + 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: 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; case CLIP_STOPPED: // Start looping again clip->state = CLIP_LOOPING; 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; } } +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; @@ -236,6 +461,27 @@ void engine_reset_clip(Engine *engine, int clip_index) { 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"; @@ -255,3 +501,12 @@ uint8_t clip_state_to_velocity(ClipState state) { 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"; + } +} diff --git a/engine.h b/engine.h index 119ce01..746c2d3 100644 --- a/engine.h +++ b/engine.h @@ -6,8 +6,15 @@ #include #include -#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 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 { CLIP_EMPTY, @@ -16,6 +23,20 @@ typedef enum { CLIP_STOPPED } 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 { ClipState state; float *buffer; @@ -25,17 +46,36 @@ typedef struct { bool is_playing; } 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 { jack_client_t *client; - jack_port_t *audio_in_port; - jack_port_t *audio_out_port; - jack_port_t *midi_in_port; + jack_port_t *audio_in_ports[MAX_CHANNELS]; + jack_port_t *audio_out_ports[MAX_CHANNELS]; + 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; Clip clips[MAX_CLIPS]; int control_channel; 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; } Engine; @@ -47,10 +87,20 @@ void engine_stop(Engine *engine); // Clip management 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); +// 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 const char* clip_state_to_string(ClipState state); uint8_t clip_state_to_velocity(ClipState state); +const char* quantize_mode_to_string(QuantizeMode mode); #endif // ENGINE_H diff --git a/test_engine.c b/test_engine.c index cce06ba..020e7ac 100644 --- a/test_engine.c +++ b/test_engine.c @@ -11,6 +11,16 @@ static Engine *create_test_engine(void) { engine->control_channel = 0; 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++) { engine->clips[i].state = CLIP_EMPTY; @@ -26,6 +36,14 @@ static Engine *create_test_engine(void) { static void destroy_test_engine(Engine *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++) { free(engine->clips[i].buffer); }