2026-10-26 23:20:00 +01:00
|
|
|
#include "graph_internal.h"
|
|
|
|
|
#include <math.h>
|
2026-10-26 22:51:25 +01:00
|
|
|
#include <stdlib.h>
|
2026-10-26 23:55:35 +01:00
|
|
|
#include <string.h>
|
2026-10-26 22:51:25 +01:00
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_graph *graph;
|
|
|
|
|
if (out_graph == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-26 22:51:25 +01:00
|
|
|
*out_graph = NULL;
|
2026-10-26 23:20:00 +01:00
|
|
|
if (max_nodes == 0U || max_nodes > XTC_MAX_NODES) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
graph = calloc(1U, sizeof(*graph));
|
|
|
|
|
if (graph == NULL) return XTC_ERR_OUT_OF_MEMORY;
|
|
|
|
|
graph->nodes = calloc(max_nodes, sizeof(*graph->nodes));
|
|
|
|
|
graph->edges = calloc(max_nodes * max_nodes, sizeof(*graph->edges));
|
2026-10-26 22:51:25 +01:00
|
|
|
if (graph->nodes == NULL || graph->edges == NULL) {
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_graph_destroy(graph);
|
|
|
|
|
return XTC_ERR_OUT_OF_MEMORY;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
|
|
|
|
graph->max_nodes = max_nodes;
|
|
|
|
|
*out_graph = graph;
|
2026-10-26 23:20:00 +01:00
|
|
|
return XTC_OK;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
void xtc_graph_destroy(xtc_graph *graph)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
|
|
|
|
if (graph != NULL) {
|
|
|
|
|
free(graph->nodes);
|
|
|
|
|
free(graph->edges);
|
|
|
|
|
free(graph);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 23:55:35 +01:00
|
|
|
static size_t node_index(const xtc_graph *graph, size_t id)
|
|
|
|
|
{
|
|
|
|
|
size_t i;
|
|
|
|
|
for (i = 0U; i < graph->node_count; ++i) if (graph->nodes[i].id == id) return i;
|
|
|
|
|
return SIZE_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
static xtc_result add_node(xtc_graph *graph, xtc_node_type type, float value,
|
|
|
|
|
size_t *out_node_id)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
2026-10-26 23:20:00 +01:00
|
|
|
if (graph == NULL || out_node_id == NULL || !isfinite(value))
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-26 23:55:35 +01:00
|
|
|
if (graph->node_count == graph->max_nodes || graph->next_id == SIZE_MAX) return XTC_ERR_CAPACITY;
|
2026-10-26 23:20:00 +01:00
|
|
|
graph->nodes[graph->node_count].type = type;
|
|
|
|
|
graph->nodes[graph->node_count].value = value;
|
2026-10-26 23:55:35 +01:00
|
|
|
graph->nodes[graph->node_count++].id = graph->next_id;
|
|
|
|
|
*out_node_id = graph->next_id++;
|
2026-10-26 23:20:00 +01:00
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_add_gain(xtc_graph *graph, float gain, size_t *out_node_id)
|
|
|
|
|
{ return add_node(graph, XTC_NODE_GAIN, gain, out_node_id); }
|
|
|
|
|
xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *out_node_id)
|
|
|
|
|
{
|
|
|
|
|
if (frequency < 0.0f) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
return add_node(graph, XTC_NODE_OSCILLATOR, frequency, out_node_id);
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_add_meter(xtc_graph *graph, size_t *out_node_id)
|
|
|
|
|
{ return add_node(graph, XTC_NODE_METER, 0.0f, out_node_id); }
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_graph_add_instrument(xtc_graph *graph, size_t *out_node_id)
|
|
|
|
|
{ return add_node(graph, XTC_NODE_INSTRUMENT, 0.0f, out_node_id); }
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result xtc_graph_add_audio(xtc_graph *graph, size_t *out_node_id)
|
|
|
|
|
{ return add_node(graph, XTC_NODE_AUDIO, 0.0f, out_node_id); }
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_result xtc_graph_add_output(xtc_graph *graph, size_t *out_node_id)
|
|
|
|
|
{ return add_node(graph, XTC_NODE_OUTPUT, 0.0f, out_node_id); }
|
2026-10-26 22:51:25 +01:00
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id,
|
|
|
|
|
size_t port_id, xtc_port_info *out_port)
|
|
|
|
|
{
|
|
|
|
|
xtc_node_type type;
|
|
|
|
|
if (graph == NULL || out_port == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-26 23:55:35 +01:00
|
|
|
node_id = node_index(graph, node_id);
|
|
|
|
|
if (node_id == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
2026-10-26 23:20:00 +01:00
|
|
|
type = graph->nodes[node_id].type;
|
2026-10-27 00:53:41 +01:00
|
|
|
if (port_id > 1U || (port_id == 0U && (type == XTC_NODE_OSCILLATOR || type == XTC_NODE_AUDIO || type == XTC_NODE_INSTRUMENT)) ||
|
2026-10-26 23:20:00 +01:00
|
|
|
(port_id == 1U && type == XTC_NODE_OUTPUT)) return XTC_ERR_NOT_FOUND;
|
|
|
|
|
out_port->direction = port_id == 0U ? XTC_PORT_INPUT : XTC_PORT_OUTPUT;
|
|
|
|
|
out_port->signal_type = XTC_SIGNAL_AUDIO;
|
|
|
|
|
out_port->channel_count = 1U;
|
|
|
|
|
return XTC_OK;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
2026-10-26 23:20:00 +01:00
|
|
|
size_t indegree[XTC_MAX_NODES] = {0U};
|
|
|
|
|
size_t head = 0U, tail = 0U, i;
|
|
|
|
|
if (graph == NULL || order == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
for (i = 0U; i < graph->edge_count; ++i)
|
|
|
|
|
++indegree[graph->edges[i].destination];
|
|
|
|
|
for (i = 0U; i < graph->node_count; ++i)
|
|
|
|
|
if (indegree[i] == 0U) order[tail++] = i;
|
2026-10-26 22:51:25 +01:00
|
|
|
while (head < tail) {
|
2026-10-26 23:20:00 +01:00
|
|
|
size_t node = order[head++];
|
|
|
|
|
for (i = 0U; i < graph->edge_count; ++i) {
|
2026-10-26 22:51:25 +01:00
|
|
|
if (graph->edges[i].source == node) {
|
|
|
|
|
size_t dest = graph->edges[i].destination;
|
2026-10-26 23:20:00 +01:00
|
|
|
if (--indegree[dest] == 0U) order[tail++] = dest;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-26 23:20:00 +01:00
|
|
|
return tail == graph->node_count ? XTC_OK : XTC_ERR_GRAPH_CYCLE;
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 23:20:00 +01:00
|
|
|
xtc_result xtc_graph_validate(const xtc_graph *graph)
|
2026-10-26 22:51:25 +01:00
|
|
|
{
|
2026-10-26 23:20:00 +01:00
|
|
|
size_t order[XTC_MAX_NODES];
|
|
|
|
|
return xtc_graph_order(graph, order);
|
2026-10-26 22:51:25 +01:00
|
|
|
}
|
2026-10-26 23:20:00 +01:00
|
|
|
|
|
|
|
|
xtc_result xtc_graph_connect_ports(xtc_graph *graph, size_t source_node,
|
|
|
|
|
size_t source_port, size_t destination_node,
|
|
|
|
|
size_t destination_port)
|
|
|
|
|
{
|
|
|
|
|
xtc_port_info source, dest;
|
|
|
|
|
size_t i;
|
|
|
|
|
xtc_result result = xtc_graph_get_port(graph, source_node, source_port, &source);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
result = xtc_graph_get_port(graph, destination_node, destination_port, &dest);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
if (source.direction != XTC_PORT_OUTPUT || dest.direction != XTC_PORT_INPUT ||
|
|
|
|
|
source.signal_type != dest.signal_type || source.channel_count != dest.channel_count)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-26 23:55:35 +01:00
|
|
|
source_node = node_index(graph, source_node);
|
|
|
|
|
destination_node = node_index(graph, destination_node);
|
2026-10-26 23:20:00 +01:00
|
|
|
for (i = 0U; i < graph->edge_count; ++i)
|
|
|
|
|
if (graph->edges[i].source == source_node && graph->edges[i].destination == destination_node)
|
|
|
|
|
return XTC_ERR_INVALID_STATE;
|
|
|
|
|
if (graph->edge_count == graph->max_nodes * graph->max_nodes) return XTC_ERR_CAPACITY;
|
|
|
|
|
graph->edges[graph->edge_count].source = source_node;
|
|
|
|
|
graph->edges[graph->edge_count++].destination = destination_node;
|
|
|
|
|
result = xtc_graph_validate(graph);
|
|
|
|
|
if (result != XTC_OK) --graph->edge_count;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_connect(xtc_graph *graph, size_t source_node, size_t destination_node)
|
|
|
|
|
{ return xtc_graph_connect_ports(graph, source_node, 1U, destination_node, 0U); }
|
2026-10-26 23:55:35 +01:00
|
|
|
|
|
|
|
|
xtc_result xtc_graph_get_counts(const xtc_graph *graph, size_t *out_nodes, size_t *out_connections)
|
|
|
|
|
{
|
|
|
|
|
if (graph == NULL || out_nodes == NULL || out_connections == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out_nodes = graph->node_count;
|
|
|
|
|
*out_connections = graph->edge_count;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_get_node(const xtc_graph *graph, size_t index, xtc_node_info *out_node)
|
|
|
|
|
{
|
|
|
|
|
if (graph == NULL || out_node == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (index >= graph->node_count) return XTC_ERR_NOT_FOUND;
|
|
|
|
|
out_node->id = graph->nodes[index].id;
|
|
|
|
|
out_node->type = graph->nodes[index].type;
|
|
|
|
|
out_node->value = graph->nodes[index].value;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_get_connection(const xtc_graph *graph, size_t index, xtc_connection_info *out_connection)
|
|
|
|
|
{
|
|
|
|
|
if (graph == NULL || out_connection == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
if (index >= graph->edge_count) return XTC_ERR_NOT_FOUND;
|
|
|
|
|
out_connection->source_node = graph->nodes[graph->edges[index].source].id;
|
|
|
|
|
out_connection->destination_node = graph->nodes[graph->edges[index].destination].id;
|
|
|
|
|
out_connection->source_port = 1U;
|
|
|
|
|
out_connection->destination_port = 0U;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_require_output_nodes(xtc_graph *graph, int required)
|
|
|
|
|
{
|
|
|
|
|
if (graph == NULL || (required != 0 && required != 1)) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
graph->require_output_nodes = required;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
static xtc_result remove_node(xtc_graph *graph, size_t id)
|
|
|
|
|
{
|
|
|
|
|
size_t index = node_index(graph, id), i, count = 0U;
|
|
|
|
|
if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
|
|
|
|
for (i = 0U; i < graph->edge_count; ++i) {
|
|
|
|
|
xtc_edge edge = graph->edges[i];
|
|
|
|
|
if (edge.source == index || edge.destination == index) continue;
|
|
|
|
|
if (edge.source > index) --edge.source;
|
|
|
|
|
if (edge.destination > index) --edge.destination;
|
|
|
|
|
graph->edges[count++] = edge;
|
|
|
|
|
}
|
|
|
|
|
graph->edge_count = count;
|
|
|
|
|
memmove(graph->nodes + index, graph->nodes + index + 1U,
|
|
|
|
|
(graph->node_count - index - 1U) * sizeof(*graph->nodes));
|
|
|
|
|
--graph->node_count;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
static xtc_result disconnect_ports(xtc_graph *graph, const xtc_connection_info *connection)
|
|
|
|
|
{
|
|
|
|
|
size_t i;
|
|
|
|
|
xtc_port_info port;
|
|
|
|
|
xtc_result result = xtc_graph_get_port(graph, connection->source_node, connection->source_port, &port);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
if (port.direction != XTC_PORT_OUTPUT) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
result = xtc_graph_get_port(graph, connection->destination_node, connection->destination_port, &port);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
if (port.direction != XTC_PORT_INPUT) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
for (i = 0U; i < graph->edge_count; ++i) {
|
|
|
|
|
if (graph->edges[i].source == node_index(graph, connection->source_node) &&
|
|
|
|
|
graph->edges[i].destination == node_index(graph, connection->destination_node)) {
|
|
|
|
|
memmove(graph->edges + i, graph->edges + i + 1U,
|
|
|
|
|
(graph->edge_count - i - 1U) * sizeof(*graph->edges));
|
|
|
|
|
--graph->edge_count;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return XTC_ERR_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit,
|
|
|
|
|
xtc_graph **out_graph, size_t *out_added_id)
|
|
|
|
|
{
|
|
|
|
|
xtc_graph *graph;
|
|
|
|
|
xtc_result result;
|
|
|
|
|
size_t added_id = 0U;
|
|
|
|
|
if (out_graph == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out_graph = NULL;
|
|
|
|
|
if (source == NULL || edit == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
result = xtc_graph_create(source->max_nodes, &graph);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
graph->node_count = source->node_count;
|
|
|
|
|
graph->edge_count = source->edge_count;
|
|
|
|
|
graph->next_id = source->next_id;
|
|
|
|
|
graph->require_output_nodes = source->require_output_nodes;
|
|
|
|
|
memcpy(graph->nodes, source->nodes, source->node_count * sizeof(*graph->nodes));
|
|
|
|
|
memcpy(graph->edges, source->edges, source->edge_count * sizeof(*graph->edges));
|
|
|
|
|
switch (edit->kind) {
|
|
|
|
|
case XTC_EDIT_ADD_NODE:
|
|
|
|
|
switch (edit->data.add.type) {
|
|
|
|
|
case XTC_NODE_GAIN: result = xtc_graph_add_gain(graph, edit->data.add.value, &added_id); break;
|
|
|
|
|
case XTC_NODE_OSCILLATOR: result = xtc_graph_add_oscillator(graph, edit->data.add.value, &added_id); break;
|
|
|
|
|
case XTC_NODE_METER: result = xtc_graph_add_meter(graph, &added_id); break;
|
2026-10-27 00:53:41 +01:00
|
|
|
case XTC_NODE_INSTRUMENT: result = xtc_graph_add_instrument(graph, &added_id); break;
|
2026-10-27 00:31:03 +01:00
|
|
|
case XTC_NODE_AUDIO: result = xtc_graph_add_audio(graph, &added_id); break;
|
2026-10-26 23:55:35 +01:00
|
|
|
case XTC_NODE_OUTPUT: result = xtc_graph_add_output(graph, &added_id); break;
|
|
|
|
|
default: result = XTC_ERR_INVALID_ARGUMENT; break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case XTC_EDIT_REMOVE_NODE: result = remove_node(graph, edit->data.node_id); break;
|
|
|
|
|
case XTC_EDIT_CONNECT:
|
|
|
|
|
result = xtc_graph_connect_ports(graph, edit->data.connection.source_node,
|
|
|
|
|
edit->data.connection.source_port, edit->data.connection.destination_node,
|
|
|
|
|
edit->data.connection.destination_port);
|
|
|
|
|
break;
|
|
|
|
|
case XTC_EDIT_DISCONNECT: result = disconnect_ports(graph, &edit->data.connection); break;
|
|
|
|
|
default: result = XTC_ERR_INVALID_ARGUMENT; break;
|
|
|
|
|
}
|
|
|
|
|
if (result != XTC_OK) { xtc_graph_destroy(graph); return result; }
|
|
|
|
|
if (out_added_id != NULL && edit->kind == XTC_EDIT_ADD_NODE) *out_added_id = added_id;
|
|
|
|
|
*out_graph = graph;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|