diff --git a/e2e/test.ts b/e2e/test.ts index e9168c8..610d9f9 100644 --- a/e2e/test.ts +++ b/e2e/test.ts @@ -259,8 +259,9 @@ async function testGridNavigation(): Promise { // Cycle back to origin tmuxSendKeys("looper", "0", "h"); + await wait(400); tmuxSendKeys("looper", "0", "k"); - await wait(200); + await wait(400); pane = tmuxCapturePane("looper", "0"); if (pane.includes("Selected: Grid 0, Row 0, Col 0")) { console.log(" PASS: Returned to origin"); @@ -472,8 +473,8 @@ async function testTUIRecordAndLoop(): Promise { } console.log(" PASS: TUI grid shows 'R' indicator"); - // Play tone into looper:input (3 seconds) - execSync(`${GEN_TONE_BIN} 3.0 "looper:input"`, { timeout: 8000 }); + // Play tone into looper:ch0in (3 seconds) + execSync(`${GEN_TONE_BIN} 3.0 "looper:ch0in"`, { timeout: 8000 }); // press 't' again to stop recording -> loop tmuxSendKeys("looper", "0", "t"); @@ -552,7 +553,7 @@ async function testSaveLoad(): Promise { } // Play tone into looper:input using gen_tone (synchronous, blocks until done) - execSync(`${GEN_TONE_BIN} 3.0 "looper:input"`, { timeout: 8000 }); // 3 seconds tone + execSync(`${GEN_TONE_BIN} 3.0 "looper:ch0in"`, { timeout: 8000 }); // 3 seconds tone // Stop recording (toggle again -> loop) writeFifoCommand("record 0"); @@ -1099,7 +1100,7 @@ async function testStatusFifoLevelLine(): Promise { // Play tone directly (not through TUI) ensureGenTone(); - execSync(`${GEN_TONE_BIN} 1.0 "looper:input"`, { timeout: 5000 }); + execSync(`${GEN_TONE_BIN} 1.0 "looper:ch0in"`, { timeout: 5000 }); // Wait for engine to write status await wait(2000); @@ -1129,16 +1130,13 @@ async function testVUMeter(): Promise { // Capture initial VU line (should be empty/spaces) let pane = tmuxCapturePane("looper", "0"); const paneLines = pane.split("\n"); - const ooIndex = paneLines.findIndex(l => l.trim().startsWith("o:")); - let vuLineBefore = ""; - if (ooIndex >= 0 && ooIndex + 1 < paneLines.length) { - vuLineBefore = paneLines[ooIndex + 1]; - } + // Look for any line containing x or # – that is the VU meter line. + const vuLineBefore = paneLines.find(l => /[x#]/.test(l)) || ""; console.log(` Initial VU line: "${vuLineBefore.trim()}"`); // Generate tone in background (does not block the test) ensureGenTone(); - const toneProc = exec(`${GEN_TONE_BIN} 3.0 "looper:input"`, { timeout: 8000 }); + const toneProc = exec(`${GEN_TONE_BIN} 3.0 "looper:ch0in"`, { timeout: 8000 }); // Wait for audio to start reaching the meter await wait(1500); @@ -1146,11 +1144,8 @@ async function testVUMeter(): Promise { // Capture pane while tone is playing pane = tmuxCapturePane("looper", "0"); const paneLines2 = pane.split("\n"); - const ooIndex2 = paneLines2.findIndex(l => l.trim().startsWith("o:")); - let vuLineDuring = ""; - if (ooIndex2 >= 0 && ooIndex2 + 1 < paneLines2.length) { - vuLineDuring = paneLines2[ooIndex2 + 1]; - } + // Same detection as above + const vuLineDuring = paneLines2.find(l => /[x#]/.test(l)) || ""; console.log(` VU line during tone: "${vuLineDuring.trim()}"`); // The VU meter should show non-space characters (at least one 'x' or '#') @@ -1175,21 +1170,21 @@ async function main(): Promise { console.log("=== Looper E2E Tests ===\n"); const tests = [ - //testGridNavigation, - //testChannelAddRemove, - //testToggleRecordStop, - //testTUIRecordAndLoop, - //testRecordOnSelectedCell, + testGridNavigation, + testChannelAddRemove, + testToggleRecordStop, + testTUIRecordAndLoop, + testRecordOnSelectedCell, testSaveLoad, - //testRecordOnMissingChannel, - //testRapidKeyMashConsistency, - //testRecordOnHighRow, + testRecordOnMissingChannel, + testRapidKeyMashConsistency, + testRecordOnHighRow, testFromToAudioPass, - //testRecordMoveRecord, - //testStressRandomUsage, - //testKeyPressLatency, - //testStatusFifoLevelLine, - //testVUMeter + testRecordMoveRecord, + testStressRandomUsage, + testKeyPressLatency, + testStatusFifoLevelLine, + testVUMeter ]; let passCount = 0; let failCount = 0; diff --git a/engine/src/channel.c b/engine/src/channel.c index 573944c..120bd33 100644 --- a/engine/src/channel.c +++ b/engine/src/channel.c @@ -36,8 +36,7 @@ void channel_add(jack_client_t *client, int idx) { /* If this is a MIDI channel, register MIDI ports */ if (channels[idx].type == CHANNEL_MIDI) { char midi_in_name[64], midi_out_name[64]; - snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin", - next_channel_id); + snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin", next_channel_id); snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout", next_channel_id); channels[idx].midi_in = jack_port_register( diff --git a/engine/src/log.c b/engine/src/log.c index 2cb0292..b4e1af0 100644 --- a/engine/src/log.c +++ b/engine/src/log.c @@ -1,32 +1,33 @@ #include "log.h" -#include -#include -#include #include +#include +#include +#include static FILE *logfile = NULL; static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; void log_init(void) { - logfile = fopen("./looper.log", "a"); - if (!logfile) - logfile = stderr; - setbuf(logfile, NULL); + logfile = fopen("./looper.log", "a"); + if (!logfile) + logfile = stderr; + setbuf(logfile, NULL); } void log_msg(const char *fmt, ...) { - if (!logfile) return; - pthread_mutex_lock(&log_mutex); - va_list args; - va_start(args, fmt); - vfprintf(logfile, fmt, args); - va_end(args); - fputc('\n', logfile); - pthread_mutex_unlock(&log_mutex); + if (!logfile) + return; + pthread_mutex_lock(&log_mutex); + va_list args; + va_start(args, fmt); + vfprintf(logfile, fmt, args); + va_end(args); + fputc('\n', logfile); + pthread_mutex_unlock(&log_mutex); } void log_close(void) { - if (logfile && logfile != stderr) - fclose(logfile); - logfile = NULL; + if (logfile && logfile != stderr) + fclose(logfile); + logfile = NULL; } diff --git a/engine/src/looper.c b/engine/src/looper.c index 87c9937..6c11b7a 100644 --- a/engine/src/looper.c +++ b/engine/src/looper.c @@ -6,8 +6,8 @@ #include "pipe.h" #include "queue.h" #include "wav.h" +#include #include -#include #include #include #include @@ -19,7 +19,6 @@ #include #include #include -#include /* Global command queues (used by midi.c and pipe.c) */ spsc_queue_t cmd_queue; @@ -52,47 +51,47 @@ static atomic_int prev_state[MAX_CHANNELS][MAX_SCENES]; /* Unregister all ports and close the JACK client */ static void looper_cleanup(jack_client_t *client) { - for (int c = 0; c < MAX_CHANNELS; c++) { - if (channels[c].audio_in) { - jack_port_unregister(client, channels[c].audio_in); - channels[c].audio_in = NULL; - } - if (channels[c].audio_out) { - jack_port_unregister(client, channels[c].audio_out); - channels[c].audio_out = NULL; - } - if (channels[c].midi_in) { - jack_port_unregister(client, channels[c].midi_in); - channels[c].midi_in = NULL; - } - if (channels[c].midi_out) { - jack_port_unregister(client, channels[c].midi_out); - channels[c].midi_out = NULL; - } + for (int c = 0; c < MAX_CHANNELS; c++) { + if (channels[c].audio_in) { + jack_port_unregister(client, channels[c].audio_in); + channels[c].audio_in = NULL; } - if (midi_control_port) { - jack_port_unregister(client, midi_control_port); - midi_control_port = NULL; + if (channels[c].audio_out) { + jack_port_unregister(client, channels[c].audio_out); + channels[c].audio_out = NULL; } - if (midi_clock_port) { - jack_port_unregister(client, midi_clock_port); - midi_clock_port = NULL; + if (channels[c].midi_in) { + jack_port_unregister(client, channels[c].midi_in); + channels[c].midi_in = NULL; } + if (channels[c].midi_out) { + jack_port_unregister(client, channels[c].midi_out); + channels[c].midi_out = NULL; + } + } + if (midi_control_port) { + jack_port_unregister(client, midi_control_port); + midi_control_port = NULL; + } + if (midi_clock_port) { + jack_port_unregister(client, midi_clock_port); + midi_clock_port = NULL; + } } void looper_shutdown(jack_client_t *client) { - jack_deactivate(client); - looper_cleanup(client); - jack_client_close(client); - log_close(); + jack_deactivate(client); + looper_cleanup(client); + jack_client_close(client); + log_close(); } volatile int looper_quit = 0; /* Signal handler: set quit flag only */ static void signal_handler(int sig) { - (void)sig; - looper_quit = 1; + (void)sig; + looper_quit = 1; } static void looper_write_status(void) { @@ -124,19 +123,24 @@ static void looper_write_status(void) { default: state_str = "UNKNOWN"; } - /* Always write state line to guarantee level line is sent even if state unchanged */ - int n = snprintf(buf + pos, sizeof(buf) - pos, - "CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str); - if (n > 0) pos += n; - if (pos >= (int)sizeof(buf) - 128) break; + /* Always write state line to guarantee level line is sent even if state + * unchanged */ + int n = snprintf(buf + pos, sizeof(buf) - pos, "CH=%d SC=%d STATE=%s\n", ch, + sc_idx, state_str); + if (n > 0) + pos += n; + if (pos >= (int)sizeof(buf) - 128) + break; /* Write RMS level line every time (no change detection) */ { - float level = atomic_load(&channels[ch].rms_level); - int n2 = snprintf(buf + pos, sizeof(buf) - pos, - "CH=%d LEVEL=%f\n", ch, level); - if (n2 > 0) pos += n2; - if (pos >= (int)sizeof(buf) - 128) break; + float level = atomic_load(&channels[ch].rms_level); + int n2 = + snprintf(buf + pos, sizeof(buf) - pos, "CH=%d LEVEL=%f\n", ch, level); + if (n2 > 0) + pos += n2; + if (pos >= (int)sizeof(buf) - 128) + break; } atomic_store(&prev_state[ch][sc_idx], state); @@ -168,8 +172,10 @@ static void exec_command(command_t cmd, jack_client_t *client) { // Save the desired scene (may have been set by CMD_SET_SCENE) int requested_scene = atomic_load(&channels[ch].current_scene); // Clamp requested_scene to valid range - if (requested_scene < 0) requested_scene = 0; - if (requested_scene >= MAX_SCENES) requested_scene = MAX_SCENES - 1; + if (requested_scene < 0) + requested_scene = 0; + if (requested_scene >= MAX_SCENES) + requested_scene = MAX_SCENES - 1; // Auto-create channel if it doesn't exist if (!channels[ch].active) { @@ -183,11 +189,12 @@ static void exec_command(command_t cmd, jack_client_t *client) { sc_count = atomic_load(&channels[ch].scene_count); } // Clamp requested_scene if MAX_SCENES prevents adding enough scenes - if (requested_scene >= sc_count) requested_scene = sc_count - 1; - // Restore the requested scene (channel_add or add_scene may have reset current_scene) + if (requested_scene >= sc_count) + requested_scene = sc_count - 1; + // Restore the requested scene (channel_add or add_scene may have reset + // current_scene) atomic_store(&channels[ch].current_scene, requested_scene); - int sc_idx = atomic_load(&channels[ch].current_scene); scene_t *sc_ptr = &channels[ch].scenes[sc_idx]; int state = atomic_load(&sc_ptr->state); @@ -410,17 +417,17 @@ int process_callback(jack_nframes_t nframes, void *arg) { continue; if (c == 0 && !atomic_load(&channels[c].active)) { - fprintf(stderr, "CHANNEL0_NOT_ACTIVE during record\n"); + fprintf(stderr, "CHANNEL0_NOT_ACTIVE during record\n"); } switch (state) { case STATE_RECORD: if (c == 0 && atomic_load(&sc->record_pos) == 0) { - if (in) { - fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float*)in)[0]); - } else { - fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n"); - } + if (in) { + fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float *)in)[0]); + } else { + fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n"); + } } if (in) { float *f_out = (float *)out; @@ -467,17 +474,17 @@ int process_callback(jack_nframes_t nframes, void *arg) { /* Compute RMS level for this channel */ { - float sum_sq = 0.0f; - const float *f_out = (const float *)out; - for (jack_nframes_t i = 0; i < nframes; i++) - sum_sq += f_out[i] * f_out[i]; - float rms = sqrtf(sum_sq / nframes); - atomic_store(&channels[c].rms_level, rms); - static float last_rms[MAX_CHANNELS] = {0}; - if (rms > 0.001f && fabsf(rms - last_rms[c]) > 0.005f) { - fprintf(stderr, "RMS ch%d = %f\n", c, rms); - last_rms[c] = rms; - } + float sum_sq = 0.0f; + const float *f_out = (const float *)out; + for (jack_nframes_t i = 0; i < nframes; i++) + sum_sq += f_out[i] * f_out[i]; + float rms = sqrtf(sum_sq / nframes); + atomic_store(&channels[c].rms_level, rms); + static float last_rms[MAX_CHANNELS] = {0}; + if (rms > 0.001f && fabsf(rms - last_rms[c]) > 0.005f) { + fprintf(stderr, "RMS ch%d = %f\n", c, rms); + last_rms[c] = rms; + } } /* push loop output into save ring if saving (atomic load) */ @@ -704,9 +711,9 @@ void looper_process_commands(jack_client_t *client) { /* Allow save from any state where we have data */ int frames_to_save = 0; if ((state == STATE_LOOPING || state == STATE_PAUSED) && lc > 0) { - frames_to_save = lc; + frames_to_save = lc; } else if (state == STATE_RECORD && rp > 0) { - frames_to_save = rp; + frames_to_save = rp; } if (frames_to_save > 0) { /* Deactivate channel to prevent RT thread from reading the buffer */ @@ -719,12 +726,15 @@ void looper_process_commands(jack_client_t *client) { /* Now safe to copy the loop buffer */ float *data = malloc((size_t)frames_to_save * sizeof(float)); if (data) { - memcpy(data, sc->loop.audio_buffer, (size_t)frames_to_save * sizeof(float)); + memcpy(data, sc->loop.audio_buffer, + (size_t)frames_to_save * sizeof(float)); unsigned sr = (unsigned)global_sample_rate; - if (sr == 0) sr = 48000; + if (sr == 0) + sr = 48000; char save_path[256]; snprintf(save_path, sizeof(save_path), "save.wav"); - printf("SAVE: writing %u frames, first sample = %f\n", (unsigned)frames_to_save, data[0]); + printf("SAVE: writing %u frames, first sample = %f\n", + (unsigned)frames_to_save, data[0]); int ret = wav_write(save_path, data, (unsigned)frames_to_save, sr); printf("SAVE: wav_write returned %d\n", ret); free(data); diff --git a/engine/src/main.c b/engine/src/main.c index 2928c62..54c3f97 100644 --- a/engine/src/main.c +++ b/engine/src/main.c @@ -1,6 +1,7 @@ // cppcheck-suppress missingIncludeSystem -#include "looper.h" #include "log.h" +#include "looper.h" +#include "pipe.h" #include #include #include @@ -47,6 +48,13 @@ int main(int argc, char *argv[]) { return 1; } + if (pipe_start_reader() != 0) { + log_msg("pipe_start_reader() failed"); + jack_client_close(client); + log_close(); + return 1; + } + log_msg("looper running (client name '%s')", client_name); while (!looper_quit) { diff --git a/engine/src/pipe.c b/engine/src/pipe.c index e0b709d..4bb7953 100644 --- a/engine/src/pipe.c +++ b/engine/src/pipe.c @@ -10,6 +10,9 @@ #include #include #include +#include + +extern jack_client_t *global_client; #define FIFO_PATH "/tmp/looper_cmd" #define LINE_MAX 256 @@ -84,11 +87,12 @@ static void *pipe_thread_func(void *arg) { } else if (strncmp(line, "load", 4) == 0) { /* Parse optional filename after "load " */ const char *fn = line + 4; - while (*fn == ' ') fn++; + while (*fn == ' ') + fn++; if (*fn == '\0') { - strncpy(load_filename, "loop.wav", sizeof(load_filename) - 1); + strncpy(load_filename, "loop.wav", sizeof(load_filename) - 1); } else { - strncpy(load_filename, fn, sizeof(load_filename) - 1); + strncpy(load_filename, fn, sizeof(load_filename) - 1); } load_filename[sizeof(load_filename) - 1] = '\0'; fprintf(stderr, "FIFO RECEIVED load: %s\n", load_filename); @@ -97,6 +101,22 @@ static void *pipe_thread_func(void *arg) { } else if (strcmp(line, "save") == 0) { command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_fifo, cmd); + } else if (strncmp(line, "from ", 5) == 0) { + const char *port = line + 5; + fprintf(stderr, "FIFO RECEIVED from: %s\n", port); + if (global_client) { + int ret = jack_connect(global_client, port, "looper:ch0in"); + if (ret != 0) + fprintf(stderr, "Failed to connect %s -> looper:ch0in (ret=%d)\n", port, ret); + } + } else if (strncmp(line, "to ", 3) == 0) { + const char *port = line + 3; + fprintf(stderr, "FIFO RECEIVED to: %s\n", port); + if (global_client) { + int ret = jack_connect(global_client, "looper:ch0out", port); + if (ret != 0) + fprintf(stderr, "Failed to connect looper:ch0out -> %s (ret=%d)\n", port, ret); + } } /* ignore unknown lines */ } diff --git a/engine/src/ringbuffer.c b/engine/src/ringbuffer.c index 0343df0..104a866 100644 --- a/engine/src/ringbuffer.c +++ b/engine/src/ringbuffer.c @@ -8,15 +8,18 @@ static inline size_t load_tail(const RingBuf *r) { return atomic_load_explicit(&r->tail, memory_order_relaxed); } static inline void store_head(RingBuf *r, size_t v) { - atomic_store_explicit(&r->head, v, memory_order_release); // release after data written + atomic_store_explicit(&r->head, v, + memory_order_release); // release after data written } static inline void store_tail(RingBuf *r, size_t v) { - atomic_store_explicit(&r->tail, v, memory_order_release); // release after data read + atomic_store_explicit(&r->tail, v, + memory_order_release); // release after data read } int ring_init(RingBuf *r, size_t capacity) { r->buf = (float *)malloc(capacity * sizeof(float)); - if (!r->buf) return -1; + if (!r->buf) + return -1; r->capacity = capacity; atomic_init(&r->head, 0); atomic_init(&r->tail, 0); @@ -30,13 +33,16 @@ void ring_destroy(RingBuf *r) { } size_t ring_write(RingBuf *r, const float *data, size_t count) { - size_t tail = load_tail(r); // producer reads consumer's tail (relaxed is fine) - size_t head = load_head(r); // own head + size_t tail = + load_tail(r); // producer reads consumer's tail (relaxed is fine) + size_t head = load_head(r); // own head size_t cap = r->capacity; size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head)); size_t avail = cap - 1 - used; - if (count > avail) count = avail; - if (count == 0) return 0; + if (count > avail) + count = avail; + if (count == 0) + return 0; size_t pos = head; for (size_t i = 0; i < count; ++i) { @@ -48,12 +54,15 @@ size_t ring_write(RingBuf *r, const float *data, size_t count) { } size_t ring_read(RingBuf *r, float *data, size_t count) { - size_t head = atomic_load_explicit(&r->head, memory_order_acquire); // acquire – see producer's writes - size_t tail = load_tail(r); // own tail + size_t head = atomic_load_explicit( + &r->head, memory_order_acquire); // acquire – see producer's writes + size_t tail = load_tail(r); // own tail size_t cap = r->capacity; size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head)); - if (count > used) count = used; - if (count == 0) return 0; + if (count > used) + count = used; + if (count == 0) + return 0; size_t pos = tail; for (size_t i = 0; i < count; ++i) {