242 lines
10 KiB
C
242 lines
10 KiB
C
#include "xtc/xtc_realtime.h"
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
struct xtc_realtime {
|
|
double sample_rate;
|
|
uint64_t position;
|
|
size_t max_frames;
|
|
xtc_plan *pending;
|
|
xtc_plan *retired;
|
|
xtc_plan *active; /* Audio owner only, until consumer is stopped/joined. */
|
|
unsigned int blocks;
|
|
unsigned int result;
|
|
unsigned int submitted_revision, pending_revision, active_revision;
|
|
};
|
|
|
|
/**
|
|
* @brief Creates a realtime engine for single-producer, single-consumer use.
|
|
*
|
|
* The engine starts without an active plan, so processing produces silence
|
|
* until a graph is submitted. The caller owns the new engine and must release
|
|
* it with xtc_realtime_destroy() after stopping the audio consumer.
|
|
*
|
|
* @param sample_rate Sample rate in Hz; must be finite and positive.
|
|
* @param max_frames Maximum frames per process call, from 1 to 65536.
|
|
* @param out_engine Receives the new engine on success and NULL on failure;
|
|
* must not be NULL.
|
|
* @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for invalid limits,
|
|
* XTC_ERR_UNSUPPORTED if required atomics are not lock-free, or
|
|
* XTC_ERR_OUT_OF_MEMORY if allocation fails.
|
|
*/
|
|
xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, xtc_realtime **out_engine)
|
|
{
|
|
xtc_realtime *engine;
|
|
if (out_engine == NULL)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
*out_engine = NULL;
|
|
if (!isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U || max_frames > 65536U)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
if (!__atomic_always_lock_free(sizeof(xtc_plan *), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0))
|
|
return XTC_ERR_UNSUPPORTED;
|
|
engine = calloc(1U, sizeof(*engine));
|
|
if (engine == NULL)
|
|
return XTC_ERR_OUT_OF_MEMORY;
|
|
engine->sample_rate = sample_rate;
|
|
engine->max_frames = max_frames;
|
|
*out_engine = engine;
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Frees acknowledged retired plans on the control thread.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
*/
|
|
void xtc_realtime_collect(xtc_realtime *engine)
|
|
{
|
|
xtc_plan *retired;
|
|
if (engine == NULL)
|
|
return;
|
|
retired = __atomic_exchange_n(&engine->retired, NULL, __ATOMIC_ACQ_REL);
|
|
xtc_plan_destroy(retired);
|
|
}
|
|
|
|
/**
|
|
* @brief Releases the engine after the audio consumer has stopped; accepts NULL.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
*/
|
|
void xtc_realtime_destroy(xtc_realtime *engine)
|
|
{
|
|
if (engine == NULL)
|
|
return;
|
|
/* Lifecycle contract: no callback can still execute. */
|
|
xtc_plan_destroy(engine->active);
|
|
xtc_plan_destroy(engine->pending);
|
|
xtc_plan_destroy(engine->retired);
|
|
free(engine);
|
|
}
|
|
|
|
/**
|
|
* @brief Compiles and publishes a graph to the single pending-plan slot.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
* @param graph Graph model; borrowed for this call.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph)
|
|
{
|
|
xtc_plan *plan = NULL;
|
|
xtc_result result;
|
|
if (engine == NULL || graph == NULL)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
xtc_realtime_collect(engine);
|
|
if (__atomic_load_n(&engine->pending, __ATOMIC_ACQUIRE) != NULL)
|
|
return XTC_ERR_CAPACITY;
|
|
result = xtc_graph_compile(graph, engine->sample_rate, engine->max_frames, &plan);
|
|
if (result != XTC_OK)
|
|
return result;
|
|
{
|
|
unsigned int revision = __atomic_load_n(&engine->submitted_revision, __ATOMIC_RELAXED) + 1U;
|
|
__atomic_store_n(&engine->pending_revision, revision, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&engine->submitted_revision, revision, __ATOMIC_RELAXED);
|
|
}
|
|
__atomic_store_n(&engine->pending, plan, __ATOMIC_RELEASE);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Copies processing counters, plan revisions, and the last processing result.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
* @param out_status Receives the status.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_get_status(const xtc_realtime *engine, xtc_realtime_status *out_status)
|
|
{
|
|
if (engine == NULL || out_status == NULL)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
out_status->processed_blocks = __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED);
|
|
out_status->submitted_revision = __atomic_load_n(&engine->submitted_revision, __ATOMIC_RELAXED);
|
|
out_status->active_revision = __atomic_load_n(&engine->active_revision, __ATOMIC_RELAXED);
|
|
out_status->last_result = (xtc_result)__atomic_load_n(&engine->result, __ATOMIC_RELAXED);
|
|
return XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Adopts a pending plan and processes stereo sources without allocation or locking.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
* @param input Borrowed input sample buffer.
|
|
* @param timeline Borrowed timeline source buffer; NULL means silence.
|
|
* @param timeline_right Borrowed right timeline source buffer; NULL means silence.
|
|
* @param instrument Borrowed instrument source buffer; NULL means silence.
|
|
* @param instrument_right Borrowed right instrument source buffer; NULL means silence.
|
|
* @param output Destination buffer with room for the requested frames.
|
|
* @param right Destination right-channel buffer; NULL selects mono rendering.
|
|
* @param frame_count Number of audio frames.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input, const float *timeline,
|
|
const float *timeline_right, const float *instrument,
|
|
const float *instrument_right, float *output, float *right, size_t frame_count)
|
|
{
|
|
xtc_result result = XTC_OK;
|
|
if (engine == NULL || output == NULL || (right && right == output) || frame_count > SIZE_MAX / sizeof(*output))
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
if (frame_count == 0U)
|
|
return XTC_OK;
|
|
if (frame_count > engine->max_frames) {
|
|
result = XTC_ERR_CAPACITY;
|
|
} else {
|
|
/* A full retirement slot delays adoption without delaying processing.
|
|
* acquire pairs with control's release exchange after its last read. */
|
|
if (__atomic_load_n(&engine->retired, __ATOMIC_ACQUIRE) == NULL) {
|
|
xtc_plan *next = __atomic_load_n(&engine->pending, __ATOMIC_ACQUIRE);
|
|
if (next != NULL) {
|
|
xtc_plan *old = engine->active;
|
|
/* Read revision BEFORE releasing the pending slot. The producer
|
|
* cannot reuse pending_revision until its acquire observes NULL. */
|
|
unsigned int revision = __atomic_load_n(&engine->pending_revision, __ATOMIC_RELAXED);
|
|
(void)__atomic_exchange_n(&engine->pending, NULL, __ATOMIC_ACQ_REL);
|
|
engine->active = next;
|
|
__atomic_store_n(&engine->active_revision, revision, __ATOMIC_RELAXED);
|
|
/* No reference to old is used after this release. */
|
|
__atomic_store_n(&engine->retired, old, __ATOMIC_RELEASE);
|
|
}
|
|
}
|
|
if (engine->active != NULL) {
|
|
result = xtc_plan_position(engine->active,engine->position);
|
|
if (result == XTC_OK) result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument,
|
|
instrument_right, output, right, frame_count);
|
|
} else {
|
|
memset(output, 0, frame_count * sizeof(*output));
|
|
if (right)
|
|
memset(right, 0, frame_count * sizeof(*right));
|
|
}
|
|
}
|
|
if (result == XTC_OK) {
|
|
if (engine->position <= UINT64_MAX-frame_count) engine->position += frame_count;
|
|
else result = XTC_ERR_CAPACITY;
|
|
}
|
|
if (result != XTC_OK) {
|
|
memset(output, 0, frame_count * sizeof(*output));
|
|
if (right)
|
|
memset(right, 0, frame_count * sizeof(*right));
|
|
}
|
|
__atomic_store_n(&engine->result, (unsigned int)result, __ATOMIC_RELAXED);
|
|
__atomic_store_n(&engine->blocks, __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED) + 1U, __ATOMIC_RELAXED);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a mono block with external input only.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
* @param input Borrowed input sample buffer.
|
|
* @param output Destination buffer with room for the requested frames.
|
|
* @param frames Number of audio frames.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, float *output, size_t frames)
|
|
{
|
|
return xtc_realtime_process_audio(engine, input, NULL, output, frames);
|
|
}
|
|
|
|
/**
|
|
* @brief Processes a mono block with timeline audio and no instrument source.
|
|
*
|
|
* @param engine Realtime engine handle.
|
|
* @param input Borrowed input sample buffer.
|
|
* @param timeline Borrowed timeline source buffer; NULL means silence.
|
|
* @param output Destination buffer with room for the requested frames.
|
|
* @param frames Number of audio frames.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, float *output,
|
|
size_t frames)
|
|
{
|
|
return xtc_realtime_process_sources(engine, input, timeline, NULL, output, frames);
|
|
}
|
|
|
|
/**
|
|
* @brief Processes mono timeline and instrument sources through the realtime engine.
|
|
*
|
|
* @param p Realtime engine handle.
|
|
* @param in Borrowed mono input buffer; NULL means silence.
|
|
* @param audio Borrowed timeline source buffer; NULL means silence.
|
|
* @param midi Borrowed instrument source buffer; NULL means silence.
|
|
* @param out Destination sample buffer with room for the requested frames.
|
|
* @param frames Number of audio frames.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const float *audio, const float *midi,
|
|
float *out, size_t frames)
|
|
{
|
|
return xtc_realtime_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames);
|
|
}
|
|
|
|
xtc_result xtc_realtime_position(xtc_realtime *p, uint64_t position)
|
|
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }
|