2 Commits

Author SHA1 Message Date
Loic Coenen
32fb5d3524 refactor: enable all e2e tests and fix audio port naming 2026-06-05 19:39:53 +00:00
Loic Coenen
20176517a4 refactor: rename looper ports to ch0in/ch0out and move connection logic to client 2026-05-31 13:05:28 +00:00
10 changed files with 202 additions and 236 deletions

View File

@@ -322,15 +322,6 @@ bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufs
return true; return true;
} }
} }
// Also look for direct connections to looper:input / looper:output (channel 0)
const char *direct_needle = is_input ? "looper:input" : "looper:output";
for (int i = 0; i < conn_count; i++) {
if (strcmp(connections[i].looper_port, direct_needle) == 0) {
strncpy(buf, connections[i].plugin_port, bufsize - 1);
buf[bufsize - 1] = '\0';
return true;
}
}
buf[0] = '\0'; buf[0] = '\0';
return false; return false;
} }

View File

@@ -36,14 +36,14 @@ int handle_client_command(const char *input, int *out_id) {
if (strcmp(token, "from") == 0) { if (strcmp(token, "from") == 0) {
const char *port = strtok(NULL, " "); const char *port = strtok(NULL, " ");
if (!port) return -1; if (!port) return -1;
int ret = carla_connect_direct(port, "looper:input"); int ret = carla_connect_direct(port, "looper:ch0in");
if (ret == 0) { if (ret == 0) {
strncpy(from_port, port, sizeof(from_port)-1); strncpy(from_port, port, sizeof(from_port)-1);
from_port[sizeof(from_port)-1] = '\0'; from_port[sizeof(from_port)-1] = '\0';
g_connect_error[0] = '\0'; g_connect_error[0] = '\0';
} else { } else {
snprintf(g_connect_error, sizeof(g_connect_error), snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: %s -> looper:input (ret=%d)", port, ret); "Failed: %s -> looper:ch0in (ret=%d)", port, ret);
} }
return ret; return ret;
} }
@@ -52,14 +52,14 @@ int handle_client_command(const char *input, int *out_id) {
if (strcmp(token, "to") == 0) { if (strcmp(token, "to") == 0) {
const char *port = strtok(NULL, " "); const char *port = strtok(NULL, " ");
if (!port) return -1; if (!port) return -1;
int ret = carla_connect_direct("looper:output", port); int ret = carla_connect_direct("looper:ch0out", port);
if (ret == 0) { if (ret == 0) {
strncpy(to_port, port, sizeof(to_port)-1); strncpy(to_port, port, sizeof(to_port)-1);
to_port[sizeof(to_port)-1] = '\0'; to_port[sizeof(to_port)-1] = '\0';
g_connect_error[0] = '\0'; g_connect_error[0] = '\0';
} else { } else {
snprintf(g_connect_error, sizeof(g_connect_error), snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: looper:output -> %s (ret=%d)", port, ret); "Failed: looper:ch0out -> %s (ret=%d)", port, ret);
} }
return ret; return ret;
} }

View File

@@ -440,14 +440,23 @@ void tui_run(void) {
int channel = selected_col; // selected column = channel number int channel = selected_col; // selected column = channel number
bool found = carla_resolve_channel_port(channel, is_to, looper_port, sizeof(looper_port)); bool found = carla_resolve_channel_port(channel, is_to, looper_port, sizeof(looper_port));
if (!found) { if (!found) {
/* Fallback to generic name (may not exist) */ /* Fallback to generic name with looper: prefix */
if (is_to) if (is_to)
snprintf(looper_port, sizeof(looper_port), "ch%dout", channel); snprintf(looper_port, sizeof(looper_port), "looper:ch%dout", channel);
else else
snprintf(looper_port, sizeof(looper_port), "ch%din", channel); snprintf(looper_port, sizeof(looper_port), "looper:ch%din", channel);
/* The actual port name includes a PID suffix, but we try anyway */ }
int ret;
const char *src, *dst;
if (is_to) {
ret = carla_connect_direct(looper_port, port_name);
src = looper_port;
dst = port_name;
} else {
ret = carla_connect_direct(port_name, looper_port);
src = port_name;
dst = looper_port;
} }
int ret = carla_connect_direct(port_name, looper_port);
if (ret == 0) { if (ret == 0) {
if (is_to) { if (is_to) {
strncpy(g_to_port, port_name, sizeof(g_to_port)-1); strncpy(g_to_port, port_name, sizeof(g_to_port)-1);
@@ -457,11 +466,11 @@ void tui_run(void) {
g_from_port[sizeof(g_from_port)-1] = '\0'; g_from_port[sizeof(g_from_port)-1] = '\0';
} }
g_connect_error[0] = '\0'; g_connect_error[0] = '\0';
log_msg("Connected %s -> %s", port_name, looper_port); log_msg("Connected %s -> %s", src, dst);
} else { } else {
snprintf(g_connect_error, sizeof(g_connect_error), snprintf(g_connect_error, sizeof(g_connect_error),
"Failed: %s -> %s (ret=%d)", port_name, looper_port, ret); "Failed: %s -> %s (ret=%d)", src, dst, ret);
log_msg("Failed to connect %s -> %s (ret=%d)", port_name, looper_port, ret); log_msg("Failed to connect %s -> %s (ret=%d)", src, dst, ret);
} }
} }
if (!potential_arg) g_selected_port[0] = '\0'; if (!potential_arg) g_selected_port[0] = '\0';

View File

@@ -259,8 +259,9 @@ async function testGridNavigation(): Promise<void> {
// Cycle back to origin // Cycle back to origin
tmuxSendKeys("looper", "0", "h"); tmuxSendKeys("looper", "0", "h");
await wait(400);
tmuxSendKeys("looper", "0", "k"); tmuxSendKeys("looper", "0", "k");
await wait(200); await wait(400);
pane = tmuxCapturePane("looper", "0"); pane = tmuxCapturePane("looper", "0");
if (pane.includes("Selected: Grid 0, Row 0, Col 0")) { if (pane.includes("Selected: Grid 0, Row 0, Col 0")) {
console.log(" PASS: Returned to origin"); console.log(" PASS: Returned to origin");
@@ -472,8 +473,8 @@ async function testTUIRecordAndLoop(): Promise<void> {
} }
console.log(" PASS: TUI grid shows 'R' indicator"); console.log(" PASS: TUI grid shows 'R' indicator");
// Play tone into looper:input (3 seconds) // Play tone into looper:ch0in (3 seconds)
execSync(`${GEN_TONE_BIN} 3.0 "looper:input"`, { timeout: 8000 }); execSync(`${GEN_TONE_BIN} 3.0 "looper:ch0in"`, { timeout: 8000 });
// press 't' again to stop recording -> loop // press 't' again to stop recording -> loop
tmuxSendKeys("looper", "0", "t"); tmuxSendKeys("looper", "0", "t");
@@ -552,7 +553,7 @@ async function testSaveLoad(): Promise<void> {
} }
// Play tone into looper:input using gen_tone (synchronous, blocks until done) // 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) // Stop recording (toggle again -> loop)
writeFifoCommand("record 0"); writeFifoCommand("record 0");
@@ -1048,8 +1049,8 @@ async function testFromToAudioPass(): Promise<void> {
} }
// Now check the connection result look for error lines produced by the fixed pipe.c // Now check the connection result look for error lines produced by the fixed pipe.c
const fromFailed = stderrLog.includes("Failed to connect system:capture_1 -> looper:input"); const fromFailed = stderrLog.includes("Failed to connect system:capture_1 -> looper:ch0in");
const toFailed = stderrLog.includes("Failed to connect looper:output -> system:playback_1"); const toFailed = stderrLog.includes("Failed to connect looper:ch0out -> system:playback_1");
const anyError = stderrLog.includes("Failed to connect") || stderrLog.includes("Retry also failed"); const anyError = stderrLog.includes("Failed to connect") || stderrLog.includes("Retry also failed");
if (fromFailed) { if (fromFailed) {
@@ -1099,7 +1100,7 @@ async function testStatusFifoLevelLine(): Promise<void> {
// Play tone directly (not through TUI) // Play tone directly (not through TUI)
ensureGenTone(); 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 // Wait for engine to write status
await wait(2000); await wait(2000);
@@ -1129,16 +1130,13 @@ async function testVUMeter(): Promise<void> {
// Capture initial VU line (should be empty/spaces) // Capture initial VU line (should be empty/spaces)
let pane = tmuxCapturePane("looper", "0"); let pane = tmuxCapturePane("looper", "0");
const paneLines = pane.split("\n"); const paneLines = pane.split("\n");
const ooIndex = paneLines.findIndex(l => l.trim().startsWith("o:")); // Look for any line containing x or # that is the VU meter line.
let vuLineBefore = ""; const vuLineBefore = paneLines.find(l => /[x#]/.test(l)) || "";
if (ooIndex >= 0 && ooIndex + 1 < paneLines.length) {
vuLineBefore = paneLines[ooIndex + 1];
}
console.log(` Initial VU line: "${vuLineBefore.trim()}"`); console.log(` Initial VU line: "${vuLineBefore.trim()}"`);
// Generate tone in background (does not block the test) // Generate tone in background (does not block the test)
ensureGenTone(); 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 // Wait for audio to start reaching the meter
await wait(1500); await wait(1500);
@@ -1146,11 +1144,8 @@ async function testVUMeter(): Promise<void> {
// Capture pane while tone is playing // Capture pane while tone is playing
pane = tmuxCapturePane("looper", "0"); pane = tmuxCapturePane("looper", "0");
const paneLines2 = pane.split("\n"); const paneLines2 = pane.split("\n");
const ooIndex2 = paneLines2.findIndex(l => l.trim().startsWith("o:")); // Same detection as above
let vuLineDuring = ""; const vuLineDuring = paneLines2.find(l => /[x#]/.test(l)) || "";
if (ooIndex2 >= 0 && ooIndex2 + 1 < paneLines2.length) {
vuLineDuring = paneLines2[ooIndex2 + 1];
}
console.log(` VU line during tone: "${vuLineDuring.trim()}"`); console.log(` VU line during tone: "${vuLineDuring.trim()}"`);
// The VU meter should show non-space characters (at least one 'x' or '#') // The VU meter should show non-space characters (at least one 'x' or '#')
@@ -1175,21 +1170,21 @@ async function main(): Promise<void> {
console.log("=== Looper E2E Tests ===\n"); console.log("=== Looper E2E Tests ===\n");
const tests = [ const tests = [
//testGridNavigation, testGridNavigation,
//testChannelAddRemove, testChannelAddRemove,
//testToggleRecordStop, testToggleRecordStop,
//testTUIRecordAndLoop, testTUIRecordAndLoop,
//testRecordOnSelectedCell, testRecordOnSelectedCell,
testSaveLoad, testSaveLoad,
//testRecordOnMissingChannel, testRecordOnMissingChannel,
//testRapidKeyMashConsistency, testRapidKeyMashConsistency,
//testRecordOnHighRow, testRecordOnHighRow,
testFromToAudioPass, testFromToAudioPass,
//testRecordMoveRecord, testRecordMoveRecord,
//testStressRandomUsage, testStressRandomUsage,
//testKeyPressLatency, testKeyPressLatency,
//testStatusFifoLevelLine, testStatusFifoLevelLine,
//testVUMeter testVUMeter
]; ];
let passCount = 0; let passCount = 0;
let failCount = 0; let failCount = 0;

View File

@@ -16,8 +16,8 @@ void init_scene(scene_t *sc) {
void channel_add(jack_client_t *client, int idx) { void channel_add(jack_client_t *client, int idx) {
char in_name[64], out_name[64]; char in_name[64], out_name[64];
pid_t pid = getpid(); pid_t pid = getpid();
snprintf(in_name, sizeof(in_name), "ch%din_%d", next_channel_id, (int)pid); snprintf(in_name, sizeof(in_name), "ch%din", next_channel_id);
snprintf(out_name, sizeof(out_name), "ch%dout_%d", next_channel_id, (int)pid); snprintf(out_name, sizeof(out_name), "ch%dout", next_channel_id);
/* Always register audio ports (needed for pass-through even for MIDI /* Always register audio ports (needed for pass-through even for MIDI
* channels?) */ * channels?) */
@@ -36,10 +36,9 @@ void channel_add(jack_client_t *client, int idx) {
/* If this is a MIDI channel, register MIDI ports */ /* If this is a MIDI channel, register MIDI ports */
if (channels[idx].type == CHANNEL_MIDI) { if (channels[idx].type == CHANNEL_MIDI) {
char midi_in_name[64], midi_out_name[64]; char midi_in_name[64], midi_out_name[64];
snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin_%d", snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin", next_channel_id);
next_channel_id, (int)pid); snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout",
snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout_%d", next_channel_id);
next_channel_id, (int)pid);
channels[idx].midi_in = jack_port_register( channels[idx].midi_in = jack_port_register(
client, midi_in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); client, midi_in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
channels[idx].midi_out = jack_port_register( channels[idx].midi_out = jack_port_register(

View File

@@ -1,8 +1,8 @@
#include "log.h" #include "log.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <pthread.h> #include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
static FILE *logfile = NULL; static FILE *logfile = NULL;
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -15,7 +15,8 @@ void log_init(void) {
} }
void log_msg(const char *fmt, ...) { void log_msg(const char *fmt, ...) {
if (!logfile) return; if (!logfile)
return;
pthread_mutex_lock(&log_mutex); pthread_mutex_lock(&log_mutex);
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);

View File

@@ -6,8 +6,8 @@
#include "pipe.h" #include "pipe.h"
#include "queue.h" #include "queue.h"
#include "wav.h" #include "wav.h"
#include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <math.h>
#include <jack/jack.h> #include <jack/jack.h>
#include <jack/midiport.h> #include <jack/midiport.h>
#include <math.h> #include <math.h>
@@ -19,7 +19,6 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h>
/* Global command queues (used by midi.c and pipe.c) */ /* Global command queues (used by midi.c and pipe.c) */
spsc_queue_t cmd_queue; spsc_queue_t cmd_queue;
@@ -124,19 +123,24 @@ static void looper_write_status(void) {
default: default:
state_str = "UNKNOWN"; state_str = "UNKNOWN";
} }
/* Always write state line to guarantee level line is sent even if state unchanged */ /* Always write state line to guarantee level line is sent even if state
int n = snprintf(buf + pos, sizeof(buf) - pos, * unchanged */
"CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str); int n = snprintf(buf + pos, sizeof(buf) - pos, "CH=%d SC=%d STATE=%s\n", ch,
if (n > 0) pos += n; sc_idx, state_str);
if (pos >= (int)sizeof(buf) - 128) break; if (n > 0)
pos += n;
if (pos >= (int)sizeof(buf) - 128)
break;
/* Write RMS level line every time (no change detection) */ /* Write RMS level line every time (no change detection) */
{ {
float level = atomic_load(&channels[ch].rms_level); float level = atomic_load(&channels[ch].rms_level);
int n2 = snprintf(buf + pos, sizeof(buf) - pos, int n2 =
"CH=%d LEVEL=%f\n", ch, level); snprintf(buf + pos, sizeof(buf) - pos, "CH=%d LEVEL=%f\n", ch, level);
if (n2 > 0) pos += n2; if (n2 > 0)
if (pos >= (int)sizeof(buf) - 128) break; pos += n2;
if (pos >= (int)sizeof(buf) - 128)
break;
} }
atomic_store(&prev_state[ch][sc_idx], state); 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) // Save the desired scene (may have been set by CMD_SET_SCENE)
int requested_scene = atomic_load(&channels[ch].current_scene); int requested_scene = atomic_load(&channels[ch].current_scene);
// Clamp requested_scene to valid range // Clamp requested_scene to valid range
if (requested_scene < 0) requested_scene = 0; if (requested_scene < 0)
if (requested_scene >= MAX_SCENES) requested_scene = MAX_SCENES - 1; requested_scene = 0;
if (requested_scene >= MAX_SCENES)
requested_scene = MAX_SCENES - 1;
// Auto-create channel if it doesn't exist // Auto-create channel if it doesn't exist
if (!channels[ch].active) { 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); sc_count = atomic_load(&channels[ch].scene_count);
} }
// Clamp requested_scene if MAX_SCENES prevents adding enough scenes // Clamp requested_scene if MAX_SCENES prevents adding enough scenes
if (requested_scene >= sc_count) requested_scene = sc_count - 1; if (requested_scene >= sc_count)
// Restore the requested scene (channel_add or add_scene may have reset current_scene) 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); atomic_store(&channels[ch].current_scene, requested_scene);
int sc_idx = atomic_load(&channels[ch].current_scene); int sc_idx = atomic_load(&channels[ch].current_scene);
scene_t *sc_ptr = &channels[ch].scenes[sc_idx]; scene_t *sc_ptr = &channels[ch].scenes[sc_idx];
int state = atomic_load(&sc_ptr->state); int state = atomic_load(&sc_ptr->state);
@@ -417,7 +424,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
case STATE_RECORD: case STATE_RECORD:
if (c == 0 && atomic_load(&sc->record_pos) == 0) { if (c == 0 && atomic_load(&sc->record_pos) == 0) {
if (in) { if (in) {
fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float*)in)[0]); fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float *)in)[0]);
} else { } else {
fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n"); fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n");
} }
@@ -590,9 +597,9 @@ int looper_init(jack_client_t *client) {
atomic_store(&channels[0].save_complete, 0); atomic_store(&channels[0].save_complete, 0);
channels[0].audio_in = jack_port_register( channels[0].audio_in = jack_port_register(
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); client, "ch0in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
channels[0].audio_out = jack_port_register( channels[0].audio_out = jack_port_register(
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); client, "ch0out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
if (!channels[0].audio_in || !channels[0].audio_out) { if (!channels[0].audio_in || !channels[0].audio_out) {
fprintf(stderr, "Could not create audio ports for channel 0\n"); fprintf(stderr, "Could not create audio ports for channel 0\n");
return -1; return -1;
@@ -719,12 +726,15 @@ void looper_process_commands(jack_client_t *client) {
/* Now safe to copy the loop buffer */ /* Now safe to copy the loop buffer */
float *data = malloc((size_t)frames_to_save * sizeof(float)); float *data = malloc((size_t)frames_to_save * sizeof(float));
if (data) { 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; unsigned sr = (unsigned)global_sample_rate;
if (sr == 0) sr = 48000; if (sr == 0)
sr = 48000;
char save_path[256]; char save_path[256];
snprintf(save_path, sizeof(save_path), "save.wav"); 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); int ret = wav_write(save_path, data, (unsigned)frames_to_save, sr);
printf("SAVE: wav_write returned %d\n", ret); printf("SAVE: wav_write returned %d\n", ret);
free(data); free(data);

View File

@@ -1,6 +1,7 @@
// cppcheck-suppress missingIncludeSystem // cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include "log.h" #include "log.h"
#include "looper.h"
#include "pipe.h"
#include <jack/jack.h> #include <jack/jack.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -47,6 +48,13 @@ int main(int argc, char *argv[]) {
return 1; 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); log_msg("looper running (client name '%s')", client_name);
while (!looper_quit) { while (!looper_quit) {

View File

@@ -12,15 +12,11 @@
#include <unistd.h> #include <unistd.h>
#include <jack/jack.h> #include <jack/jack.h>
extern jack_client_t *global_client;
#define FIFO_PATH "/tmp/looper_cmd" #define FIFO_PATH "/tmp/looper_cmd"
#define LINE_MAX 256 #define LINE_MAX 256
/* Global JACK client (from looper.c) */
extern jack_client_t *global_client;
/* Stored ports for from/to */
static char fifo_from[256] = "";
static char fifo_to[256] = "";
/* Filename for the next load command (default "loop.wav") */ /* Filename for the next load command (default "loop.wav") */
char load_filename[256] = "loop.wav"; char load_filename[256] = "loop.wav";
@@ -91,7 +87,8 @@ static void *pipe_thread_func(void *arg) {
} else if (strncmp(line, "load", 4) == 0) { } else if (strncmp(line, "load", 4) == 0) {
/* Parse optional filename after "load " */ /* Parse optional filename after "load " */
const char *fn = line + 4; const char *fn = line + 4;
while (*fn == ' ') fn++; while (*fn == ' ')
fn++;
if (*fn == '\0') { if (*fn == '\0') {
strncpy(load_filename, "loop.wav", sizeof(load_filename) - 1); strncpy(load_filename, "loop.wav", sizeof(load_filename) - 1);
} else { } else {
@@ -104,74 +101,21 @@ static void *pipe_thread_func(void *arg) {
} else if (strcmp(line, "save") == 0) { } else if (strcmp(line, "save") == 0) {
command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0}; command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0};
queue_push(&cmd_queue_main_fifo, cmd); queue_push(&cmd_queue_main_fifo, cmd);
} } else if (strncmp(line, "from ", 5) == 0) {
// --- from <port> --- const char *port = line + 5;
else if (strncmp(line, "from ", 5) == 0) { fprintf(stderr, "FIFO RECEIVED from: %s\n", port);
fprintf(stderr, "FIFO RECEIVED from: %s\n", line + 5);
strncpy(fifo_from, line + 5, sizeof(fifo_from)-1);
fifo_from[sizeof(fifo_from)-1] = '\0';
// Immediately connect source to looper:input (independently of :to)
if (global_client) { if (global_client) {
const char *target = "looper:input"; int ret = jack_connect(global_client, port, "looper:ch0in");
int ret = jack_connect(global_client, fifo_from, target);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", fifo_from, target, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, fifo_from, target);
if (ret != 0) if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", fifo_from, target, ret); 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;
// --- to <port> --- fprintf(stderr, "FIFO RECEIVED to: %s\n", port);
else if (strncmp(line, "to ", 3) == 0) {
fprintf(stderr, "FIFO RECEIVED to: %s\n", line + 3);
strncpy(fifo_to, line + 3, sizeof(fifo_to)-1);
fifo_to[sizeof(fifo_to)-1] = '\0';
// Immediately connect looper:output to target (independently of :from)
if (global_client) { if (global_client) {
const char *source = "looper:output"; int ret = jack_connect(global_client, "looper:ch0out", port);
int ret = jack_connect(global_client, source, fifo_to);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", source, fifo_to, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, source, fifo_to);
if (ret != 0) if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", source, fifo_to, ret); fprintf(stderr, "Failed to connect looper:ch0out -> %s (ret=%d)\n", port, ret);
}
}
}
// --- connect [from] [to] ---
else if (strncmp(line, "connect", 7) == 0) {
char from[256] = "";
char to[256] = "";
// parse optional arguments: "connect from to"
char *p = line + 7;
while (*p == ' ') p++;
if (*p) {
char *space = strchr(p, ' ');
if (space) {
strncpy(from, p, space - p); from[space-p] = '\0';
strncpy(to, space+1, sizeof(to)-1);
} else {
strncpy(from, p, sizeof(from)-1);
}
}
// fallback to stored ports
if (!from[0]) strncpy(from, fifo_from, sizeof(from)-1);
if (!to[0]) strncpy(to, fifo_to, sizeof(to)-1);
if (from[0] && to[0] && global_client) {
int ret = jack_connect(global_client, from, to);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", from, to, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, from, to);
if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", from, to, ret);
}
} }
} }
/* ignore unknown lines */ /* ignore unknown lines */

View File

@@ -8,15 +8,18 @@ static inline size_t load_tail(const RingBuf *r) {
return atomic_load_explicit(&r->tail, memory_order_relaxed); return atomic_load_explicit(&r->tail, memory_order_relaxed);
} }
static inline void store_head(RingBuf *r, size_t v) { 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) { 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) { int ring_init(RingBuf *r, size_t capacity) {
r->buf = (float *)malloc(capacity * sizeof(float)); r->buf = (float *)malloc(capacity * sizeof(float));
if (!r->buf) return -1; if (!r->buf)
return -1;
r->capacity = capacity; r->capacity = capacity;
atomic_init(&r->head, 0); atomic_init(&r->head, 0);
atomic_init(&r->tail, 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 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 tail =
load_tail(r); // producer reads consumer's tail (relaxed is fine)
size_t head = load_head(r); // own head size_t head = load_head(r); // own head
size_t cap = r->capacity; size_t cap = r->capacity;
size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head)); size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head));
size_t avail = cap - 1 - used; size_t avail = cap - 1 - used;
if (count > avail) count = avail; if (count > avail)
if (count == 0) return 0; count = avail;
if (count == 0)
return 0;
size_t pos = head; size_t pos = head;
for (size_t i = 0; i < count; ++i) { 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 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 head = atomic_load_explicit(
&r->head, memory_order_acquire); // acquire see producer's writes
size_t tail = load_tail(r); // own tail size_t tail = load_tail(r); // own tail
size_t cap = r->capacity; size_t cap = r->capacity;
size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head)); size_t used = (head >= tail) ? (head - tail) : (cap - (tail - head));
if (count > used) count = used; if (count > used)
if (count == 0) return 0; count = used;
if (count == 0)
return 0;
size_t pos = tail; size_t pos = tail;
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {