2026-10-26 23:20:00 +01:00
|
|
|
#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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void xtc_plan_destroy(xtc_plan *plan)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
2026-10-26 23:20:00 +01:00
|
|
|
if (plan == NULL) return;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-26 23:55:35 +01:00
|
|
|
size_t node, edge, storage_nodes, cursor = 0U;
|
2026-10-26 23:20:00 +01:00
|
|
|
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;
|
2026-10-26 23:55:35 +01:00
|
|
|
storage_nodes = graph->node_count == 0U ? 1U : graph->node_count;
|
2026-10-26 23:20:00 +01:00
|
|
|
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;
|
2026-10-26 23:55:35 +01:00
|
|
|
plan->nodes = calloc(storage_nodes, sizeof(*plan->nodes));
|
|
|
|
|
plan->order = calloc(storage_nodes, sizeof(*plan->order));
|
2026-10-26 23:20:00 +01:00
|
|
|
plan->offsets = calloc(plan->node_count + 1U, sizeof(*plan->offsets));
|
|
|
|
|
plan->sources = calloc(graph->edge_count + 1U, sizeof(*plan->sources));
|
2026-10-26 23:55:35 +01:00
|
|
|
plan->render = calloc(storage_nodes, sizeof(*plan->render));
|
|
|
|
|
plan->buffers = calloc(storage_nodes * max_frames, sizeof(*plan->buffers));
|
|
|
|
|
plan->peaks = calloc(storage_nodes, sizeof(*plan->peaks));
|
|
|
|
|
plan->phases = calloc(storage_nodes, sizeof(*plan->phases));
|
2026-10-26 23:20:00 +01:00
|
|
|
if (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;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
plan->render[node] = (unsigned char)(has_output || graph->require_output_nodes ?
|
2026-10-26 23:20:00 +01:00
|
|
|
plan->nodes[node].type == XTC_NODE_OUTPUT : terminal);
|
|
|
|
|
}
|
|
|
|
|
plan->offsets[plan->node_count] = cursor;
|
|
|
|
|
*out_plan = plan;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result xtc_plan_reset(xtc_plan *plan)
|
|
|
|
|
{
|
|
|
|
|
if (plan == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
memset(plan->phases, 0, plan->node_count * sizeof(*plan->phases));
|
|
|
|
|
memset(plan->peaks, 0, plan->node_count * sizeof(*plan->peaks));
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-26 23:55:35 +01:00
|
|
|
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;
|
2026-10-26 23:20:00 +01:00
|
|
|
if (plan->nodes[node_id].type != XTC_NODE_METER) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out_peak = plan->peaks[node_id];
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output,
|
2026-10-26 23:20:00 +01:00
|
|
|
size_t frame_count)
|
|
|
|
|
{
|
|
|
|
|
size_t index, frame, edge;
|
|
|
|
|
if (plan == NULL || output == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (frame_count > plan->max_frames) 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 (index = 0U; index < plan->node_count; ++index) {
|
|
|
|
|
size_t node = plan->order[index];
|
|
|
|
|
xtc_node spec = plan->nodes[node];
|
|
|
|
|
float *buffer = plan->buffers + node * plan->max_frames;
|
|
|
|
|
float peak = 0.0f;
|
|
|
|
|
double phase = plan->phases[node];
|
|
|
|
|
for (frame = 0U; frame < frame_count; ++frame) {
|
|
|
|
|
float sample = 0.0f;
|
2026-10-27 00:31:03 +01:00
|
|
|
if (spec.type == XTC_NODE_AUDIO) {
|
|
|
|
|
sample = timeline ? timeline[frame] : 0.0f;
|
|
|
|
|
} else if (spec.type == XTC_NODE_OSCILLATOR) {
|
2026-10-26 23:20:00 +01:00
|
|
|
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[plan->sources[edge] * plan->max_frames + frame];
|
|
|
|
|
if (spec.type == XTC_NODE_GAIN) sample *= spec.value;
|
|
|
|
|
}
|
|
|
|
|
if (!isfinite(sample)) sample = 0.0f;
|
|
|
|
|
buffer[frame] = sample;
|
|
|
|
|
if (fabsf(sample) > peak) peak = fabsf(sample);
|
|
|
|
|
}
|
|
|
|
|
plan->phases[node] = phase;
|
|
|
|
|
if (spec.type == XTC_NODE_METER) plan->peaks[node] = peak;
|
|
|
|
|
}
|
|
|
|
|
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[index * plan->max_frames + frame];
|
|
|
|
|
output[frame] = isfinite(sample) ? sample : 0.0f;
|
|
|
|
|
}
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
2026-10-27 00:31:03 +01:00
|
|
|
|
|
|
|
|
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); }
|