2026-10-27 00:53:41 +01:00
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
|
#include "xtc/xtc_backend.h"
|
2026-10-27 01:06:57 +01:00
|
|
|
#include "xtc/xtc_mixer.h"
|
2026-10-27 00:53:41 +01:00
|
|
|
#include <jack/jack.h>
|
|
|
|
|
#include <jack/midiport.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <math.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
typedef struct {
|
2026-10-27 01:06:57 +01:00
|
|
|
jack_port_t *input, *right, *midi;
|
|
|
|
|
unsigned int command, seen, loud, right_loud, heard, bad;
|
2026-10-27 00:53:41 +01:00
|
|
|
} probe;
|
|
|
|
|
static int process(jack_nframes_t frames, void *context)
|
|
|
|
|
{
|
|
|
|
|
probe *p = context;
|
|
|
|
|
const float *input = jack_port_get_buffer(p->input, frames);
|
2026-10-27 01:06:57 +01:00
|
|
|
const float *right = jack_port_get_buffer(p->right, frames);
|
2026-10-27 00:53:41 +01:00
|
|
|
void *midi = jack_port_get_buffer(p->midi, frames);
|
2026-10-27 01:06:57 +01:00
|
|
|
unsigned int right_loud = 0;
|
2026-10-27 00:53:41 +01:00
|
|
|
unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0;
|
|
|
|
|
jack_nframes_t i;
|
2026-10-27 01:06:57 +01:00
|
|
|
if (!input || !right || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
|
2026-10-27 00:53:41 +01:00
|
|
|
jack_midi_clear_buffer(midi);
|
|
|
|
|
if (command && command != p->seen) {
|
|
|
|
|
unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0};
|
|
|
|
|
if (jack_midi_event_write(midi, frames > 8 ? 8 : 0, bytes, 3) != 0)
|
|
|
|
|
__atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
p->seen = command;
|
|
|
|
|
}
|
|
|
|
|
for (i = 0; i < frames; ++i) {
|
|
|
|
|
if (!isfinite(input[i]) || fabsf(input[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
if (fabsf(input[i]) > 0.01f) loud = 1;
|
2026-10-27 01:06:57 +01:00
|
|
|
if (!isfinite(right[i]) || fabsf(right[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
if (fabsf(right[i]) > 0.01f) right_loud = 1;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
__atomic_store_n(&p->loud, loud, __ATOMIC_RELAXED);
|
2026-10-27 01:06:57 +01:00
|
|
|
__atomic_store_n(&p->right_loud, right_loud, __ATOMIC_RELAXED);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (loud) __atomic_store_n(&p->heard, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
static void wait_blocks(void)
|
|
|
|
|
{ struct timespec pause = {0, 200000000L}; nanosleep(&pause, NULL); }
|
|
|
|
|
#define REQUIRE(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); goto cleanup; } } while (0)
|
|
|
|
|
int main(void)
|
|
|
|
|
{
|
|
|
|
|
char directory[] = "/tmp/xtc-live-midi-XXXXXX", song[256] = "", name[64];
|
2026-10-27 01:06:57 +01:00
|
|
|
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 2};
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_backend *backend = NULL;
|
|
|
|
|
xtc_project *p = NULL, *q = NULL;
|
|
|
|
|
jack_client_t *client = NULL;
|
|
|
|
|
jack_status_t flags = 0;
|
|
|
|
|
probe state = {0};
|
|
|
|
|
const char *destination;
|
|
|
|
|
uint64_t track, clip, note;
|
|
|
|
|
int result = EXIT_FAILURE;
|
|
|
|
|
REQUIRE(mkdtemp(directory));
|
|
|
|
|
snprintf(song, sizeof(song), "%s/song.xtc", directory);
|
|
|
|
|
snprintf(name, sizeof(name), "xtc-midi-%ld", (long)getpid());
|
|
|
|
|
REQUIRE(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_project_add_track(p, "Keys", &track) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_open(&config, &backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_prepare_project(backend, p) == XTC_OK);
|
|
|
|
|
client = jack_client_open("xtc-midi-probe", JackNoStartServer, &flags); REQUIRE(client);
|
|
|
|
|
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
2026-10-27 01:06:57 +01:00
|
|
|
state.right = jack_port_register(client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
2026-10-27 00:53:41 +01:00
|
|
|
state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
|
2026-10-27 01:06:57 +01:00
|
|
|
REQUIRE(state.input && state.right && state.midi);
|
2026-10-27 00:53:41 +01:00
|
|
|
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
|
|
|
|
|
REQUIRE(xtc_backend_start(backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
|
|
|
|
|
wait_blocks(); /* PipeWire JACK publishes newly activated ports asynchronously. */
|
|
|
|
|
{
|
|
|
|
|
int connected = jack_connect(client, jack_port_name(state.midi), destination);
|
|
|
|
|
if (connected && connected != EEXIST) fprintf(stderr, "MIDI connection returned %d: %s -> %s\n", connected, jack_port_name(state.midi), destination);
|
|
|
|
|
REQUIRE(connected == 0 || connected == EEXIST);
|
|
|
|
|
}
|
|
|
|
|
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
2026-10-27 01:06:57 +01:00
|
|
|
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
|
|
|
|
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
|
|
|
|
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
|
|
|
|
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
2026-10-27 00:53:41 +01:00
|
|
|
__atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks();
|
|
|
|
|
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
__atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks();
|
|
|
|
|
REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
__atomic_store_n(&state.command, 3U, __ATOMIC_RELEASE); wait_blocks();
|
|
|
|
|
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
REQUIRE(xtc_backend_panic(backend) == XTC_OK); wait_blocks();
|
|
|
|
|
REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
__atomic_store_n(&state.command, 5U, __ATOMIC_RELEASE); wait_blocks();
|
|
|
|
|
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
REQUIRE(jack_disconnect(client, jack_port_name(state.midi), destination) == 0);
|
|
|
|
|
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_project_add_note(p, clip, 0, 960, 69, 100, 0, ¬e) == XTC_OK);
|
2026-10-27 01:06:57 +01:00
|
|
|
REQUIRE(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
|
2026-10-27 00:53:41 +01:00
|
|
|
REQUIRE(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_prepare_project(backend, q) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
2026-10-27 01:06:57 +01:00
|
|
|
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
|
|
|
|
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
|
|
|
|
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED) && __atomic_load_n(&state.right_loud, __ATOMIC_RELAXED));
|
|
|
|
|
{ xtc_backend_status status; REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0); }
|
2026-10-27 00:53:41 +01:00
|
|
|
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_seek(backend, 7680000) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
|
|
|
|
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
2026-10-27 01:06:57 +01:00
|
|
|
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
|
|
|
|
|
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
|
2026-10-27 00:53:41 +01:00
|
|
|
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
|
|
|
|
|
REQUIRE(__atomic_load_n(&state.heard, __ATOMIC_RELAXED) && !__atomic_load_n(&state.bad, __ATOMIC_RELAXED));
|
2026-10-27 01:06:57 +01:00
|
|
|
puts("Live JACK MIDI/panic/disconnect and saved stereo gain/pan playback/meters passed.");
|
2026-10-27 00:53:41 +01:00
|
|
|
result = EXIT_SUCCESS;
|
|
|
|
|
cleanup:
|
|
|
|
|
if (backend && xtc_backend_close(backend) != XTC_OK) return EXIT_FAILURE;
|
|
|
|
|
if (client && jack_client_close(client) != 0) return EXIT_FAILURE;
|
|
|
|
|
xtc_project_destroy(p); xtc_project_destroy(q);
|
|
|
|
|
if (song[0]) unlink(song);
|
|
|
|
|
rmdir(directory);
|
|
|
|
|
return result;
|
|
|
|
|
}
|