2026-10-26 23:32:04 +01:00
|
|
|
#include "xtc/xtc_backend.h"
|
|
|
|
|
#include "xtc/xtc_realtime.h"
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#ifdef XTC_HAVE_JACK
|
|
|
|
|
#include <jack/jack.h>
|
2026-10-27 00:53:41 +01:00
|
|
|
#include <jack/midiport.h>
|
2026-10-26 23:32:04 +01:00
|
|
|
#include <errno.h>
|
|
|
|
|
|
|
|
|
|
struct xtc_backend {
|
|
|
|
|
jack_client_t *client;
|
2026-10-27 00:53:41 +01:00
|
|
|
jack_port_t *input, *output, *midi_input;
|
2026-10-26 23:32:04 +01:00
|
|
|
xtc_realtime *engine;
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_audio_plan *audio;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan *midi;
|
|
|
|
|
unsigned int panic_requested, panic_seen, midi_dropped;
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
|
2026-10-27 00:53:41 +01:00
|
|
|
float *timeline, *instrument;
|
2026-10-26 23:32:04 +01:00
|
|
|
size_t max_frames;
|
2026-10-27 00:10:45 +01:00
|
|
|
uint64_t sample_position;
|
2026-10-26 23:32:04 +01:00
|
|
|
unsigned int expected_rate;
|
|
|
|
|
unsigned int rate, frames, lost, rate_changed, xruns;
|
|
|
|
|
unsigned int process_result;
|
|
|
|
|
int ready, running; /* Control thread only. */
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-27 00:53:41 +01:00
|
|
|
static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nframes_t frames)
|
|
|
|
|
{
|
|
|
|
|
xtc_midi_event events[XTC_MIDI_MAX_EVENTS];
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
uint32_t i, total;
|
|
|
|
|
unsigned int requested = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
|
|
|
|
|
void *buffer;
|
|
|
|
|
if (!backend->midi) return XTC_OK;
|
|
|
|
|
if (requested != backend->panic_seen) {
|
|
|
|
|
xtc_midi_plan_panic(backend->midi, position); backend->panic_seen = requested;
|
|
|
|
|
}
|
|
|
|
|
buffer = jack_port_get_buffer(backend->midi_input, frames);
|
|
|
|
|
total = buffer ? jack_midi_get_event_count(buffer) : 0;
|
|
|
|
|
if (!buffer || total > XTC_MIDI_MAX_EVENTS) {
|
|
|
|
|
(void)__atomic_fetch_add(&backend->midi_dropped, total ? total : 1U, __ATOMIC_RELAXED);
|
|
|
|
|
xtc_midi_plan_panic(backend->midi, position);
|
|
|
|
|
} else for (i = 0; i < total; ++i) {
|
|
|
|
|
jack_midi_event_t raw;
|
|
|
|
|
xtc_result result;
|
|
|
|
|
if (jack_midi_event_get(&raw, buffer, i) != 0 || raw.time >= frames) {
|
|
|
|
|
(void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
xtc_midi_plan_panic(backend->midi, position); count = 0; break;
|
|
|
|
|
}
|
|
|
|
|
result = xtc_midi_decode(raw.buffer, raw.size, (unsigned int)raw.time, events + count);
|
|
|
|
|
if (result == XTC_ERR_UNSUPPORTED) continue;
|
|
|
|
|
if (result != XTC_OK || (count && events[count].offset < events[count - 1].offset)) {
|
|
|
|
|
(void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED);
|
|
|
|
|
xtc_midi_plan_panic(backend->midi, position); count = 0; break;
|
|
|
|
|
}
|
|
|
|
|
++count;
|
|
|
|
|
}
|
|
|
|
|
return xtc_midi_plan_render(backend->midi, position, events, count, backend->instrument, (size_t)frames);
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
static int process(jack_nframes_t frames, void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
float *output = jack_port_get_buffer(backend->output, frames);
|
|
|
|
|
xtc_result result = XTC_OK;
|
|
|
|
|
if (output == NULL) {
|
2026-10-27 00:53:41 +01:00
|
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
2026-10-27 00:31:03 +01:00
|
|
|
if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U);
|
2026-10-26 23:32:04 +01:00
|
|
|
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
|
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) {
|
|
|
|
|
result = XTC_ERR_INVALID_STATE;
|
|
|
|
|
} else if ((size_t)frames > backend->max_frames) {
|
|
|
|
|
result = XTC_ERR_CAPACITY;
|
|
|
|
|
} else {
|
|
|
|
|
const float *input = jack_port_get_buffer(backend->input, frames);
|
2026-10-27 00:31:03 +01:00
|
|
|
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
|
|
|
|
if (backend->recorder) xtc_recorder_push(backend->recorder, input, (size_t)frames);
|
|
|
|
|
if (backend->audio)
|
|
|
|
|
result = xtc_audio_plan_render(backend->audio, position, backend->timeline, (size_t)frames);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (result == XTC_OK) result = render_midi(backend, position, frames);
|
2026-10-27 00:31:03 +01:00
|
|
|
if (result == XTC_OK)
|
2026-10-27 00:53:41 +01:00
|
|
|
result = xtc_realtime_process_sources(backend->engine, input,
|
|
|
|
|
backend->audio ? backend->timeline : NULL,
|
|
|
|
|
backend->midi ? backend->instrument : NULL, output, (size_t)frames);
|
2026-10-26 23:32:04 +01:00
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
if (result == XTC_OK) {
|
|
|
|
|
uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
|
|
|
|
if (position > UINT64_MAX - frames) result = XTC_ERR_CAPACITY;
|
|
|
|
|
else __atomic_store_n(&backend->sample_position, position + frames, __ATOMIC_RELAXED);
|
|
|
|
|
}
|
2026-10-27 00:31:03 +01:00
|
|
|
if (result != XTC_OK) {
|
2026-10-27 00:53:41 +01:00
|
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
2026-10-27 00:31:03 +01:00
|
|
|
if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U);
|
|
|
|
|
memset(output, 0, (size_t)frames * sizeof(*output));
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
__atomic_store_n(&backend->process_result, (unsigned int)result, __ATOMIC_RELAXED);
|
|
|
|
|
return 0; /* Keep connected and silent on recoverable processing errors. */
|
|
|
|
|
}
|
|
|
|
|
static void shutdown_callback(void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
__atomic_store_n(&backend->lost, 1U, __ATOMIC_RELEASE);
|
|
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
static void port_connection(jack_port_id_t source, jack_port_id_t destination, int connected, void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
if (!connected && (jack_port_by_id(backend->client, source) == backend->midi_input ||
|
|
|
|
|
jack_port_by_id(backend->client, destination) == backend->midi_input))
|
|
|
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
static int rate_callback(jack_nframes_t rate, void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
__atomic_store_n(&backend->rate, (unsigned int)rate, __ATOMIC_RELAXED);
|
|
|
|
|
if ((unsigned int)rate != backend->expected_rate)
|
|
|
|
|
__atomic_store_n(&backend->rate_changed, 1U, __ATOMIC_RELEASE);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
static int buffer_callback(jack_nframes_t frames, void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
__atomic_store_n(&backend->frames, (unsigned int)frames, __ATOMIC_RELAXED);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
static int xrun_callback(void *context)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend = context;
|
|
|
|
|
(void)__atomic_fetch_add(&backend->xruns, 1U, __ATOMIC_RELAXED);
|
2026-10-27 00:53:41 +01:00
|
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
2026-10-26 23:32:04 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend)
|
|
|
|
|
{
|
|
|
|
|
xtc_backend *backend;
|
|
|
|
|
xtc_result result;
|
|
|
|
|
jack_status_t status = 0;
|
|
|
|
|
if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out_backend = NULL;
|
|
|
|
|
if (config == NULL || config->client_name == NULL || config->client_name[0] == '\0' ||
|
|
|
|
|
config->max_frames == 0U || config->max_frames > 65536U || config->channel_count != 1U)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-27 00:10:45 +01:00
|
|
|
if (!__atomic_always_lock_free(sizeof(uint64_t), 0)) return XTC_ERR_UNSUPPORTED;
|
2026-10-26 23:32:04 +01:00
|
|
|
if (config->kind != XTC_BACKEND_JACK) return XTC_ERR_UNSUPPORTED;
|
|
|
|
|
if (strlen(config->client_name) >= (size_t)jack_client_name_size()) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
backend = calloc(1U, sizeof(*backend));
|
|
|
|
|
if (backend == NULL) return XTC_ERR_OUT_OF_MEMORY;
|
2026-10-27 00:31:03 +01:00
|
|
|
backend->timeline = calloc(config->max_frames, sizeof(float));
|
2026-10-27 00:53:41 +01:00
|
|
|
backend->instrument = calloc(config->max_frames, sizeof(float));
|
|
|
|
|
if (!backend->timeline || !backend->instrument) {
|
|
|
|
|
free(backend->timeline); free(backend->instrument); free(backend); return XTC_ERR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
backend->max_frames = config->max_frames;
|
|
|
|
|
backend->client = jack_client_open(config->client_name, JackNoStartServer, &status);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (backend->client == NULL) { free(backend->instrument); free(backend->timeline); free(backend); return XTC_ERR_IO; }
|
2026-10-26 23:32:04 +01:00
|
|
|
backend->expected_rate = (unsigned int)jack_get_sample_rate(backend->client);
|
|
|
|
|
backend->rate = backend->expected_rate;
|
|
|
|
|
backend->frames = (unsigned int)jack_get_buffer_size(backend->client);
|
|
|
|
|
result = backend->frames == 0U || backend->frames > config->max_frames ? XTC_ERR_CAPACITY :
|
|
|
|
|
xtc_realtime_create((double)backend->rate, config->max_frames, &backend->engine);
|
|
|
|
|
if (result != XTC_OK) goto fail;
|
|
|
|
|
backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U);
|
|
|
|
|
backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
|
2026-10-27 00:53:41 +01:00
|
|
|
backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U);
|
|
|
|
|
if (backend->input == NULL || backend->output == NULL || backend->midi_input == NULL) { result = XTC_ERR_IO; goto fail; }
|
|
|
|
|
if (jack_set_port_connect_callback(backend->client, port_connection, backend) != 0 ||
|
|
|
|
|
jack_set_process_callback(backend->client, process, backend) != 0 ||
|
2026-10-26 23:32:04 +01:00
|
|
|
jack_set_sample_rate_callback(backend->client, rate_callback, backend) != 0 ||
|
|
|
|
|
jack_set_buffer_size_callback(backend->client, buffer_callback, backend) != 0 ||
|
|
|
|
|
jack_set_xrun_callback(backend->client, xrun_callback, backend) != 0) {
|
|
|
|
|
result = XTC_ERR_IO;
|
|
|
|
|
goto fail;
|
|
|
|
|
}
|
|
|
|
|
jack_on_shutdown(backend->client, shutdown_callback, backend);
|
|
|
|
|
backend->ready = 1;
|
|
|
|
|
*out_backend = backend;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
fail:
|
|
|
|
|
/* Client never activated: no processing callback can reference the engine.
|
|
|
|
|
* close synchronizes any registered notification callbacks. */
|
|
|
|
|
if (jack_client_close(backend->client) != 0) {
|
|
|
|
|
*out_backend = backend; /* Preserve callback context for close retry. */
|
|
|
|
|
return XTC_ERR_IO;
|
|
|
|
|
}
|
|
|
|
|
xtc_realtime_destroy(backend->engine);
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_audio_plan_destroy(backend->audio);
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_destroy(backend->midi);
|
|
|
|
|
free(backend->instrument);
|
2026-10-27 00:31:03 +01:00
|
|
|
free(backend->timeline);
|
2026-10-26 23:32:04 +01:00
|
|
|
free(backend);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result xtc_backend_close(xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL) return XTC_OK;
|
|
|
|
|
/* JACK removes this client and joins callbacks before returning success. */
|
|
|
|
|
if (jack_client_close(backend->client) != 0) return XTC_ERR_IO;
|
|
|
|
|
xtc_realtime_destroy(backend->engine);
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_audio_plan_destroy(backend->audio);
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_destroy(backend->midi);
|
|
|
|
|
free(backend->instrument);
|
2026-10-27 00:31:03 +01:00
|
|
|
free(backend->timeline);
|
2026-10-26 23:32:04 +01:00
|
|
|
free(backend);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_start(xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
|
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
if (backend->running) return XTC_ERR_INVALID_STATE;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_reset(backend->midi);
|
|
|
|
|
backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
|
2026-10-26 23:32:04 +01:00
|
|
|
if (jack_activate(backend->client) != 0) return XTC_ERR_IO;
|
|
|
|
|
backend->running = 1;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_stop(xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
2026-10-27 00:53:41 +01:00
|
|
|
if (!backend->running) { xtc_midi_plan_reset(backend->midi); return XTC_OK; }
|
2026-10-26 23:32:04 +01:00
|
|
|
if (jack_deactivate(backend->client) != 0) return XTC_ERR_IO;
|
|
|
|
|
backend->running = 0;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_reset(backend->midi);
|
2026-10-26 23:32:04 +01:00
|
|
|
xtc_realtime_collect(backend->engine);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL || sample > UINT64_MAX - 65536U) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready || backend->running ||
|
|
|
|
|
__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
|
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_reset(backend->midi);
|
2026-10-27 00:10:45 +01:00
|
|
|
__atomic_store_n(&backend->sample_position, sample, __ATOMIC_RELAXED);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
|
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
2026-10-26 23:55:35 +01:00
|
|
|
if (!backend->running) {
|
|
|
|
|
xtc_realtime *replacement = NULL;
|
|
|
|
|
xtc_result result = xtc_realtime_create((double)backend->expected_rate, backend->max_frames, &replacement);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
result = xtc_realtime_submit(replacement, graph);
|
|
|
|
|
if (result != XTC_OK) { xtc_realtime_destroy(replacement); return result; }
|
|
|
|
|
/* Inactive client, or successful deactivate: process callback quiescent.
|
|
|
|
|
* Notifications never access engine. Preserve old engine until ready. */
|
|
|
|
|
xtc_realtime_destroy(backend->engine);
|
|
|
|
|
backend->engine = replacement;
|
|
|
|
|
__atomic_store_n(&backend->process_result, (unsigned int)XTC_OK, __ATOMIC_RELAXED);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
return xtc_realtime_submit(backend->engine, graph);
|
|
|
|
|
}
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
|
|
|
|
{
|
|
|
|
|
xtc_audio_plan *candidate = NULL;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan *midi = NULL;
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result result;
|
|
|
|
|
if (!backend || !project) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
result = xtc_audio_plan_create(project, backend->expected_rate, &candidate);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (result == XTC_OK) result = xtc_midi_plan_create(project, backend->expected_rate, &midi);
|
2026-10-27 00:31:03 +01:00
|
|
|
if (result == XTC_OK) result = xtc_backend_submit(backend, xtc_project_graph(project));
|
2026-10-27 00:53:41 +01:00
|
|
|
if (result != XTC_OK) { xtc_audio_plan_destroy(candidate); xtc_midi_plan_destroy(midi); return result; }
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_audio_plan_destroy(backend->audio); backend->audio = candidate;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_destroy(backend->midi); backend->midi = midi;
|
2026-10-27 00:31:03 +01:00
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
const char *xtc_backend_asset_issue(const xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
const char *path = "";
|
|
|
|
|
if (backend) xtc_audio_plan_issues(backend->audio, &path);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder)
|
|
|
|
|
{
|
|
|
|
|
if (!backend) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
if (recorder && (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
|
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE))) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
backend->recorder = recorder;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_backend_panic(xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
if (!backend) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
|
|
|
|
if (!backend->running) xtc_midi_plan_reset(backend->midi);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out)
|
|
|
|
|
{
|
|
|
|
|
if (!backend || !out) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
*out = jack_port_name(backend->midi_input); return XTC_OK;
|
|
|
|
|
}
|
2026-10-26 23:32:04 +01:00
|
|
|
void xtc_backend_collect(xtc_backend *backend)
|
|
|
|
|
{ if (backend != NULL) xtc_realtime_collect(backend->engine); }
|
|
|
|
|
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status)
|
|
|
|
|
{
|
|
|
|
|
xtc_realtime_status engine_status;
|
|
|
|
|
if (backend == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
(void)xtc_realtime_get_status(backend->engine, &engine_status);
|
2026-10-27 00:53:41 +01:00
|
|
|
out_status->midi_dropped = __atomic_load_n(&backend->midi_dropped, __ATOMIC_RELAXED);
|
2026-10-27 00:31:03 +01:00
|
|
|
out_status->asset_issues = (unsigned int)xtc_audio_plan_issues(backend->audio, NULL);
|
2026-10-26 23:32:04 +01:00
|
|
|
out_status->sample_rate = __atomic_load_n(&backend->rate, __ATOMIC_RELAXED);
|
|
|
|
|
out_status->buffer_frames = __atomic_load_n(&backend->frames, __ATOMIC_RELAXED);
|
|
|
|
|
out_status->xruns = __atomic_load_n(&backend->xruns, __ATOMIC_RELAXED);
|
|
|
|
|
out_status->server_lost = (int)__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE);
|
|
|
|
|
out_status->sample_rate_changed = (int)__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE);
|
|
|
|
|
out_status->running = backend->running && !out_status->server_lost;
|
2026-10-27 00:10:45 +01:00
|
|
|
out_status->sample_position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED);
|
2026-10-26 23:32:04 +01:00
|
|
|
out_status->processed_blocks = engine_status.processed_blocks;
|
2026-10-26 23:55:35 +01:00
|
|
|
out_status->submitted_revision = engine_status.submitted_revision;
|
|
|
|
|
out_status->active_revision = engine_status.active_revision;
|
2026-10-26 23:32:04 +01:00
|
|
|
out_status->process_result = (xtc_result)__atomic_load_n(&backend->process_result, __ATOMIC_RELAXED);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction,
|
|
|
|
|
const char **out_name)
|
|
|
|
|
{
|
|
|
|
|
if (backend == NULL || out_name == NULL ||
|
|
|
|
|
(direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
*out_name = jack_port_name(direction == XTC_PORT_INPUT ? backend->input : backend->output);
|
|
|
|
|
return *out_name == NULL ? XTC_ERR_IO : XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
|
|
|
|
|
xtc_backend_port_visitor visitor, void *context)
|
|
|
|
|
{
|
|
|
|
|
const char **ports;
|
|
|
|
|
size_t i;
|
|
|
|
|
if (backend == NULL || visitor == NULL ||
|
|
|
|
|
(direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
ports = jack_get_ports(backend->client, NULL, JACK_DEFAULT_AUDIO_TYPE,
|
|
|
|
|
direction == XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput);
|
|
|
|
|
if (ports == NULL) return XTC_OK;
|
|
|
|
|
for (i = 0U; ports[i] != NULL; ++i) if (visitor(ports[i], context) != 0) break;
|
|
|
|
|
jack_free(ports);
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
static xtc_result route(xtc_backend *backend, xtc_port_direction direction,
|
|
|
|
|
const char *remote, int disconnect)
|
|
|
|
|
{
|
|
|
|
|
const char *own = NULL;
|
|
|
|
|
const char *source, *dest;
|
|
|
|
|
xtc_result result;
|
|
|
|
|
int code;
|
|
|
|
|
if (remote == NULL || remote[0] == '\0') return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
result = xtc_backend_port_name(backend, direction, &own);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
if (!backend->running) return XTC_ERR_INVALID_STATE;
|
|
|
|
|
source = direction == XTC_PORT_OUTPUT ? own : remote;
|
|
|
|
|
dest = direction == XTC_PORT_OUTPUT ? remote : own;
|
|
|
|
|
code = disconnect ? jack_disconnect(backend->client, source, dest) :
|
|
|
|
|
jack_connect(backend->client, source, dest);
|
|
|
|
|
return code == 0 || (!disconnect && code == EEXIST) ? XTC_OK : XTC_ERR_IO;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
|
|
|
{ return route(backend, direction, remote, 0); }
|
|
|
|
|
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
|
|
|
{ return route(backend, direction, remote, 1); }
|
|
|
|
|
|
|
|
|
|
#else
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_backend_panic(xtc_backend *backend)
|
|
|
|
|
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out)
|
|
|
|
|
{ (void)backend; (void)out; return XTC_ERR_UNSUPPORTED; }
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
|
|
|
|
{ (void)backend; (void)project; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
const char *xtc_backend_asset_issue(const xtc_backend *backend)
|
|
|
|
|
{ (void)backend; return ""; }
|
|
|
|
|
xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder)
|
|
|
|
|
{ (void)backend; (void)recorder; return XTC_ERR_UNSUPPORTED; }
|
2026-10-27 00:10:45 +01:00
|
|
|
xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample)
|
|
|
|
|
{ (void)backend; (void)sample; return XTC_ERR_UNSUPPORTED; }
|
2026-10-26 23:32:04 +01:00
|
|
|
/* Keep core/offline builds available on machines without JACK headers. */
|
|
|
|
|
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend)
|
|
|
|
|
{
|
|
|
|
|
if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out_backend = NULL;
|
|
|
|
|
if (config == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
return XTC_ERR_UNSUPPORTED;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_close(xtc_backend *backend)
|
|
|
|
|
{ return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_start(xtc_backend *backend)
|
|
|
|
|
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_stop(xtc_backend *backend)
|
|
|
|
|
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|
|
|
|
{ (void)backend; (void)graph; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
void xtc_backend_collect(xtc_backend *backend) { (void)backend; }
|
|
|
|
|
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status)
|
|
|
|
|
{ (void)backend; (void)out_status; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name)
|
|
|
|
|
{ (void)backend; (void)direction; (void)out_name; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
|
|
|
|
|
xtc_backend_port_visitor visitor, void *context)
|
|
|
|
|
{ (void)backend; (void)direction; (void)visitor; (void)context; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
|
|
|
{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
|
|
|
{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; }
|
|
|
|
|
#endif
|