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:
237
engine.c
237
engine.c
@@ -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) {
|
||||||
|
|||||||
33
engine.h
33
engine.h
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
65
test_tui.c
65
test_tui.c
@@ -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);
|
||||||
|
|||||||
266
transport.c
266
transport.c
@@ -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
43
tui.c
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user