xtc/src/engine/offline_engine.c

377 lines
16 KiB
C

#include "xtc/xtc_dsp.h"
#include "../graph/graph_internal.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#define XTC_TAU 6.28318530717958647692
struct xtc_plan {
size_t node_count, max_frames;
xtc_node *nodes;
size_t *order, *offsets, *sources;
unsigned char *render;
float *buffers, *peaks;
double *phases;
double sample_rate;
float input_gain;
uint64_t position;
xtc_native **native;
xtc_plugin **plugins;
};
/**
* @brief Releases a compiled plan and all resources it owns.
*
* @param plan Plan to destroy; NULL is accepted.
*/
void xtc_plan_destroy(xtc_plan *plan)
{
if (plan == NULL)
return;
if (plan->native) {
size_t i;
for (i = 0; i < plan->node_count * 2; ++i)
xtc_native_destroy(plan->native[i]);
}
if (plan->plugins) {
size_t i;
for (i = 0; i < plan->node_count * 2; ++i) xtc_plugin_destroy(plan->plugins[i]);
}
free(plan->plugins);
free(plan->native);
free(plan->nodes);
free(plan->order);
free(plan->offsets);
free(plan->sources);
free(plan->render);
free(plan->buffers);
free(plan->peaks);
free(plan->phases);
free(plan);
}
/**
* @brief Allocates an independent execution plan with preallocated processing buffers.
*
* @param graph Graph model; borrowed for this call.
* @param sample_rate Sample rate in hertz.
* @param max_frames Maximum frames per processing block.
* @param out_plan Receives the newly allocated plan; caller owns it on success.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t max_frames, xtc_plan **out_plan)
{
xtc_plan *plan;
xtc_result result;
size_t node, edge, storage_nodes, cursor = 0U;
int has_output = 0;
if (out_plan == NULL)
return XTC_ERR_INVALID_ARGUMENT;
*out_plan = NULL;
if (graph == NULL || !isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U ||
max_frames > XTC_MAX_FRAMES)
return XTC_ERR_INVALID_ARGUMENT;
storage_nodes = graph->node_count == 0U ? 1U : graph->node_count;
for (node = 0U; node < graph->node_count; ++node) {
if (graph->nodes[node].type == XTC_NODE_OSCILLATOR && (double)graph->nodes[node].value > sample_rate * 0.5)
return XTC_ERR_INVALID_ARGUMENT;
if (graph->nodes[node].type == XTC_NODE_OUTPUT)
has_output = 1;
}
/* Fixed graph/frame limits bound all allocation products below. */
plan = calloc(1U, sizeof(*plan));
if (plan == NULL)
return XTC_ERR_OUT_OF_MEMORY;
plan->node_count = graph->node_count;
plan->max_frames = max_frames;
plan->sample_rate = sample_rate;
plan->input_gain = 1.0f;
plan->plugins = calloc(storage_nodes * 2U,sizeof(*plan->plugins));
plan->native = calloc(storage_nodes * 2U, sizeof(*plan->native));
plan->nodes = calloc(storage_nodes, sizeof(*plan->nodes));
plan->order = calloc(storage_nodes, sizeof(*plan->order));
plan->offsets = calloc(plan->node_count + 1U, sizeof(*plan->offsets));
plan->sources = calloc(graph->edge_count + 1U, sizeof(*plan->sources));
plan->render = calloc(storage_nodes, sizeof(*plan->render));
plan->buffers = calloc(storage_nodes * max_frames * 2U, sizeof(*plan->buffers));
plan->peaks = calloc(storage_nodes, sizeof(*plan->peaks));
plan->phases = calloc(storage_nodes, sizeof(*plan->phases));
if (plan->plugins == NULL || plan->native == NULL || plan->nodes == NULL || plan->order == NULL || plan->offsets == NULL ||
plan->sources == NULL || plan->render == NULL || plan->buffers == NULL || plan->peaks == NULL ||
plan->phases == NULL) {
xtc_plan_destroy(plan);
return XTC_ERR_OUT_OF_MEMORY;
}
result = xtc_graph_order(graph, plan->order);
if (result != XTC_OK) {
xtc_plan_destroy(plan);
return result;
}
memcpy(plan->nodes, graph->nodes, plan->node_count * sizeof(*plan->nodes));
for (node = 0U; node < plan->node_count; ++node) {
int terminal = 1;
plan->offsets[node] = cursor;
for (edge = 0U; edge < graph->edge_count; ++edge) {
if (graph->edges[edge].destination == node)
plan->sources[cursor++] = graph->edges[edge].source;
if (graph->edges[edge].source == node)
terminal = 0;
}
plan->render[node] =
(unsigned char)(has_output || graph->require_output_nodes ? plan->nodes[node].type == XTC_NODE_OUTPUT
: terminal);
}
for (node = 0; node < plan->node_count; ++node) {
if (plan->nodes[node].type == XTC_NODE_PLUGIN) {
size_t side;
for (side = 0; side < 2; ++side) {
result = xtc_plugin_create(&plan->nodes[node].plugin,sample_rate,max_frames,
&plan->plugins[side * plan->node_count + node]);
if (result != XTC_OK) { xtc_plan_destroy(plan); return result; }
}
}
if (plan->nodes[node].type == XTC_NODE_NATIVE) {
size_t side;
for (side = 0; side < 2; ++side) {
result = xtc_native_create(&plan->nodes[node].native, sample_rate, max_frames, plan->nodes[node].value,
&plan->native[side * plan->node_count + node]);
if (result != XTC_OK) {
xtc_plan_destroy(plan);
return result;
}
}
}
}
plan->offsets[plan->node_count] = cursor;
*out_plan = plan;
return XTC_OK;
}
/**
* @brief Resets oscillator phases, meters, and native processor state without allocating.
*
* @param plan Compiled graph plan and its single-owner runtime state.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_plan_reset(xtc_plan *plan)
{
if (plan == NULL)
return XTC_ERR_INVALID_ARGUMENT;
{
size_t i;
for (i = 0; i < plan->node_count * 2; ++i)
xtc_native_reset(plan->native[i]);
}
plan->position = 0;
memset(plan->phases, 0, plan->node_count * sizeof(*plan->phases));
memset(plan->peaks, 0, plan->node_count * sizeof(*plan->peaks));
return XTC_OK;
}
/**
* @brief Copies the last block's absolute peak for a meter node.
*
* @param plan Compiled graph plan and its single-owner runtime state.
* @param node_id Stable node identifier.
* @param out_peak Receives the peak.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_peak)
{
if (plan == NULL || out_peak == NULL)
return XTC_ERR_INVALID_ARGUMENT;
size_t index;
for (index = 0U; index < plan->node_count; ++index)
if (plan->nodes[index].id == node_id)
break;
if (index == plan->node_count)
return XTC_ERR_NOT_FOUND;
node_id = index;
if (plan->nodes[node_id].type != XTC_NODE_METER)
return XTC_ERR_INVALID_ARGUMENT;
*out_peak = plan->peaks[node_id];
return XTC_OK;
}
/**
* @brief Processes a block with separate stereo timeline and instrument sources.
*
* @param plan Compiled graph plan and its single-owner runtime state.
* @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_plan_process_stereo(xtc_plan *plan, 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)
{
size_t index, frame, edge, channel;
if (plan == NULL || output == NULL || (right && right == output))
return XTC_ERR_INVALID_ARGUMENT;
if (frame_count > plan->max_frames || plan->position > UINT64_MAX - frame_count)
return XTC_ERR_CAPACITY;
if (frame_count == 0U)
return XTC_OK;
/* All external input reads finish before output is written, allowing aliasing.
* Nonfinite input/arithmetic becomes silence per sample, never persistent DSP state. */
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
const float *audio_source = channel ? timeline_right : timeline;
const float *midi_source = channel ? instrument_right : instrument;
for (index = 0U; index < plan->node_count; ++index) {
size_t node = plan->order[index];
const xtc_node *spec = &plan->nodes[node];
float *buffer = plan->buffers + (channel * plan->node_count + node) * plan->max_frames;
float peak = 0.0f;
double phase = plan->phases[node];
for (frame = 0U; frame < frame_count; ++frame) {
float sample = 0.0f;
if (spec->type == XTC_NODE_INSTRUMENT) {
sample = midi_source ? midi_source[frame] : 0.0f;
} else if (spec->type == XTC_NODE_AUDIO) {
sample = audio_source ? audio_source[frame] : 0.0f;
} else if (spec->type == XTC_NODE_OSCILLATOR) {
sample = (float)sin(XTC_TAU * phase);
phase += (double)spec->value / plan->sample_rate;
if (phase >= 1.0)
phase -= 1.0;
} else {
if (plan->offsets[node] == plan->offsets[node + 1U])
sample = input == NULL ? 0.0f : input[frame] * plan->input_gain;
for (edge = plan->offsets[node]; edge < plan->offsets[node + 1U]; ++edge)
sample += plan->buffers[(channel * plan->node_count + plan->sources[edge]) * plan->max_frames +
frame];
if (spec->type == XTC_NODE_GAIN)
sample *= spec->value;
}
sample = xtc_dsp_finite(sample);
buffer[frame] = sample;
if (fabsf(sample) > peak)
peak = fabsf(sample);
}
if (spec->type == XTC_NODE_NATIVE) {
xtc_native_block block = {spec->native.kind == XTC_NATIVE_EFFECT ? buffer : NULL,
buffer,
frame_count,
plan->position,
NULL,
0};
xtc_result result = xtc_native_process(plan->native[channel * plan->node_count + node], &block);
if (result != XTC_OK) {
memset(output, 0, frame_count * sizeof(*output));
if (right)
memset(right, 0, frame_count * sizeof(*right));
return result;
}
}
if (spec->type == XTC_NODE_PLUGIN) {
xtc_result result = xtc_plugin_process_at(plan->plugins[channel * plan->node_count + node],
spec->plugin.inputs ? buffer : NULL,buffer,frame_count,plan->position);
if (result != XTC_OK) {
memset(output,0,frame_count*sizeof(*output));
if (right) memset(right,0,frame_count*sizeof(*right));
return result;
}
}
if (!right || channel == 1)
plan->phases[node] = phase;
if (spec->type == XTC_NODE_METER)
plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak;
}
}
/* Finish both channels before writing either caller buffer. */
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
float *destination = channel ? right : output;
for (frame = 0U; frame < frame_count; ++frame) {
float sample = 0.0f;
for (index = 0U; index < plan->node_count; ++index)
if (plan->render[index])
sample += plan->buffers[(channel * plan->node_count + index) * plan->max_frames + frame];
destination[frame] = xtc_dsp_finite(sample);
}
}
plan->position += frame_count;
return XTC_OK;
}
/**
* @brief Compiles at 48000 Hz, applies input gain, renders once, and releases temporary state.
*
* @param graph Graph model; borrowed for this call.
* @param input Borrowed input sample buffer.
* @param output Destination buffer with room for the requested frames.
* @param frame_count Number of audio frames.
* @param input_gain Finite linear gain applied to external input before processing.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float *output, size_t frame_count,
float input_gain)
{
xtc_plan *plan = NULL;
xtc_result result;
if (input == NULL || output == NULL || !isfinite(input_gain))
return XTC_ERR_INVALID_ARGUMENT;
result = xtc_graph_compile(graph, 48000.0, frame_count == 0U ? 1U : frame_count, &plan);
if (result != XTC_OK)
return result;
plan->input_gain = input_gain;
result = xtc_plan_process(plan, input, output, frame_count);
xtc_plan_destroy(plan);
return result;
}
/**
* @brief Processes a mono block with external input and no timeline or instrument sources.
*
* @param plan Compiled graph plan and its single-owner runtime state.
* @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_plan_process(xtc_plan *plan, const float *input, float *output, size_t frames)
{
return xtc_plan_process_audio(plan, input, NULL, output, frames);
}
/**
* @brief Processes a mono block with timeline audio and no instrument source.
*
* @param plan Compiled graph plan and its single-owner runtime state.
* @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_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output,
size_t frames)
{
return xtc_plan_process_sources(plan, input, timeline, NULL, output, frames);
}
/**
* @brief Processes a mono block with separate timeline and instrument sources.
*
* @param p Compiled graph plan and its single-owner runtime state.
* @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_plan_process_sources(xtc_plan *p, const float *in, const float *audio, const float *midi, float *out,
size_t frames)
{
return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames);
}
xtc_result xtc_plan_position(xtc_plan *p, uint64_t position)
{ if (!p || position > UINT64_MAX-65536U) return XTC_ERR_INVALID_ARGUMENT; p->position = position; return XTC_OK; }