#include "xtc/xtc_backend.h" #include "xtc/xtc_realtime.h" #include #include #ifdef XTC_HAVE_JACK #include #include 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; size_t max_frames; uint64_t sample_position; 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); __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); } 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)); } __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; 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; } 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; } 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); 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); 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; } 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; } 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; } 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); 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); out_status->processed_blocks = engine_status.processed_blocks; out_status->submitted_revision = engine_status.submitted_revision; out_status->active_revision = engine_status.active_revision; 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; } /* 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