From 5ed187a181d7cc789502fad1d92cee58c2d30082 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Mon, 4 May 2026 22:53:24 +0000 Subject: [PATCH] feat: replace mutex with lock-free ring buffer for real-time audio recording Co-authored-by: aider (deepseek/deepseek-coder) --- dispatcher.c | 33 +++++++++++++++++++++++++++++++-- dispatcher.h | 6 ++++++ engine.c | 7 ++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/dispatcher.c b/dispatcher.c index 47b0f48..bc853fd 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -152,11 +152,30 @@ static AppState clip_trigger(AppState state, int clip_index) { clip->buffer_size = 0; clip->read_position = 0; break; - case CLIP_RECORDING: + case CLIP_RECORDING: { + // Transition to looping: copy from ring buffer to clip buffer clip->state = CLIP_LOOPING; - clip->buffer_size = clip->write_position; clip->read_position = 0; + + // Determine which channel this clip belongs to + int channel = clip_index % MAX_CHANNELS; + + // Read from ring buffer + size_t wp = atomic_load(&state.record_write_pos[channel]); + size_t rp = atomic_load(&state.record_read_pos[channel]); + size_t available = wp - rp; + + if (available > 0 && clip->buffer != NULL) { + size_t to_copy = (available < MAX_BUFFER_SIZE) ? available : MAX_BUFFER_SIZE; + for (size_t i = 0; i < to_copy; i++) { + clip->buffer[i] = state.record_buffer[channel][(rp + i) % MAX_BUFFER_SIZE]; + } + clip->buffer_size = to_copy; + clip->write_position = to_copy; + atomic_store(&state.record_read_pos[channel], wp); + } break; + } case CLIP_LOOPING: clip->state = CLIP_STOPPED; clip->read_position = 0; @@ -236,6 +255,10 @@ static AppState clip_reset(AppState state, int clip_index) { clip->read_position = 0; if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); + // Also reset the ring buffer read position for this channel + int channel = clip_index % MAX_CHANNELS; + atomic_store(&state.record_read_pos[channel], atomic_load(&state.record_write_pos[channel])); + return state; } @@ -612,6 +635,12 @@ static void* dispatcher_thread_func(void *arg) { DispatchFn dispatcher_init(AppState *initial_state) { memcpy(&dispatcher.state, initial_state, sizeof(AppState)); + // Initialize ring buffer positions + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + atomic_store(&dispatcher.state.record_write_pos[ch], 0); + atomic_store(&dispatcher.state.record_read_pos[ch], 0); + } + // NEW: Ensure midi clip events are allocated (in case initial_state has NULL pointers) for (int i = 0; i < MAX_CLIPS; i++) { if (dispatcher.state.midi_clips[i].events == NULL) { diff --git a/dispatcher.h b/dispatcher.h index 1d0158b..6c29ab0 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -109,6 +109,12 @@ typedef struct { int current_batch_size; // NEW: current batch being recorded } undo; + // Ring buffers for real-time recording (written by JACK callback, read by dispatcher) + // Lock-free: single producer (JACK callback), single consumer (dispatcher thread) + float record_buffer[MAX_CHANNELS][MAX_BUFFER_SIZE]; + atomic_size_t record_write_pos[MAX_CHANNELS]; // Only written by JACK callback + atomic_size_t record_read_pos[MAX_CHANNELS]; // Only written by dispatcher thread + // Carla host CarlaHost carla_host; diff --git a/engine.c b/engine.c index 0081ca0..a70df0b 100644 --- a/engine.c +++ b/engine.c @@ -144,9 +144,10 @@ static int process_callback(jack_nframes_t nframes, void *arg) { Clip *clip = &state->clips[clip_idx]; if (clip->state == CLIP_RECORDING) { - if (clip->write_position < MAX_BUFFER_SIZE && clip->buffer != NULL) { - clip->buffer[clip->write_position++] = audio_in[ch][i]; - } + // Write to lock-free ring buffer instead of clip buffer directly + size_t wp = atomic_load(&state->record_write_pos[ch]); + state->record_buffer[ch][wp % MAX_BUFFER_SIZE] = audio_in[ch][i]; + atomic_store(&state->record_write_pos[ch], wp + 1); } if (clip->state == CLIP_LOOPING && clip->buffer_size > 0 && clip->buffer != NULL) {