Compare commits
1 Commits
master
...
integrate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fb5d3524 |
53
e2e/test.ts
53
e2e/test.ts
@@ -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");
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ 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",
|
snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin", next_channel_id);
|
||||||
next_channel_id);
|
|
||||||
snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout",
|
snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout",
|
||||||
next_channel_id);
|
next_channel_id);
|
||||||
channels[idx].midi_in = jack_port_register(
|
channels[idx].midi_in = jack_port_register(
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
#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;
|
||||||
|
|
||||||
void log_init(void) {
|
void log_init(void) {
|
||||||
logfile = fopen("./looper.log", "a");
|
logfile = fopen("./looper.log", "a");
|
||||||
if (!logfile)
|
if (!logfile)
|
||||||
logfile = stderr;
|
logfile = stderr;
|
||||||
setbuf(logfile, NULL);
|
setbuf(logfile, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_msg(const char *fmt, ...) {
|
void log_msg(const char *fmt, ...) {
|
||||||
if (!logfile) return;
|
if (!logfile)
|
||||||
pthread_mutex_lock(&log_mutex);
|
return;
|
||||||
va_list args;
|
pthread_mutex_lock(&log_mutex);
|
||||||
va_start(args, fmt);
|
va_list args;
|
||||||
vfprintf(logfile, fmt, args);
|
va_start(args, fmt);
|
||||||
va_end(args);
|
vfprintf(logfile, fmt, args);
|
||||||
fputc('\n', logfile);
|
va_end(args);
|
||||||
pthread_mutex_unlock(&log_mutex);
|
fputc('\n', logfile);
|
||||||
|
pthread_mutex_unlock(&log_mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_close(void) {
|
void log_close(void) {
|
||||||
if (logfile && logfile != stderr)
|
if (logfile && logfile != stderr)
|
||||||
fclose(logfile);
|
fclose(logfile);
|
||||||
logfile = NULL;
|
logfile = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -52,47 +51,47 @@ static atomic_int prev_state[MAX_CHANNELS][MAX_SCENES];
|
|||||||
|
|
||||||
/* Unregister all ports and close the JACK client */
|
/* Unregister all ports and close the JACK client */
|
||||||
static void looper_cleanup(jack_client_t *client) {
|
static void looper_cleanup(jack_client_t *client) {
|
||||||
for (int c = 0; c < MAX_CHANNELS; c++) {
|
for (int c = 0; c < MAX_CHANNELS; c++) {
|
||||||
if (channels[c].audio_in) {
|
if (channels[c].audio_in) {
|
||||||
jack_port_unregister(client, channels[c].audio_in);
|
jack_port_unregister(client, channels[c].audio_in);
|
||||||
channels[c].audio_in = NULL;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (midi_control_port) {
|
if (channels[c].audio_out) {
|
||||||
jack_port_unregister(client, midi_control_port);
|
jack_port_unregister(client, channels[c].audio_out);
|
||||||
midi_control_port = NULL;
|
channels[c].audio_out = NULL;
|
||||||
}
|
}
|
||||||
if (midi_clock_port) {
|
if (channels[c].midi_in) {
|
||||||
jack_port_unregister(client, midi_clock_port);
|
jack_port_unregister(client, channels[c].midi_in);
|
||||||
midi_clock_port = NULL;
|
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) {
|
void looper_shutdown(jack_client_t *client) {
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
looper_cleanup(client);
|
looper_cleanup(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
log_close();
|
log_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
volatile int looper_quit = 0;
|
volatile int looper_quit = 0;
|
||||||
|
|
||||||
/* Signal handler: set quit flag only */
|
/* Signal handler: set quit flag only */
|
||||||
static void signal_handler(int sig) {
|
static void signal_handler(int sig) {
|
||||||
(void)sig;
|
(void)sig;
|
||||||
looper_quit = 1;
|
looper_quit = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void looper_write_status(void) {
|
static void looper_write_status(void) {
|
||||||
@@ -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);
|
||||||
@@ -410,17 +417,17 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (c == 0 && !atomic_load(&channels[c].active)) {
|
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) {
|
switch (state) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (in) {
|
if (in) {
|
||||||
float *f_out = (float *)out;
|
float *f_out = (float *)out;
|
||||||
@@ -467,17 +474,17 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
|
|
||||||
/* Compute RMS level for this channel */
|
/* Compute RMS level for this channel */
|
||||||
{
|
{
|
||||||
float sum_sq = 0.0f;
|
float sum_sq = 0.0f;
|
||||||
const float *f_out = (const float *)out;
|
const float *f_out = (const float *)out;
|
||||||
for (jack_nframes_t i = 0; i < nframes; i++)
|
for (jack_nframes_t i = 0; i < nframes; i++)
|
||||||
sum_sq += f_out[i] * f_out[i];
|
sum_sq += f_out[i] * f_out[i];
|
||||||
float rms = sqrtf(sum_sq / nframes);
|
float rms = sqrtf(sum_sq / nframes);
|
||||||
atomic_store(&channels[c].rms_level, rms);
|
atomic_store(&channels[c].rms_level, rms);
|
||||||
static float last_rms[MAX_CHANNELS] = {0};
|
static float last_rms[MAX_CHANNELS] = {0};
|
||||||
if (rms > 0.001f && fabsf(rms - last_rms[c]) > 0.005f) {
|
if (rms > 0.001f && fabsf(rms - last_rms[c]) > 0.005f) {
|
||||||
fprintf(stderr, "RMS ch%d = %f\n", c, rms);
|
fprintf(stderr, "RMS ch%d = %f\n", c, rms);
|
||||||
last_rms[c] = rms;
|
last_rms[c] = rms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* push loop output into save ring if saving (atomic load) */
|
/* 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 */
|
/* Allow save from any state where we have data */
|
||||||
int frames_to_save = 0;
|
int frames_to_save = 0;
|
||||||
if ((state == STATE_LOOPING || state == STATE_PAUSED) && lc > 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) {
|
} else if (state == STATE_RECORD && rp > 0) {
|
||||||
frames_to_save = rp;
|
frames_to_save = rp;
|
||||||
}
|
}
|
||||||
if (frames_to_save > 0) {
|
if (frames_to_save > 0) {
|
||||||
/* Deactivate channel to prevent RT thread from reading the buffer */
|
/* 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 */
|
/* 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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.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
|
||||||
@@ -84,11 +87,12 @@ 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 {
|
||||||
strncpy(load_filename, fn, sizeof(load_filename) - 1);
|
strncpy(load_filename, fn, sizeof(load_filename) - 1);
|
||||||
}
|
}
|
||||||
load_filename[sizeof(load_filename) - 1] = '\0';
|
load_filename[sizeof(load_filename) - 1] = '\0';
|
||||||
fprintf(stderr, "FIFO RECEIVED load: %s\n", load_filename);
|
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) {
|
} 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) {
|
||||||
|
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 */
|
/* ignore unknown lines */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
size_t head = load_head(r); // own head
|
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 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(
|
||||||
size_t tail = load_tail(r); // own tail
|
&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 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user