feat: implement bind feature for associating channels with MIDI notes

Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-09 10:22:33 +00:00
parent 740ebaa969
commit 4bacab68c6
3 changed files with 151 additions and 23 deletions

View File

@@ -18,6 +18,7 @@ atomic_int cmd_remove = 0;
jack_port_t *midi_control_port = NULL; jack_port_t *midi_control_port = NULL;
jack_port_t *midi_clock_port = NULL; jack_port_t *midi_clock_port = NULL;
atomic_int control_key_active = 0; atomic_int control_key_active = 0;
atomic_int bind_channel = 0;
/* Deferred removal index (1 second grace) */ /* Deferred removal index (1 second grace) */
static int pending_unregister_idx = -1; static int pending_unregister_idx = -1;

View File

@@ -7,6 +7,7 @@
extern atomic_int control_key_active; extern atomic_int control_key_active;
extern atomic_int cmd_add; extern atomic_int cmd_add;
extern atomic_int cmd_remove; extern atomic_int cmd_remove;
extern atomic_int bind_channel;
void midi_handle_events(void *port_buffer, jack_nframes_t nframes) void midi_handle_events(void *port_buffer, jack_nframes_t nframes)
{ {
@@ -30,30 +31,37 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes)
int ck = atomic_load(&control_key_active); int ck = atomic_load(&control_key_active);
if (ck) { if (ck) {
atomic_store(&control_key_active, 0); atomic_store(&control_key_active, 0);
switch (note) { if (note < 16) {
case 60: atomic_store(&cmd_add, 1); break; atomic_store(&bind_channel, note);
case 61: atomic_store(&cmd_remove, 1); break; } else {
case 62: /* trigger looper channel 0 */ switch (note) {
{ case 60: atomic_store(&cmd_add, 1); break;
int cur0 = atomic_load(&channels[0].state); case 61: atomic_store(&cmd_remove, 1); break;
switch (cur0) { case 62: /* trigger looper channel via bind_channel */
case STATE_IDLE: {
atomic_store(&channels[0].state, STATE_RECORD); int bch = atomic_load(&bind_channel);
break; if (bch >= 0 && bch < MAX_CHANNELS) {
case STATE_RECORD: int cur = atomic_load(&channels[bch].state);
atomic_store(&channels[0].state, STATE_LOOPING); switch (cur) {
break; case STATE_IDLE:
case STATE_LOOPING: atomic_store(&channels[bch].state, STATE_RECORD);
atomic_store(&channels[0].state, STATE_PAUSED); break;
break; case STATE_RECORD:
case STATE_PAUSED: atomic_store(&channels[bch].state, STATE_LOOPING);
atomic_store(&channels[0].state, STATE_LOOPING); break;
break; case STATE_LOOPING:
atomic_store(&channels[bch].state, STATE_PAUSED);
break;
case STATE_PAUSED:
atomic_store(&channels[bch].state, STATE_LOOPING);
break;
}
}
} }
break;
default:
break;
} }
break;
default:
break;
} }
} else { } else {
/* direct mapping */ /* direct mapping */

View File

@@ -515,6 +515,119 @@ static int test_control_key_modifier(void) {
return 0; return 0;
} }
/* test bind channel */
static int test_bind_channel(void) {
printf("Test: controlkey bind channel (note 0) and toggle\n");
pid_t pid = start_looper();
if (pid < 0) return 1;
jack_client_t *client;
jack_status_t status;
client = jack_client_open("test_bind", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
jack_port_t *audio_out = jack_port_register(client, "out",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
jack_port_t *audio_in = jack_port_register(client, "in",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (!audio_out || !audio_in) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
usleep(200000);
char my_out[64], my_in[64];
snprintf(my_out, sizeof(my_out), "test_bind:out");
snprintf(my_in, sizeof(my_in), "test_bind:in");
if (jack_connect(client, my_out, "looper:input") ||
jack_connect(client, "looper:output", my_in)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
/* Send control key + note 0 to bind to channel 0 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send control key failed\n");
return 1;
}
usleep(200000);
if (send_jack_note_on("looper:control", 0, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send bind note 0 failed\n");
return 1;
}
usleep(200000);
/* Now toggle using control+note62 should toggle channel 0 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send control key again failed\n");
return 1;
}
usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send toggle note 62 failed\n");
return 1;
}
/* Wait and detect bursts as before */
int sr = jack_get_sample_rate(client);
continuous_sine = 0;
beep_remaining = (int)(0.1f * sr);
bursts = 0;
prev_above = 0;
passthrough_output_port = audio_out;
passthrough_input_port = audio_in;
passthrough_phase = 0.0f;
passthrough_freq = 440.0f;
passthrough_sample_rate = sr;
passthrough_total_samples = 0;
passthrough_sum_sq = 0.0;
passthrough_done = 0;
jack_set_process_callback(client, passthrough_process, NULL);
if (jack_activate(client)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
usleep(200000); /* allow beep */
/* send control+note62 again to move RECORD->LOOPING */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key for loop\n");
return 1;
}
usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: toggle for loop\n");
return 1;
}
usleep(2000000);
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
int got_bursts = bursts;
printf(" detected bursts: %d\n", got_bursts);
if (got_bursts < 3) {
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
return 1;
}
printf(" PASS (bind and toggle)\n");
return 0;
}
/* test remove channel */ /* test remove channel */
static int test_remove_channel(void) { static int test_remove_channel(void) {
printf("Test: dynamic channel removal via MIDI command\n"); printf("Test: dynamic channel removal via MIDI command\n");
@@ -619,7 +732,13 @@ int main(void) {
failures++; failures++;
} }
/* 7. Test channel removal */ /* 7. Test bind channel */
if (test_bind_channel() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
/* 8. Test channel removal */
if (test_remove_channel() != 0) { if (test_remove_channel() != 0) {
fprintf(stderr, " FAILED\n"); fprintf(stderr, " FAILED\n");
failures++; failures++;