xtc/src/backend/jack_backend.c

336 lines
16 KiB
C
Raw Normal View History

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>
#include <errno.h>
struct xtc_backend {
jack_client_t *client;
jack_port_t *input, *output;
xtc_realtime *engine;
xtc_audio_plan *audio;
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
float *timeline;
2026-10-26 23:32:04 +01:00
size_t max_frames;
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. */
};
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) {
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);
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);
if (result == XTC_OK)
result = xtc_realtime_process_audio(backend->engine, input,
backend->audio ? backend->timeline : NULL, output, (size_t)frames);
2026-10-26 23:32:04 +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);
}
if (result != XTC_OK) {
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);
}
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);
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;
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;
backend->timeline = calloc(config->max_frames, sizeof(float));
if (!backend->timeline) { 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);
if (backend->client == NULL) { 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);
if (backend->input == NULL || backend->output == NULL) { result = XTC_ERR_IO; goto fail; }
if (jack_set_process_callback(backend->client, process, backend) != 0 ||
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);
xtc_audio_plan_destroy(backend->audio);
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);
xtc_audio_plan_destroy(backend->audio);
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;
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;
if (!backend->running) return XTC_OK;
if (jack_deactivate(backend->client) != 0) return XTC_ERR_IO;
backend->running = 0;
xtc_realtime_collect(backend->engine);
return XTC_OK;
}
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;
__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;
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);
}
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
{
xtc_audio_plan *candidate = NULL;
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);
if (result == XTC_OK) result = xtc_backend_submit(backend, xtc_project_graph(project));
if (result != XTC_OK) { xtc_audio_plan_destroy(candidate); return result; }
xtc_audio_plan_destroy(backend->audio); backend->audio = candidate;
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-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);
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;
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;
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
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; }
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