feat: refactor transport into separate module with master/slave clock support

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-01 21:08:38 +00:00
parent c2ad0e874c
commit a47598df8c
6 changed files with 490 additions and 229 deletions

239
engine.c
View File

@@ -36,60 +36,8 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
// 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);
// Update atomic mirrors for frontend reads
atomic_store(&engine->transport_clock_count, engine->transport.clock_count);
atomic_store(&engine->transport_sample_position, engine->transport.sample_position);
if (engine->transport.clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
engine->transport.beat_position =
(engine->transport.beat_position + 1) % BEATS_PER_BAR;
atomic_store(&engine->transport_beat_position, engine->transport.beat_position);
if (engine->transport.beat_position == 0) {
engine->transport.bar_position++;
atomic_store(&engine->transport_bar_position, 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;
atomic_store(&engine->transport_rolling, 1);
atomic_store(&engine->transport_clock_count, 0);
atomic_store(&engine->transport_beat_position, 0);
atomic_store(&engine->transport_bar_position, 0);
atomic_store(&engine->transport_sample_position, 0);
} else if (status == 0xFC) { // MIDI Stop
engine->transport.rolling = false;
atomic_store(&engine->transport_rolling, 0);
} else if (status == 0xFB) { // MIDI Continue
engine->transport.rolling = true;
atomic_store(&engine->transport_rolling, 1);
}
// 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 transport (handles both master and slave clock)
transport_process(engine->transport, nframes, midi_clock_buf, midi_out_buf);
// Process control channel MIDI input (clip triggers)
event_index = 0;
@@ -108,8 +56,9 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
// Read quantize mode atomically (frontend may update it)
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic);
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
if (current_quantize != QUANTIZE_OFF && engine->transport.rolling) {
if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) {
// Queue for quantization
jack_nframes_t trigger_time = midi_event.time;
queue_trigger(engine, clip_index, false, trigger_time);
@@ -150,8 +99,9 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
// Read quantize mode atomically (frontend may update it)
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic);
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
if (current_quantize != QUANTIZE_OFF && engine->transport.rolling) {
if (current_quantize != QUANTIZE_OFF && transport_state == TRANSPORT_PLAYING) {
// Queue for quantization
jack_nframes_t trigger_time = midi_event.time;
queue_trigger(engine, scene_index, true, trigger_time);
@@ -207,34 +157,8 @@ static void shutdown_callback(void *arg) {
// 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;
}
if (!engine->transport) return current_frame;
return transport_get_next_quantize_frame(engine->transport, current_frame, engine->quantize_mode);
}
// Queue a trigger for quantization
@@ -261,7 +185,9 @@ void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t
// 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;
if (!engine->queued_triggers) return;
TransportState transport_state = (TransportState)atomic_load(&engine->transport->state_atomic);
if (transport_state != TRANSPORT_PLAYING) return;
jack_nframes_t quantize_frame = get_next_quantize_frame(engine, 0);
@@ -468,23 +394,14 @@ void engine_process_commands(Engine *engine) {
action.type = ACTION_RESET_TRANSPORT;
action.index = 0;
action.value = 0;
action.previous_rolling = engine->transport.rolling;
action.previous_clock_count = engine->transport.clock_count;
action.previous_beat_position = engine->transport.beat_position;
action.previous_bar_position = engine->transport.bar_position;
action.previous_sample_position = engine->transport.sample_position;
action.previous_rolling = (engine->transport->state == TRANSPORT_PLAYING);
action.previous_clock_count = engine->transport->clock_count;
action.previous_beat_position = engine->transport->beat_position;
action.previous_bar_position = engine->transport->bar_position;
action.previous_sample_position = engine->transport->sample_position;
engine_push_undo_action(engine, &action);
engine->transport.rolling = false;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
atomic_store(&engine->transport_rolling, 0);
atomic_store(&engine->transport_clock_count, 0);
atomic_store(&engine->transport_beat_position, 0);
atomic_store(&engine->transport_bar_position, 0);
atomic_store(&engine->transport_sample_position, 0);
transport_reset(engine->transport);
break;
}
@@ -497,6 +414,31 @@ void engine_process_commands(Engine *engine) {
break;
}
case CMD_TRANSPORT_PLAY:
transport_play(engine->transport);
break;
case CMD_TRANSPORT_PAUSE:
transport_pause(engine->transport);
break;
case CMD_TRANSPORT_STOP:
transport_stop(engine->transport);
break;
case CMD_TRANSPORT_TOGGLE_PLAY:
transport_toggle_play(engine->transport);
break;
case CMD_SET_CLOCK_SOURCE:
transport_set_clock_source(engine->transport, (ClockSource)cmd.index);
break;
case CMD_SET_BPM:
transport_set_bpm(engine->transport, (double)cmd.value / 100.0);
break;
}
read++;
}
@@ -580,16 +522,16 @@ void engine_undo(Engine *engine) {
}
case ACTION_RESET_TRANSPORT: {
engine->transport.rolling = action->previous_rolling;
engine->transport.clock_count = action->previous_clock_count;
engine->transport.beat_position = action->previous_beat_position;
engine->transport.bar_position = action->previous_bar_position;
engine->transport.sample_position = action->previous_sample_position;
atomic_store(&engine->transport_rolling, action->previous_rolling ? 1 : 0);
atomic_store(&engine->transport_clock_count, action->previous_clock_count);
atomic_store(&engine->transport_beat_position, action->previous_beat_position);
atomic_store(&engine->transport_bar_position, action->previous_bar_position);
atomic_store(&engine->transport_sample_position, action->previous_sample_position);
engine->transport->state = action->previous_rolling ? TRANSPORT_PLAYING : TRANSPORT_STOPPED;
engine->transport->clock_count = action->previous_clock_count;
engine->transport->beat_position = action->previous_beat_position;
engine->transport->bar_position = action->previous_bar_position;
engine->transport->sample_position = action->previous_sample_position;
atomic_store(&engine->transport->state_atomic, engine->transport->state);
atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count);
atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position);
atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position);
atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position);
break;
}
}
@@ -690,16 +632,16 @@ void engine_redo(Engine *engine) {
}
case ACTION_RESET_TRANSPORT: {
engine->transport.rolling = true;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
atomic_store(&engine->transport_rolling, 1);
atomic_store(&engine->transport_clock_count, 0);
atomic_store(&engine->transport_beat_position, 0);
atomic_store(&engine->transport_bar_position, 0);
atomic_store(&engine->transport_sample_position, 0);
engine->transport->state = TRANSPORT_PLAYING;
engine->transport->clock_count = 0;
engine->transport->beat_position = 0;
engine->transport->bar_position = 0;
engine->transport->sample_position = 0;
atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING);
atomic_store(&engine->transport->clock_count_atomic, 0);
atomic_store(&engine->transport->beat_position_atomic, 0);
atomic_store(&engine->transport->bar_position_atomic, 0);
atomic_store(&engine->transport->sample_position_atomic, 0);
break;
}
}
@@ -726,20 +668,19 @@ int engine_init(Engine *engine, const char *client_name) {
command_queue_init(&engine->command_queue);
// Initialize atomic state mirrors
atomic_store(&engine->transport_rolling, 0);
atomic_store(&engine->transport_clock_count, 0);
atomic_store(&engine->transport_beat_position, 0);
atomic_store(&engine->transport_bar_position, 0);
atomic_store(&engine->transport_sample_position, 0);
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
atomic_store(&engine->quantize_threshold_atomic, 0);
// 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;
engine->transport = (Transport *)calloc(1, sizeof(Transport));
if (!engine->transport) {
// Cleanup on allocation failure
for (int j = 0; j < MAX_CLIPS; j++) {
free(engine->clips[j].buffer);
}
return -1;
}
transport_init(engine->transport, engine->sample_rate);
// Initialize clips
for (int i = 0; i < MAX_CLIPS; i++) {
@@ -828,6 +769,13 @@ void engine_cleanup(Engine *engine) {
}
engine->queued_triggers = NULL;
// Free transport
if (engine->transport) {
transport_cleanup(engine->transport);
free(engine->transport);
engine->transport = NULL;
}
if (engine->client) {
jack_client_close(engine->client);
engine->client = NULL;
@@ -896,11 +844,34 @@ void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples) {
engine_submit_command(engine, CMD_SET_QUANTIZE_THRESHOLD, 0, samples);
}
void engine_reset_transport(Engine *engine) {
void engine_transport_play(Engine *engine) {
if (!engine) return;
engine_submit_command(engine, CMD_RESET_TRANSPORT, 0, 0);
printf("Transport reset\n");
engine_submit_command(engine, CMD_TRANSPORT_PLAY, 0, 0);
}
void engine_transport_pause(Engine *engine) {
if (!engine) return;
engine_submit_command(engine, CMD_TRANSPORT_PAUSE, 0, 0);
}
void engine_transport_stop(Engine *engine) {
if (!engine) return;
engine_submit_command(engine, CMD_TRANSPORT_STOP, 0, 0);
}
void engine_transport_toggle_play(Engine *engine) {
if (!engine) return;
engine_submit_command(engine, CMD_TRANSPORT_TOGGLE_PLAY, 0, 0);
}
void engine_set_clock_source(Engine *engine, ClockSource source) {
if (!engine) return;
engine_submit_command(engine, CMD_SET_CLOCK_SOURCE, (int)source, 0);
}
void engine_set_bpm(Engine *engine, double bpm) {
if (!engine) return;
engine_submit_command(engine, CMD_SET_BPM, 0, (jack_nframes_t)(bpm * 100.0));
}
const char* clip_state_to_string(ClipState state) {