1038 lines
38 KiB
C
1038 lines
38 KiB
C
#include "xtc/xtc_backend.h"
|
|
#include "xtc/xtc_realtime.h"
|
|
#include "xtc/xtc_dsp.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef XTC_HAVE_JACK
|
|
#include <jack/jack.h>
|
|
#include <jack/midiport.h>
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
|
|
struct xtc_backend {
|
|
jack_client_t *client;
|
|
jack_port_t *input, *output, *midi_input, *output_right;
|
|
xtc_realtime *engine;
|
|
xtc_audio_plan *audio;
|
|
xtc_midi_plan *midi;
|
|
unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right;
|
|
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
|
|
float *timeline, *instrument;
|
|
size_t max_frames;
|
|
size_t transition_frames; /* Control owner while stopped. */
|
|
xtc_gain_ramp ramp; /* Audio owner while active. */
|
|
unsigned int stop_requested, stop_done;
|
|
int stop_seen; /* Audio owner. */
|
|
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. */
|
|
};
|
|
|
|
/**
|
|
* @brief Decodes queued JACK MIDI and renders the instrument for the current audio block.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param position Absolute sample position at the start of the block.
|
|
* @param frames Number of audio frames.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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_stereo(backend->midi, position, events, count, backend->instrument,
|
|
backend->output_right ? backend->instrument + backend->max_frames : NULL,
|
|
(size_t)frames);
|
|
}
|
|
|
|
/**
|
|
* @brief Runs the JACK audio callback, rendering sources, processing the graph, and publishing status.
|
|
*
|
|
* @param frames Number of audio frames.
|
|
* @param context Opaque callback context.
|
|
* @return Zero after publishing the callback result.
|
|
*/
|
|
static int process(jack_nframes_t frames, void *context)
|
|
{
|
|
xtc_backend *backend = context;
|
|
float *output = jack_port_get_buffer(backend->output, frames);
|
|
float *right = backend->output_right ? jack_port_get_buffer(backend->output_right, frames) : NULL;
|
|
xtc_result result = XTC_OK;
|
|
if (output == NULL || (backend->output_right && !right)) {
|
|
if (output)
|
|
memset(output, 0, (size_t)frames * sizeof(*output));
|
|
if (right)
|
|
memset(right, 0, (size_t)frames * sizeof(*right));
|
|
__atomic_store_n(&backend->peak_left, 0U, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&backend->peak_right, 0U, __ATOMIC_RELAXED);
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
|
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->stop_done, __ATOMIC_ACQUIRE)) {
|
|
memset(output, 0, (size_t)frames * sizeof(*output));
|
|
if (right) memset(right, 0, (size_t)frames * sizeof(*right));
|
|
__atomic_store_n(&backend->peak_left, 0U, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&backend->peak_right, 0U, __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_stereo(backend->audio, position, backend->timeline,
|
|
right ? backend->timeline + backend->max_frames : NULL, (size_t)frames);
|
|
if (result == XTC_OK)
|
|
result = render_midi(backend, position, frames);
|
|
if (result == XTC_OK)
|
|
result =
|
|
xtc_realtime_process_stereo(backend->engine, input, backend->audio ? backend->timeline : NULL,
|
|
backend->audio && right ? backend->timeline + backend->max_frames : NULL,
|
|
backend->midi ? backend->instrument : NULL,
|
|
backend->midi && right ? backend->instrument + backend->max_frames : NULL,
|
|
output, right, (size_t)frames);
|
|
}
|
|
if (result == XTC_OK && backend->transition_frames) {
|
|
if (__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE) && !backend->stop_seen) {
|
|
(void)xtc_gain_ramp_target(&backend->ramp, 0, backend->transition_frames);
|
|
backend->stop_seen = 1;
|
|
}
|
|
(void)xtc_gain_ramp_process(&backend->ramp, output, right, (size_t)frames);
|
|
if (backend->stop_seen && !backend->ramp.remaining)
|
|
__atomic_store_n(&backend->stop_done, 1U, __ATOMIC_RELEASE);
|
|
}
|
|
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) {
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
|
if (backend->recorder)
|
|
xtc_recorder_push(backend->recorder, NULL, 1U);
|
|
memset(output, 0, (size_t)frames * sizeof(*output));
|
|
if (right)
|
|
memset(right, 0, (size_t)frames * sizeof(*right));
|
|
}
|
|
{
|
|
size_t i;
|
|
float left_peak = 0, right_peak = 0;
|
|
for (i = 0; i < frames; ++i) {
|
|
float l = fabsf(output[i]), r = right ? fabsf(right[i]) : l;
|
|
if (l > left_peak)
|
|
left_peak = l;
|
|
if (r > right_peak)
|
|
right_peak = r;
|
|
}
|
|
__atomic_store_n(&backend->peak_left, (unsigned int)(fminf(left_peak, 1000) * 1000000), __ATOMIC_RELAXED);
|
|
__atomic_store_n(&backend->peak_right, (unsigned int)(fminf(right_peak, 1000) * 1000000), __ATOMIC_RELAXED);
|
|
}
|
|
__atomic_store_n(&backend->process_result, (unsigned int)result, __ATOMIC_RELAXED);
|
|
return 0; /* Keep connected and silent on recoverable processing errors. */
|
|
}
|
|
|
|
/**
|
|
* @brief Marks server loss and clears running state when JACK shuts down.
|
|
*
|
|
* @param context Opaque callback context.
|
|
*/
|
|
static void shutdown_callback(void *context)
|
|
{
|
|
xtc_backend *backend = context;
|
|
__atomic_store_n(&backend->lost, 1U, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
/**
|
|
* @brief Requests a MIDI panic when a connection to the MIDI input changes.
|
|
*
|
|
* @param source JACK source port identifier.
|
|
* @param destination JACK destination port identifier.
|
|
* @param connected Nonzero when the ports became connected.
|
|
* @param context Opaque callback context.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @brief Records a JACK sample-rate change and disables processing until recovery.
|
|
*
|
|
* @param rate Sample rate in hertz.
|
|
* @param context Opaque callback context.
|
|
* @return Zero to acknowledge the rate notification.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Records the JACK buffer size and reports whether it exceeds the configured limit.
|
|
*
|
|
* @param frames Number of audio frames.
|
|
* @param context Opaque callback context.
|
|
* @return Zero if the buffer fits, nonzero if it exceeds the configured capacity.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Increments the xrun counter and requests a MIDI panic.
|
|
*
|
|
* @param context Opaque callback context.
|
|
* @return Zero to acknowledge the xrun notification.
|
|
*/
|
|
static int xrun_callback(void *context)
|
|
{
|
|
xtc_backend *backend = context;
|
|
(void)__atomic_fetch_add(&backend->xruns, 1U, __ATOMIC_RELAXED);
|
|
(void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Opens a JACK client and registers its ports without starting the server or connecting routes.
|
|
*
|
|
* @param config Backend kind, client name, block capacity, and output channel configuration.
|
|
* @param out_backend Receives the newly allocated backend; caller owns it on success.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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 && config->channel_count != 2U))
|
|
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 * config->channel_count, sizeof(float));
|
|
backend->instrument = calloc(config->max_frames * config->channel_count, sizeof(float));
|
|
if (!backend->timeline || !backend->instrument) {
|
|
free(backend->timeline);
|
|
free(backend->instrument);
|
|
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->instrument);
|
|
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);
|
|
backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U);
|
|
if (config->channel_count == 2U)
|
|
backend->output_right =
|
|
jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
|
|
if ((config->channel_count == 2U && !backend->output_right) || 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 ||
|
|
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);
|
|
xtc_midi_plan_destroy(backend->midi);
|
|
free(backend->instrument);
|
|
free(backend->timeline);
|
|
free(backend);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @brief Quiesces callbacks and releases the backend; failed cleanup leaves the handle owned by the caller.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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);
|
|
xtc_midi_plan_destroy(backend->midi);
|
|
free(backend->instrument);
|
|
free(backend->timeline);
|
|
free(backend);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Activates the backend and enables audio processing.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
xtc_midi_plan_reset(backend->midi);
|
|
backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE);
|
|
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
|
|
backend->stop_seen = 0;
|
|
(void)xtc_gain_ramp_reset(&backend->ramp, backend->transition_frames ? 0 : 1);
|
|
(void)xtc_gain_ramp_target(&backend->ramp, 1, backend->transition_frames);
|
|
if (jack_activate(backend->client) != 0)
|
|
return XTC_ERR_IO;
|
|
backend->running = 1;
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Deactivates the backend and establishes callback quiescence.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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) {
|
|
xtc_midi_plan_reset(backend->midi);
|
|
return XTC_OK;
|
|
}
|
|
if (jack_deactivate(backend->client) != 0)
|
|
return XTC_ERR_IO;
|
|
backend->running = 0;
|
|
__atomic_store_n(&backend->stop_requested, 0U, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&backend->stop_done, 0U, __ATOMIC_RELAXED);
|
|
xtc_midi_plan_reset(backend->midi);
|
|
xtc_realtime_collect(backend->engine);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Changes the stopped backend's sample position and resets MIDI voices.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param sample Absolute sample position.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
xtc_midi_plan_reset(backend->midi);
|
|
__atomic_store_n(&backend->sample_position, sample, __ATOMIC_RELAXED);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Compiles and submits a graph while preserving the previous engine on failure.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param graph Graph model; borrowed for this call.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @brief Preloads project audio and MIDI and submits its graph while stopped.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param project Project model; borrowed for this call.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
|
{
|
|
xtc_audio_plan *candidate = NULL;
|
|
xtc_midi_plan *midi = 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_audio_plan_set_fade_frames(candidate, backend->transition_frames);
|
|
if (result == XTC_OK)
|
|
result = xtc_midi_plan_create(project, backend->expected_rate, &midi);
|
|
if (result == XTC_OK)
|
|
result = xtc_midi_plan_set_fade_frames(midi, backend->transition_frames);
|
|
if (result == XTC_OK)
|
|
result = xtc_backend_submit(backend, xtc_project_graph(project));
|
|
if (result != XTC_OK) {
|
|
xtc_audio_plan_destroy(candidate);
|
|
xtc_midi_plan_destroy(midi);
|
|
return result;
|
|
}
|
|
xtc_audio_plan_destroy(backend->audio);
|
|
backend->audio = candidate;
|
|
xtc_midi_plan_destroy(backend->midi);
|
|
backend->midi = midi;
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the first asset issue path, or an empty string when none is available.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @return Borrowed path string, or an empty string if no issue is available.
|
|
*/
|
|
const char *xtc_backend_asset_issue(const xtc_backend *backend)
|
|
{
|
|
const char *path = "";
|
|
if (backend)
|
|
xtc_audio_plan_issues(backend->audio, &path);
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* @brief Attaches or detaches a borrowed recorder while the backend is stopped.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param recorder Recorder handle.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Requests an instrument panic at the next audio block boundary.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the borrowed name of the backend's MIDI input port.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param out Receives the newly allocated object; caller owns it on success.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/* Control-only configuration; applied to source plans on next preparation. */
|
|
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
|
|
{
|
|
if (!backend || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
|
|
if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE;
|
|
backend->transition_frames = frames;
|
|
return XTC_OK;
|
|
}
|
|
|
|
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
|
{
|
|
if (!backend) return XTC_ERR_INVALID_ARGUMENT;
|
|
if (!backend->ready || backend->recorder) return XTC_ERR_INVALID_STATE;
|
|
if (!backend->running || !backend->transition_frames ||
|
|
__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
|
|
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE) ||
|
|
__atomic_load_n(&backend->process_result, __ATOMIC_RELAXED) != (unsigned int)XTC_OK)
|
|
return xtc_backend_stop(backend);
|
|
__atomic_store_n(&backend->stop_requested, 1U, __ATOMIC_RELEASE);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/* Control thread: acknowledgement is not quiescence until stop succeeds. */
|
|
void xtc_backend_collect(xtc_backend *backend)
|
|
{
|
|
if (backend != NULL) {
|
|
if (__atomic_load_n(&backend->stop_done, __ATOMIC_ACQUIRE)) {
|
|
if (xtc_backend_stop(backend) != XTC_OK)
|
|
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
|
|
}
|
|
xtc_realtime_collect(backend->engine);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Copies individually coherent backend counters and transport state.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param out_status Receives the status.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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->peak_left =
|
|
backend->running ? (float)__atomic_load_n(&backend->peak_left, __ATOMIC_RELAXED) / 1000000.0f : 0;
|
|
out_status->peak_right =
|
|
backend->running ? (float)__atomic_load_n(&backend->peak_right, __ATOMIC_RELAXED) / 1000000.0f : 0;
|
|
out_status->stopping = (int)__atomic_load_n(&backend->stop_requested, __ATOMIC_ACQUIRE);
|
|
out_status->midi_dropped = __atomic_load_n(&backend->midi_dropped, __ATOMIC_RELAXED);
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the borrowed name of the mono input or primary output port.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param direction Local port direction.
|
|
* @param out_name Receives the name.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the borrowed name of an output channel port.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param channel Output channel index: 0 for primary, 1 for stereo right.
|
|
* @param out Receives the newly allocated object; caller owns it on success.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_backend_output_name(const xtc_backend *backend, size_t channel, const char **out)
|
|
{
|
|
if (!backend || !out || channel > 1)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE))
|
|
return XTC_ERR_INVALID_STATE;
|
|
if (channel && !backend->output_right)
|
|
return XTC_ERR_NOT_FOUND;
|
|
*out = jack_port_name(channel ? backend->output_right : backend->output);
|
|
return *out ? XTC_OK : XTC_ERR_IO;
|
|
}
|
|
|
|
/**
|
|
* @brief Visits available audio ports matching the requested direction until the visitor stops.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param direction Local port direction.
|
|
* @param visitor Visitor called with each borrowed port name.
|
|
* @param context Opaque callback context.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Connects or disconnects a local audio port and a named remote port.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param direction Local port direction.
|
|
* @param remote Borrowed remote port name.
|
|
* @param disconnect Nonzero to disconnect, zero to connect.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Connects this client's port to a remote port with the opposite direction.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param direction Local port direction.
|
|
* @param remote Borrowed remote port name.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
{
|
|
return route(backend, direction, remote, 0);
|
|
}
|
|
|
|
/**
|
|
* @brief Disconnects this client's port from a remote port.
|
|
*
|
|
* @param backend Backend handle.
|
|
* @param direction Local port direction.
|
|
* @param remote Borrowed remote port name.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
|
|
{
|
|
return route(backend, direction, remote, 1);
|
|
}
|
|
|
|
#else
|
|
/**
|
|
* @brief Reports that output name is unavailable without JACK support.
|
|
*
|
|
* @param b Unused in this build.
|
|
* @param c Unused in this build.
|
|
* @param out Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_output_name(const xtc_backend *b, size_t c, const char **out)
|
|
{
|
|
(void)b;
|
|
(void)c;
|
|
(void)out;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that panic is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_panic(xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that midi port name is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param out Unused in this build.
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that prepare project is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param project Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
|
{
|
|
(void)backend;
|
|
(void)project;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns an empty issue string in builds without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @return Borrowed empty static string.
|
|
*/
|
|
const char *xtc_backend_asset_issue(const xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that set recorder is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param recorder Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder)
|
|
{
|
|
(void)backend;
|
|
(void)recorder;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that seek is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param sample Unused in this build.
|
|
* @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. */
|
|
/**
|
|
* @brief Validates open arguments and reports that JACK support is unavailable.
|
|
*
|
|
* @param config Configuration to check; must not be NULL.
|
|
* @param out_backend Receives NULL; must not be NULL.
|
|
* @return XTC_ERR_INVALID_ARGUMENT for missing pointers, otherwise XTC_ERR_UNSUPPORTED; clears a supplied output handle.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Accepts a NULL backend in builds without JACK support.
|
|
*
|
|
* @param backend Handle to close; NULL is accepted.
|
|
* @return XTC_OK for NULL, otherwise XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_close(xtc_backend *backend)
|
|
{
|
|
return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that start is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_start(xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that stop is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_stop(xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that submit is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param graph Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|
{
|
|
(void)backend;
|
|
(void)graph;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Performs no collection in builds without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
*/
|
|
void xtc_backend_collect(xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that get status is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param out_status Unused in this build.
|
|
* @return XTC_ERR_UNSUPPORTED.
|
|
*/
|
|
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status)
|
|
{
|
|
(void)backend;
|
|
(void)out_status;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that port name is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param direction Unused in this build.
|
|
* @param out_name Unused in this build.
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that visit ports is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param direction Unused in this build.
|
|
* @param visitor Unused in this build.
|
|
* @param context Unused in this build.
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that connect is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param direction Unused in this build.
|
|
* @param remote Unused in this build.
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @brief Reports that disconnect is unavailable without JACK support.
|
|
*
|
|
* @param backend Unused in this build.
|
|
* @param direction Unused in this build.
|
|
* @param remote Unused in this build.
|
|
* @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;
|
|
}
|
|
xtc_result xtc_backend_set_transition_frames(xtc_backend *backend, size_t frames)
|
|
{
|
|
(void)backend; (void)frames;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
|
{
|
|
(void)backend;
|
|
return XTC_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
#endif
|