xtc/src/engine/realtime.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; }