#include "xtc/xtc_dsp.h" #include "../graph/graph_internal.h" #include #include #include #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(plan->plugins[channel * plan->node_count + node], spec->plugin.inputs ? buffer : NULL,buffer,frame_count); 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); }