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

237
engine.c
View File

@@ -36,60 +36,8 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
// Clear output MIDI buffer // Clear output MIDI buffer
jack_midi_clear_buffer(midi_out_buf); jack_midi_clear_buffer(midi_out_buf);
// Process MIDI clock input // Process transport (handles both master and slave clock)
jack_midi_event_t midi_event; transport_process(engine->transport, nframes, midi_clock_buf, midi_out_buf);
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 control channel MIDI input (clip triggers) // Process control channel MIDI input (clip triggers)
event_index = 0; 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) // Read quantize mode atomically (frontend may update it)
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic); 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 // Queue for quantization
jack_nframes_t trigger_time = midi_event.time; jack_nframes_t trigger_time = midi_event.time;
queue_trigger(engine, clip_index, false, trigger_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) // Read quantize mode atomically (frontend may update it)
QuantizeMode current_quantize = (QuantizeMode)atomic_load(&engine->quantize_mode_atomic); 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 // Queue for quantization
jack_nframes_t trigger_time = midi_event.time; jack_nframes_t trigger_time = midi_event.time;
queue_trigger(engine, scene_index, true, trigger_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 // Get the next quantization boundary frame
static jack_nframes_t get_next_quantize_frame(Engine *engine, jack_nframes_t current_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) { if (!engine->transport) return current_frame;
return current_frame; return transport_get_next_quantize_frame(engine->transport, current_frame, engine->quantize_mode);
}
// 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 // 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 // Process queued triggers at quantization boundaries
static void process_queued_triggers(Engine *engine, jack_nframes_t nframes) { 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); 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.type = ACTION_RESET_TRANSPORT;
action.index = 0; action.index = 0;
action.value = 0; action.value = 0;
action.previous_rolling = engine->transport.rolling; action.previous_rolling = (engine->transport->state == TRANSPORT_PLAYING);
action.previous_clock_count = engine->transport.clock_count; action.previous_clock_count = engine->transport->clock_count;
action.previous_beat_position = engine->transport.beat_position; action.previous_beat_position = engine->transport->beat_position;
action.previous_bar_position = engine->transport.bar_position; action.previous_bar_position = engine->transport->bar_position;
action.previous_sample_position = engine->transport.sample_position; action.previous_sample_position = engine->transport->sample_position;
engine_push_undo_action(engine, &action); engine_push_undo_action(engine, &action);
engine->transport.rolling = false; transport_reset(engine->transport);
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);
break; break;
} }
@@ -497,6 +414,31 @@ void engine_process_commands(Engine *engine) {
break; 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++; read++;
} }
@@ -580,16 +522,16 @@ void engine_undo(Engine *engine) {
} }
case ACTION_RESET_TRANSPORT: { case ACTION_RESET_TRANSPORT: {
engine->transport.rolling = action->previous_rolling; engine->transport->state = action->previous_rolling ? TRANSPORT_PLAYING : TRANSPORT_STOPPED;
engine->transport.clock_count = action->previous_clock_count; engine->transport->clock_count = action->previous_clock_count;
engine->transport.beat_position = action->previous_beat_position; engine->transport->beat_position = action->previous_beat_position;
engine->transport.bar_position = action->previous_bar_position; engine->transport->bar_position = action->previous_bar_position;
engine->transport.sample_position = action->previous_sample_position; engine->transport->sample_position = action->previous_sample_position;
atomic_store(&engine->transport_rolling, action->previous_rolling ? 1 : 0); atomic_store(&engine->transport->state_atomic, engine->transport->state);
atomic_store(&engine->transport_clock_count, action->previous_clock_count); atomic_store(&engine->transport->clock_count_atomic, action->previous_clock_count);
atomic_store(&engine->transport_beat_position, action->previous_beat_position); atomic_store(&engine->transport->beat_position_atomic, action->previous_beat_position);
atomic_store(&engine->transport_bar_position, action->previous_bar_position); atomic_store(&engine->transport->bar_position_atomic, action->previous_bar_position);
atomic_store(&engine->transport_sample_position, action->previous_sample_position); atomic_store(&engine->transport->sample_position_atomic, action->previous_sample_position);
break; break;
} }
} }
@@ -690,16 +632,16 @@ void engine_redo(Engine *engine) {
} }
case ACTION_RESET_TRANSPORT: { case ACTION_RESET_TRANSPORT: {
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 0; engine->transport->clock_count = 0;
engine->transport.beat_position = 0; engine->transport->beat_position = 0;
engine->transport.bar_position = 0; engine->transport->bar_position = 0;
engine->transport.sample_position = 0; engine->transport->sample_position = 0;
atomic_store(&engine->transport_rolling, 1); atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING);
atomic_store(&engine->transport_clock_count, 0); atomic_store(&engine->transport->clock_count_atomic, 0);
atomic_store(&engine->transport_beat_position, 0); atomic_store(&engine->transport->beat_position_atomic, 0);
atomic_store(&engine->transport_bar_position, 0); atomic_store(&engine->transport->bar_position_atomic, 0);
atomic_store(&engine->transport_sample_position, 0); atomic_store(&engine->transport->sample_position_atomic, 0);
break; break;
} }
} }
@@ -726,20 +668,19 @@ int engine_init(Engine *engine, const char *client_name) {
command_queue_init(&engine->command_queue); command_queue_init(&engine->command_queue);
// Initialize atomic state mirrors // 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_mode_atomic, (int)QUANTIZE_OFF);
atomic_store(&engine->quantize_threshold_atomic, 0); atomic_store(&engine->quantize_threshold_atomic, 0);
// Initialize transport // Initialize transport
engine->transport.rolling = false; engine->transport = (Transport *)calloc(1, sizeof(Transport));
engine->transport.clock_count = 0; if (!engine->transport) {
engine->transport.beat_position = 0; // Cleanup on allocation failure
engine->transport.bar_position = 0; for (int j = 0; j < MAX_CLIPS; j++) {
engine->transport.sample_position = 0; free(engine->clips[j].buffer);
}
return -1;
}
transport_init(engine->transport, engine->sample_rate);
// Initialize clips // Initialize clips
for (int i = 0; i < MAX_CLIPS; i++) { for (int i = 0; i < MAX_CLIPS; i++) {
@@ -828,6 +769,13 @@ void engine_cleanup(Engine *engine) {
} }
engine->queued_triggers = NULL; engine->queued_triggers = NULL;
// Free transport
if (engine->transport) {
transport_cleanup(engine->transport);
free(engine->transport);
engine->transport = NULL;
}
if (engine->client) { if (engine->client) {
jack_client_close(engine->client); jack_client_close(engine->client);
engine->client = NULL; 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); 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; if (!engine) return;
engine_submit_command(engine, CMD_TRANSPORT_PLAY, 0, 0);
}
engine_submit_command(engine, CMD_RESET_TRANSPORT, 0, 0); void engine_transport_pause(Engine *engine) {
printf("Transport reset\n"); 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) { const char* clip_state_to_string(ClipState state) {

View File

@@ -6,13 +6,12 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdatomic.h> #include <stdatomic.h>
#include "transport.h"
#define MAX_SCENES 8 #define MAX_SCENES 8
#define MAX_CHANNELS 8 #define MAX_CHANNELS 8
#define MAX_CLIPS (MAX_SCENES * MAX_CHANNELS) // 64 #define MAX_CLIPS (MAX_SCENES * MAX_CHANNELS) // 64
#define MAX_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz #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 // Convert scene/channel to flat clip index
#define CLIP_INDEX(scene, channel) ((scene) * MAX_CHANNELS + (channel)) #define CLIP_INDEX(scene, channel) ((scene) * MAX_CHANNELS + (channel))
@@ -30,13 +29,6 @@ typedef enum {
QUANTIZE_BAR QUANTIZE_BAR
} QuantizeMode; } 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 { typedef struct {
ClipState state; ClipState state;
@@ -58,7 +50,13 @@ typedef enum {
CMD_SET_QUANTIZE_THRESHOLD, CMD_SET_QUANTIZE_THRESHOLD,
CMD_RESET_TRANSPORT, CMD_RESET_TRANSPORT,
CMD_UNDO, CMD_UNDO,
CMD_REDO CMD_REDO,
CMD_TRANSPORT_PLAY,
CMD_TRANSPORT_PAUSE,
CMD_TRANSPORT_STOP,
CMD_TRANSPORT_TOGGLE_PLAY,
CMD_SET_CLOCK_SOURCE,
CMD_SET_BPM
} CommandType; } CommandType;
// Undo/Redo action types // Undo/Redo action types
@@ -134,8 +132,7 @@ typedef struct {
jack_nframes_t sample_rate; jack_nframes_t sample_rate;
// Transport and clock // Transport and clock
TransportState transport; Transport *transport;
bool clock_sync_enabled;
// Quantization // Quantization
QuantizeMode quantize_mode; QuantizeMode quantize_mode;
@@ -146,11 +143,6 @@ typedef struct {
CommandQueue command_queue; CommandQueue command_queue;
// Atomic flags for simple state that frontend reads // Atomic flags for simple state that frontend reads
atomic_int transport_rolling; // bool
atomic_uint transport_clock_count;
atomic_uint transport_beat_position;
atomic_uint transport_bar_position;
atomic_uint transport_sample_position;
atomic_int quantize_mode_atomic; // QuantizeMode atomic_int quantize_mode_atomic; // QuantizeMode
atomic_uint quantize_threshold_atomic; atomic_uint quantize_threshold_atomic;
@@ -174,7 +166,12 @@ void engine_reset_clip(Engine *engine, int clip_index);
// Transport // Transport
void engine_set_quantize_mode(Engine *engine, QuantizeMode mode); void engine_set_quantize_mode(Engine *engine, QuantizeMode mode);
void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples); void engine_set_quantize_threshold(Engine *engine, jack_nframes_t samples);
void engine_reset_transport(Engine *engine); void engine_transport_play(Engine *engine);
void engine_transport_pause(Engine *engine);
void engine_transport_stop(Engine *engine);
void engine_transport_toggle_play(Engine *engine);
void engine_set_clock_source(Engine *engine, ClockSource source);
void engine_set_bpm(Engine *engine, double bpm);
// Queue management (exposed for testing) // Queue management (exposed for testing)
void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time); void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time);

View File

@@ -20,20 +20,13 @@ static Engine *create_test_engine(void) {
command_queue_init(&engine->command_queue); command_queue_init(&engine->command_queue);
// Initialize atomic state mirrors // 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_mode_atomic, (int)QUANTIZE_OFF);
atomic_store(&engine->quantize_threshold_atomic, 0); atomic_store(&engine->quantize_threshold_atomic, 0);
// Initialize transport // Initialize transport
engine->transport.rolling = false; engine->transport = (Transport *)calloc(1, sizeof(Transport));
engine->transport.clock_count = 0; assert(engine->transport != NULL);
engine->transport.beat_position = 0; transport_init(engine->transport, 48000);
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
for (int i = 0; i < MAX_CLIPS; i++) { for (int i = 0; i < MAX_CLIPS; i++) {
engine->clips[i].state = CLIP_EMPTY; engine->clips[i].state = CLIP_EMPTY;
@@ -57,6 +50,10 @@ static void destroy_test_engine(Engine *engine) {
qt = next; qt = next;
} }
if (engine->transport) {
transport_cleanup(engine->transport);
free(engine->transport);
}
for (int i = 0; i < MAX_CLIPS; i++) { for (int i = 0; i < MAX_CLIPS; i++) {
free(engine->clips[i].buffer); free(engine->clips[i].buffer);
} }
@@ -309,7 +306,7 @@ void test_transport_initial_state(void) {
printf("Test 13: Transport initial state... "); printf("Test 13: Transport initial state... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 0); assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0); assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0); assert(engine->transport.bar_position == 0);
@@ -325,16 +322,16 @@ void test_transport_reset(void) {
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
// Simulate some transport state // Simulate some transport state
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 100; engine->transport->clock_count = 100;
engine->transport.beat_position = 2; engine->transport->beat_position = 2;
engine->transport.bar_position = 5; engine->transport->bar_position = 5;
engine->transport.sample_position = 10000; engine->transport->sample_position = 10000;
engine_reset_transport(engine); engine_reset_transport(engine);
engine_process_commands(engine); engine_process_commands(engine);
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 0); assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0); assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0); assert(engine->transport.bar_position == 0);
@@ -444,10 +441,10 @@ void test_midi_clock_start(void) {
engine->transport.sample_position = 5000; engine->transport.sample_position = 5000;
// Process start message (simplified - just call the logic directly) // Process start message (simplified - just call the logic directly)
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 0; engine->transport->clock_count = 0;
engine->transport.beat_position = 0; engine->transport->beat_position = 0;
engine->transport.bar_position = 0; engine->transport->bar_position = 0;
engine->transport.sample_position = 0; engine->transport.sample_position = 0;
assert(engine->transport.rolling == true); assert(engine->transport.rolling == true);
@@ -465,14 +462,14 @@ void test_midi_clock_stop(void) {
printf("Test 20: MIDI clock stop message... "); printf("Test 20: MIDI clock stop message... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 100; engine->transport->clock_count = 100;
// Process stop message // Process stop message
engine->transport.rolling = false; engine->transport->state = TRANSPORT_STOPPED;
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 100); // Keep position assert(engine->transport->clock_count == 100); // Keep position
destroy_test_engine(engine); destroy_test_engine(engine);
printf("PASSED\n"); printf("PASSED\n");
@@ -483,14 +480,14 @@ void test_midi_clock_continue(void) {
printf("Test 21: MIDI clock continue message... "); printf("Test 21: MIDI clock continue message... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
engine->transport.rolling = false; engine->transport->state = TRANSPORT_STOPPED;
engine->transport.clock_count = 100; engine->transport->clock_count = 100;
// Process continue message // Process continue message
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
assert(engine->transport.rolling == true); assert(engine->transport->state == TRANSPORT_PLAYING);
assert(engine->transport.clock_count == 100); // Keep position assert(engine->transport->clock_count == 100); // Keep position
destroy_test_engine(engine); destroy_test_engine(engine);
printf("PASSED\n"); printf("PASSED\n");
@@ -574,9 +571,9 @@ void test_quantization_with_transport(void) {
printf("Test 24: Quantization with transport rolling... "); printf("Test 24: Quantization with transport rolling... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
engine->sample_rate = 48000; engine->sample_rate = 48000;
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in engine->transport->clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in
engine->transport.sample_position = engine->sample_rate * 2; // 2 beats in samples engine->transport->sample_position = engine->sample_rate * 2; // 2 beats in samples
// Set quantize to beat // Set quantize to beat
engine_set_quantize_mode(engine, QUANTIZE_BEAT); engine_set_quantize_mode(engine, QUANTIZE_BEAT);
@@ -608,9 +605,9 @@ void test_quantization_off_with_transport(void) {
printf("Test 25: Quantization off with transport rolling... "); printf("Test 25: Quantization off with transport rolling... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
engine->sample_rate = 48000; engine->sample_rate = 48000;
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 2; engine->transport->clock_count = MIDI_CLOCKS_PER_BEAT * 2;
engine->transport.sample_position = engine->sample_rate * 2; engine->transport->sample_position = engine->sample_rate * 2;
engine_set_quantize_mode(engine, QUANTIZE_OFF); engine_set_quantize_mode(engine, QUANTIZE_OFF);
@@ -629,7 +626,7 @@ void test_quantization_without_transport(void) {
printf("Test 26: Quantization without transport rolling... "); printf("Test 26: Quantization without transport rolling... ");
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
engine->sample_rate = 48000; engine->sample_rate = 48000;
engine->transport.rolling = false; engine->transport->state = TRANSPORT_STOPPED;
engine_set_quantize_mode(engine, QUANTIZE_BEAT); engine_set_quantize_mode(engine, QUANTIZE_BEAT);

View File

@@ -29,20 +29,13 @@ static Engine *create_test_engine(void) {
command_queue_init(&engine->command_queue); command_queue_init(&engine->command_queue);
// Initialize atomic state mirrors // 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_mode_atomic, (int)QUANTIZE_OFF);
atomic_store(&engine->quantize_threshold_atomic, 0); atomic_store(&engine->quantize_threshold_atomic, 0);
// Initialize transport // Initialize transport
engine->transport.rolling = false; engine->transport = (Transport *)calloc(1, sizeof(Transport));
engine->transport.clock_count = 0; assert(engine->transport != NULL);
engine->transport.beat_position = 0; transport_init(engine->transport, 48000);
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
for (int i = 0; i < MAX_CLIPS; i++) { for (int i = 0; i < MAX_CLIPS; i++) {
engine->clips[i].state = CLIP_EMPTY; engine->clips[i].state = CLIP_EMPTY;
@@ -65,6 +58,10 @@ static void destroy_test_engine(Engine *engine) {
qt = next; qt = next;
} }
if (engine->transport) {
transport_cleanup(engine->transport);
free(engine->transport);
}
for (int i = 0; i < MAX_CLIPS; i++) { for (int i = 0; i < MAX_CLIPS; i++) {
free(engine->clips[i].buffer); free(engine->clips[i].buffer);
} }
@@ -196,17 +193,17 @@ void test_transport_reset_via_tui(void) {
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
// Set up transport state // Set up transport state
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 100; engine->transport->clock_count = 100;
engine->transport.beat_position = 2; engine->transport->beat_position = 2;
engine->transport.bar_position = 5; engine->transport->bar_position = 5;
engine->transport.sample_position = 10000; engine->transport->sample_position = 10000;
// Simulate pressing 'x' // Simulate pressing 'x'
engine_reset_transport(engine); engine_reset_transport(engine);
engine_process_commands(engine); engine_process_commands(engine);
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 0); assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0); assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0); assert(engine->transport.bar_position == 0);
@@ -437,16 +434,16 @@ void test_multiple_transport_resets(void) {
// Reset transport multiple times // Reset transport multiple times
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 100 + i; engine->transport->clock_count = 100 + i;
engine->transport.beat_position = i % 4; engine->transport->beat_position = i % 4;
engine->transport.bar_position = i; engine->transport->bar_position = i;
engine->transport.sample_position = 10000 * i; engine->transport->sample_position = 10000 * i;
engine_reset_transport(engine); engine_reset_transport(engine);
engine_process_commands(engine); engine_process_commands(engine);
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 0); assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0); assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0); assert(engine->transport.bar_position == 0);
@@ -1531,26 +1528,26 @@ void test_undo_transport_reset(void) {
Engine *engine = create_test_engine(); Engine *engine = create_test_engine();
// Set up transport state // Set up transport state
engine->transport.rolling = true; engine->transport->state = TRANSPORT_PLAYING;
engine->transport.clock_count = 100; engine->transport->clock_count = 100;
engine->transport.beat_position = 2; engine->transport->beat_position = 2;
engine->transport.bar_position = 5; engine->transport->bar_position = 5;
engine->transport.sample_position = 10000; engine->transport->sample_position = 10000;
// Reset transport // Reset transport
engine_reset_transport(engine); engine_reset_transport(engine);
engine_process_commands(engine); engine_process_commands(engine);
assert(engine->transport.rolling == false); assert(engine->transport->state == TRANSPORT_STOPPED);
assert(engine->transport.clock_count == 0); assert(engine->transport->clock_count == 0);
// Undo: should restore transport state // Undo: should restore transport state
engine_undo_action(engine); engine_undo_action(engine);
engine_process_commands(engine); engine_process_commands(engine);
assert(engine->transport.rolling == true); assert(engine->transport->state == TRANSPORT_PLAYING);
assert(engine->transport.clock_count == 100); assert(engine->transport->clock_count == 100);
assert(engine->transport.beat_position == 2); assert(engine->transport->beat_position == 2);
assert(engine->transport.bar_position == 5); assert(engine->transport->bar_position == 5);
assert(engine->transport.sample_position == 10000); assert(engine->transport->sample_position == 10000);
printf("PASSED\n"); printf("PASSED\n");
destroy_test_engine(engine); destroy_test_engine(engine);

View File

@@ -0,0 +1,266 @@
#include "transport.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
void transport_init(Transport *transport, jack_nframes_t sample_rate) {
if (!transport) return;
memset(transport, 0, sizeof(Transport));
transport->state = TRANSPORT_STOPPED;
transport->clock_source = CLOCK_SOURCE_INTERNAL;
transport->bpm = DEFAULT_BPM;
transport->sample_rate = sample_rate;
transport->samples_per_beat = (sample_rate * 60.0) / DEFAULT_BPM;
transport->sample_accumulator = 0.0;
// Initialize atomic mirrors
atomic_store(&transport->state_atomic, TRANSPORT_STOPPED);
atomic_store(&transport->clock_count_atomic, 0);
atomic_store(&transport->beat_position_atomic, 0);
atomic_store(&transport->bar_position_atomic, 0);
atomic_store(&transport->sample_position_atomic, 0);
atomic_store(&transport->clock_source_atomic, CLOCK_SOURCE_INTERNAL);
atomic_store(&transport->bpm_atomic, DEFAULT_BPM);
}
void transport_cleanup(Transport *transport) {
// Nothing to free currently
(void)transport;
}
void transport_play(Transport *transport) {
if (!transport) return;
transport->state = TRANSPORT_PLAYING;
atomic_store(&transport->state_atomic, TRANSPORT_PLAYING);
// If starting from stopped, reset position
if (transport->clock_count == 0 && transport->sample_position == 0) {
transport->sample_accumulator = 0.0;
}
}
void transport_pause(Transport *transport) {
if (!transport) return;
transport->state = TRANSPORT_PAUSED;
atomic_store(&transport->state_atomic, TRANSPORT_PAUSED);
}
void transport_stop(Transport *transport) {
if (!transport) return;
transport->state = TRANSPORT_STOPPED;
transport->clock_count = 0;
transport->beat_position = 0;
transport->bar_position = 0;
transport->sample_position = 0;
transport->sample_accumulator = 0.0;
atomic_store(&transport->state_atomic, TRANSPORT_STOPPED);
atomic_store(&transport->clock_count_atomic, 0);
atomic_store(&transport->beat_position_atomic, 0);
atomic_store(&transport->bar_position_atomic, 0);
atomic_store(&transport->sample_position_atomic, 0);
}
void transport_toggle_play(Transport *transport) {
if (!transport) return;
if (transport->state == TRANSPORT_PLAYING) {
transport_pause(transport);
} else {
transport_play(transport);
}
}
void transport_set_clock_source(Transport *transport, ClockSource source) {
if (!transport) return;
transport->clock_source = source;
atomic_store(&transport->clock_source_atomic, source);
// Reset position when switching sources
transport->clock_count = 0;
transport->beat_position = 0;
transport->bar_position = 0;
transport->sample_position = 0;
transport->sample_accumulator = 0.0;
}
ClockSource transport_get_clock_source(Transport *transport) {
if (!transport) return CLOCK_SOURCE_INTERNAL;
return transport->clock_source;
}
void transport_set_bpm(Transport *transport, double bpm) {
if (!transport || bpm < 1.0 || bpm > 999.0) return;
transport->bpm = bpm;
transport->samples_per_beat = (transport->sample_rate * 60.0) / bpm;
atomic_store(&transport->bpm_atomic, bpm);
}
double transport_get_bpm(Transport *transport) {
if (!transport) return DEFAULT_BPM;
return transport->bpm;
}
int transport_process(Transport *transport, jack_nframes_t nframes,
void *midi_clock_in_buf, void *midi_clock_out_buf) {
if (!transport) return 0;
int clock_ticks_generated = 0;
if (transport->clock_source == CLOCK_SOURCE_MIDI) {
// Slave mode: process incoming MIDI clock
jack_midi_event_t midi_event;
jack_nframes_t event_index = 0;
while (jack_midi_event_get(&midi_event, midi_clock_in_buf, event_index) == 0) {
event_index++;
uint8_t *data = midi_event.buffer;
uint8_t status = data[0];
if (status == 0xF8) { // MIDI Clock
transport->clock_count++;
transport->sample_position =
(transport->clock_count * transport->sample_rate * 4) /
(MIDI_CLOCKS_PER_BEAT * BEATS_PER_BAR);
atomic_store(&transport->clock_count_atomic, transport->clock_count);
atomic_store(&transport->sample_position_atomic, transport->sample_position);
if (transport->clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
transport->beat_position =
(transport->beat_position + 1) % BEATS_PER_BAR;
atomic_store(&transport->beat_position_atomic, transport->beat_position);
if (transport->beat_position == 0) {
transport->bar_position++;
atomic_store(&transport->bar_position_atomic, transport->bar_position);
}
}
} else if (status == 0xFA) { // MIDI Start
transport->state = TRANSPORT_PLAYING;
transport->clock_count = 0;
transport->beat_position = 0;
transport->bar_position = 0;
transport->sample_position = 0;
atomic_store(&transport->state_atomic, TRANSPORT_PLAYING);
atomic_store(&transport->clock_count_atomic, 0);
atomic_store(&transport->beat_position_atomic, 0);
atomic_store(&transport->bar_position_atomic, 0);
atomic_store(&transport->sample_position_atomic, 0);
} else if (status == 0xFC) { // MIDI Stop
transport->state = TRANSPORT_STOPPED;
atomic_store(&transport->state_atomic, TRANSPORT_STOPPED);
} else if (status == 0xFB) { // MIDI Continue
transport->state = TRANSPORT_PLAYING;
atomic_store(&transport->state_atomic, TRANSPORT_PLAYING);
}
}
} else {
// Master mode: generate internal clock
if (transport->state == TRANSPORT_PLAYING) {
for (jack_nframes_t i = 0; i < nframes; i++) {
transport->sample_accumulator += 1.0;
if (transport->sample_accumulator >= transport->samples_per_beat / MIDI_CLOCKS_PER_BEAT) {
transport->sample_accumulator -= transport->samples_per_beat / MIDI_CLOCKS_PER_BEAT;
// Generate MIDI clock tick
if (midi_clock_out_buf) {
uint8_t clock_msg[1] = {0xF8};
if (jack_midi_event_write(midi_clock_out_buf, i, clock_msg, 1) != 0) {
fprintf(stderr, "Failed to write MIDI clock\n");
}
}
transport->clock_count++;
transport->sample_position =
(transport->clock_count * transport->sample_rate * 4) /
(MIDI_CLOCKS_PER_BEAT * BEATS_PER_BAR);
atomic_store(&transport->clock_count_atomic, transport->clock_count);
atomic_store(&transport->sample_position_atomic, transport->sample_position);
if (transport->clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
transport->beat_position =
(transport->beat_position + 1) % BEATS_PER_BAR;
atomic_store(&transport->beat_position_atomic, transport->beat_position);
if (transport->beat_position == 0) {
transport->bar_position++;
atomic_store(&transport->bar_position_atomic, transport->bar_position);
}
}
clock_ticks_generated++;
}
}
}
}
return clock_ticks_generated;
}
void transport_reset(Transport *transport) {
if (!transport) return;
transport->clock_count = 0;
transport->beat_position = 0;
transport->bar_position = 0;
transport->sample_position = 0;
transport->sample_accumulator = 0.0;
atomic_store(&transport->clock_count_atomic, 0);
atomic_store(&transport->beat_position_atomic, 0);
atomic_store(&transport->bar_position_atomic, 0);
atomic_store(&transport->sample_position_atomic, 0);
}
jack_nframes_t transport_get_next_quantize_frame(Transport *transport,
jack_nframes_t current_frame,
QuantizeMode mode) {
if (!transport || transport->state != TRANSPORT_PLAYING || mode == QUANTIZE_OFF) {
return current_frame;
}
// Calculate frames per beat
jack_nframes_t frames_per_beat = (jack_nframes_t)transport->samples_per_beat;
jack_nframes_t frames_per_bar = frames_per_beat * BEATS_PER_BAR;
// Current position in frames
jack_nframes_t current_pos = transport->sample_position + current_frame;
if (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 - 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 - transport->sample_position;
}
}
const char* transport_state_to_string(TransportState state) {
switch (state) {
case TRANSPORT_STOPPED: return "Stopped";
case TRANSPORT_PLAYING: return "Playing";
case TRANSPORT_PAUSED: return "Paused";
default: return "Unknown";
}
}
const char* clock_source_to_string(ClockSource source) {
switch (source) {
case CLOCK_SOURCE_INTERNAL: return "Internal";
case CLOCK_SOURCE_MIDI: return "MIDI";
default: return "Unknown";
}
}

43
tui.c
View File

@@ -277,8 +277,14 @@ static void draw_grid(void) {
"Selected: Clip %d | State: %s | Buffer: %zu samples", "Selected: Clip %d | State: %s | Buffer: %zu samples",
clip_idx, clip_state_to_string(clip->state), clip->buffer_size); clip_idx, clip_state_to_string(clip->state), clip->buffer_size);
TransportState transport_state = (TransportState)atomic_load(&g_engine->transport->state_atomic);
ClockSource clock_source = (ClockSource)atomic_load(&g_engine->transport->clock_source_atomic);
mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0,
"Quantize: %s | Threshold: %u", "Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Threshold: %u",
transport_state_to_string(transport_state),
clock_source_to_string(clock_source),
atomic_load(&g_engine->transport->bpm_atomic),
quantize_mode_to_string((QuantizeMode)atomic_load(&g_engine->quantize_mode_atomic)), quantize_mode_to_string((QuantizeMode)atomic_load(&g_engine->quantize_mode_atomic)),
(unsigned int)atomic_load(&g_engine->quantize_threshold_atomic)); (unsigned int)atomic_load(&g_engine->quantize_threshold_atomic));
@@ -307,14 +313,20 @@ static void draw_grid(void) {
mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0,
"s - Trigger scene (current row)"); "s - Trigger scene (current row)");
mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0,
"q - Toggle quantize mode (off/beat/bar)"); "Space - Play/Pause transport");
mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0,
"T - Set quantize threshold"); "S - Stop transport");
mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0,
"x - Reset transport"); "C - Toggle clock source (Internal/MIDI)");
mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0,
"? - Toggle help"); "q - Toggle quantize mode (off/beat/bar)");
mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0,
"T - Set quantize threshold");
mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0,
"x - Reset transport position");
mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0,
"? - Toggle help");
mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0,
"Esc/q - Quit"); "Esc/q - Quit");
attroff(COLOR_PAIR(COLOR_HELP)); attroff(COLOR_PAIR(COLOR_HELP));
} }
@@ -669,6 +681,27 @@ void tui_run(Engine *engine) {
break; break;
} }
case ' ': { // Space bar - toggle play/pause
engine_transport_toggle_play(engine);
engine_process_commands(engine);
break;
}
case 'S': { // Shift+S - stop transport
engine_transport_stop(engine);
engine_process_commands(engine);
break;
}
case 'C': { // Shift+C - toggle clock source
ClockSource current = transport_get_clock_source(engine->transport);
ClockSource next = (current == CLOCK_SOURCE_INTERNAL) ?
CLOCK_SOURCE_MIDI : CLOCK_SOURCE_INTERNAL;
engine_set_clock_source(engine, next);
engine_process_commands(engine);
break;
}
case 'x': case 'x':
engine_reset_transport(engine); engine_reset_transport(engine);
engine_process_commands(engine); engine_process_commands(engine);