diff --git a/engine.c b/engine.c index 3b3b7d8..4409adf 100644 --- a/engine.c +++ b/engine.c @@ -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) { diff --git a/engine.h b/engine.h index 4954dfc..714abd2 100644 --- a/engine.h +++ b/engine.h @@ -6,13 +6,12 @@ #include #include #include +#include "transport.h" #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)) @@ -30,13 +29,6 @@ typedef enum { 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; @@ -58,7 +50,13 @@ typedef enum { CMD_SET_QUANTIZE_THRESHOLD, CMD_RESET_TRANSPORT, 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; // Undo/Redo action types @@ -134,8 +132,7 @@ typedef struct { jack_nframes_t sample_rate; // Transport and clock - TransportState transport; - bool clock_sync_enabled; + Transport *transport; // Quantization QuantizeMode quantize_mode; @@ -146,11 +143,6 @@ typedef struct { CommandQueue command_queue; // 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_uint quantize_threshold_atomic; @@ -174,7 +166,12 @@ 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); +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) void queue_trigger(Engine *engine, int clip_index, bool is_scene, jack_nframes_t time); diff --git a/test_engine.c b/test_engine.c index bfbc9b0..8f5c63e 100644 --- a/test_engine.c +++ b/test_engine.c @@ -20,20 +20,13 @@ static Engine *create_test_engine(void) { 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)); + assert(engine->transport != NULL); + transport_init(engine->transport, 48000); for (int i = 0; i < MAX_CLIPS; i++) { engine->clips[i].state = CLIP_EMPTY; @@ -57,6 +50,10 @@ static void destroy_test_engine(Engine *engine) { qt = next; } + if (engine->transport) { + transport_cleanup(engine->transport); + free(engine->transport); + } for (int i = 0; i < MAX_CLIPS; i++) { free(engine->clips[i].buffer); } @@ -309,7 +306,7 @@ void test_transport_initial_state(void) { printf("Test 13: Transport initial state... "); 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.beat_position == 0); assert(engine->transport.bar_position == 0); @@ -325,16 +322,16 @@ void test_transport_reset(void) { Engine *engine = create_test_engine(); // Simulate some transport state - engine->transport.rolling = true; - engine->transport.clock_count = 100; - engine->transport.beat_position = 2; - engine->transport.bar_position = 5; - engine->transport.sample_position = 10000; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = 100; + engine->transport->beat_position = 2; + engine->transport->bar_position = 5; + engine->transport->sample_position = 10000; engine_reset_transport(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.beat_position == 0); assert(engine->transport.bar_position == 0); @@ -444,10 +441,10 @@ void test_midi_clock_start(void) { engine->transport.sample_position = 5000; // Process start message (simplified - just call the logic directly) - engine->transport.rolling = true; - engine->transport.clock_count = 0; - engine->transport.beat_position = 0; - engine->transport.bar_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; assert(engine->transport.rolling == true); @@ -465,14 +462,14 @@ void test_midi_clock_stop(void) { printf("Test 20: MIDI clock stop message... "); Engine *engine = create_test_engine(); - engine->transport.rolling = true; - engine->transport.clock_count = 100; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = 100; // Process stop message - engine->transport.rolling = false; + engine->transport->state = TRANSPORT_STOPPED; - assert(engine->transport.rolling == false); - assert(engine->transport.clock_count == 100); // Keep position + assert(engine->transport->state == TRANSPORT_STOPPED); + assert(engine->transport->clock_count == 100); // Keep position destroy_test_engine(engine); printf("PASSED\n"); @@ -483,14 +480,14 @@ void test_midi_clock_continue(void) { printf("Test 21: MIDI clock continue message... "); Engine *engine = create_test_engine(); - engine->transport.rolling = false; - engine->transport.clock_count = 100; + engine->transport->state = TRANSPORT_STOPPED; + engine->transport->clock_count = 100; // Process continue message - engine->transport.rolling = true; + engine->transport->state = TRANSPORT_PLAYING; - assert(engine->transport.rolling == true); - assert(engine->transport.clock_count == 100); // Keep position + assert(engine->transport->state == TRANSPORT_PLAYING); + assert(engine->transport->clock_count == 100); // Keep position destroy_test_engine(engine); printf("PASSED\n"); @@ -574,9 +571,9 @@ void test_quantization_with_transport(void) { printf("Test 24: Quantization with transport rolling... "); Engine *engine = create_test_engine(); engine->sample_rate = 48000; - engine->transport.rolling = true; - 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->state = TRANSPORT_PLAYING; + engine->transport->clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in + engine->transport->sample_position = engine->sample_rate * 2; // 2 beats in samples // Set quantize to 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... "); Engine *engine = create_test_engine(); engine->sample_rate = 48000; - engine->transport.rolling = true; - engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 2; - engine->transport.sample_position = engine->sample_rate * 2; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = MIDI_CLOCKS_PER_BEAT * 2; + engine->transport->sample_position = engine->sample_rate * 2; engine_set_quantize_mode(engine, QUANTIZE_OFF); @@ -629,7 +626,7 @@ void test_quantization_without_transport(void) { printf("Test 26: Quantization without transport rolling... "); Engine *engine = create_test_engine(); engine->sample_rate = 48000; - engine->transport.rolling = false; + engine->transport->state = TRANSPORT_STOPPED; engine_set_quantize_mode(engine, QUANTIZE_BEAT); diff --git a/test_tui.c b/test_tui.c index 119fc50..d5c8fe5 100644 --- a/test_tui.c +++ b/test_tui.c @@ -29,20 +29,13 @@ static Engine *create_test_engine(void) { 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)); + assert(engine->transport != NULL); + transport_init(engine->transport, 48000); for (int i = 0; i < MAX_CLIPS; i++) { engine->clips[i].state = CLIP_EMPTY; @@ -65,6 +58,10 @@ static void destroy_test_engine(Engine *engine) { qt = next; } + if (engine->transport) { + transport_cleanup(engine->transport); + free(engine->transport); + } for (int i = 0; i < MAX_CLIPS; i++) { free(engine->clips[i].buffer); } @@ -196,17 +193,17 @@ void test_transport_reset_via_tui(void) { Engine *engine = create_test_engine(); // Set up transport state - engine->transport.rolling = true; - engine->transport.clock_count = 100; - engine->transport.beat_position = 2; - engine->transport.bar_position = 5; - engine->transport.sample_position = 10000; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = 100; + engine->transport->beat_position = 2; + engine->transport->bar_position = 5; + engine->transport->sample_position = 10000; // Simulate pressing 'x' engine_reset_transport(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.beat_position == 0); assert(engine->transport.bar_position == 0); @@ -437,16 +434,16 @@ void test_multiple_transport_resets(void) { // Reset transport multiple times for (int i = 0; i < 5; i++) { - engine->transport.rolling = true; - engine->transport.clock_count = 100 + i; - engine->transport.beat_position = i % 4; - engine->transport.bar_position = i; - engine->transport.sample_position = 10000 * i; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = 100 + i; + engine->transport->beat_position = i % 4; + engine->transport->bar_position = i; + engine->transport->sample_position = 10000 * i; engine_reset_transport(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.beat_position == 0); assert(engine->transport.bar_position == 0); @@ -1531,26 +1528,26 @@ void test_undo_transport_reset(void) { Engine *engine = create_test_engine(); // Set up transport state - engine->transport.rolling = true; - engine->transport.clock_count = 100; - engine->transport.beat_position = 2; - engine->transport.bar_position = 5; - engine->transport.sample_position = 10000; + engine->transport->state = TRANSPORT_PLAYING; + engine->transport->clock_count = 100; + engine->transport->beat_position = 2; + engine->transport->bar_position = 5; + engine->transport->sample_position = 10000; // Reset transport engine_reset_transport(engine); engine_process_commands(engine); - assert(engine->transport.rolling == false); - assert(engine->transport.clock_count == 0); + assert(engine->transport->state == TRANSPORT_STOPPED); + assert(engine->transport->clock_count == 0); // Undo: should restore transport state engine_undo_action(engine); engine_process_commands(engine); - assert(engine->transport.rolling == true); - assert(engine->transport.clock_count == 100); - assert(engine->transport.beat_position == 2); - assert(engine->transport.bar_position == 5); - assert(engine->transport.sample_position == 10000); + assert(engine->transport->state == TRANSPORT_PLAYING); + assert(engine->transport->clock_count == 100); + assert(engine->transport->beat_position == 2); + assert(engine->transport->bar_position == 5); + assert(engine->transport->sample_position == 10000); printf("PASSED\n"); destroy_test_engine(engine); diff --git a/transport.c b/transport.c index e69de29..80a22f0 100644 --- a/transport.c +++ b/transport.c @@ -0,0 +1,266 @@ +#include "transport.h" +#include +#include +#include + +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"; + } +} diff --git a/tui.c b/tui.c index 11a6eca..a6d9e76 100644 --- a/tui.c +++ b/tui.c @@ -277,8 +277,14 @@ static void draw_grid(void) { "Selected: Clip %d | State: %s | Buffer: %zu samples", 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, - "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)), (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, "s - Trigger scene (current row)"); 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, - "T - Set quantize threshold"); + "S - Stop transport"); mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, - "x - Reset transport"); + "C - Toggle clock source (Internal/MIDI)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, - "? - Toggle help"); + "q - Toggle quantize mode (off/beat/bar)"); 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"); attroff(COLOR_PAIR(COLOR_HELP)); } @@ -669,6 +681,27 @@ void tui_run(Engine *engine) { 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': engine_reset_transport(engine); engine_process_commands(engine);