#include "xtc/xtc_realtime.h" #include #include #include 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; }