Phase 1 plus some refactoring.

This commit is contained in:
Johannes Findeisen 2026-10-26 23:20:00 +01:00
commit 8075e1b79d
17 changed files with 465 additions and 266 deletions

View file

@ -49,8 +49,12 @@ $(TEST_OBJECT): tests/unit/test_core.c
$(BUILD_DIR)/test_core: $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a -lm
test: $(BUILD_DIR)/test_core $(BUILD_DIR)/xtc-check
$(BUILD_DIR)/test_offline: $(BUILD_DIR)/tests/unit/test_offline.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
test: $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
./$(BUILD_DIR)/test_core
./$(BUILD_DIR)/test_offline
./$(BUILD_DIR)/xtc-check
debug:
@ -75,5 +79,5 @@ install: all
clean:
rm -rf "$(BUILD_DIR)"
-include $(CORE_OBJECTS:.o=.d) $(TEST_OBJECT:.o=.d) $(BUILD_DIR)/src/main.d \
-include $(CORE_OBJECTS:.o=.d) $(TEST_OBJECT:.o=.d) $(BUILD_DIR)/src/main.d $(BUILD_DIR)/tests/unit/test_offline.d \
$(BUILD_DIR)/src/gui/main.d $(BUILD_DIR)/src/gui/main_window.d

View file

@ -14,9 +14,9 @@
This repository is a starter scaffold, not a production DAW. It currently provides:
- a small C library/API;
- a minimal graph model with validation and topological ordering;
- a gain processor and offline audio-block processing;
- a C unit test;
- a mono graph model with port validation, cycle rejection and topological ordering;
- compiled offline plans with preallocated buffers, sine oscillator, gain, meter and output nodes;
- C tests for deterministic rendering, routing, validation and lifecycle;
- an optional FLTK GUI shell;
- architecture, format, GUI, roadmap, and decision documents.

View file

@ -23,9 +23,13 @@ thread: validate, topologically sort, determine buffer requirements, allocate
resources, and publish a prepared immutable execution plan. The audio thread
must not traverse a graph being edited.
The current scaffold is deliberately incomplete: `xt_graph_process_offline()`
validates the graph and applies gain nodes in insertion order. It is a test
placeholder, not the final graph execution algorithm.
The offline engine now copies a validated graph into a prepared plan with
stable node IDs, topological order, precomputed input adjacency, per-node mono
buffers and oscillator/meter state. Each plan has one processing owner. Graph
edits and destruction cannot affect an existing plan. All nodes are scheduled,
including disconnected nodes; only explicit output nodes are rendered (or
terminal nodes when no output node exists). See AUDIO_ENGINE.md for contracts.
Plan publication across threads and retirement remain Phase 2 work.
## Ownership and threads

View file

@ -25,3 +25,57 @@ Audio sample position is the authoritative playback clock. Use integer or
rational timeline representations to avoid cumulative floating-point drift.
Define tempo-map conversion and behavior at tempo changes before implementing
tempo automation.
## Phase 1 offline API
`xtc_graph_create` accepts 1..256 nodes; IDs are insertion indexes, stable for
that graph's lifetime. Supported nodes are gain (any finite multiplier), sine
oscillator (finite nonnegative Hz), pass-through peak meter and audio output.
Ports carry mono float audio: input 0, output 1. Oscillators have no input and
output nodes have no output. Port lookup reports direction, signal type and
channel count. MIDI/control ports and channel conversion are not implemented;
invalid ports/directions, duplicate edges and cycles are rejected before an
edit becomes visible. Fan-out copies a source to consumers; fan-in sums inputs
without normalization. Failed additions leave IDs and graph contents unchanged.
`xtc_graph_compile(graph, sample_rate, max_frames, &plan)` runs on the control
thread. Empty graphs are rejected. Sample rate must be finite and positive,
oscillators must be at or below Nyquist, and max_frames is 1..65536. Compilation
allocates and copies topology/parameters, builds a topological schedule and
input adjacency, and allocates one max_frames float buffer per node. The
maximum buffer storage is 64 MiB per plan. Compilation may fail with an
allocation error; output plan is then NULL. Destroy a plan on the control thread.
`xtc_plan_process` uses only prepared state: no allocation/free, sorting,
logging, I/O or locks. Each call accepts up to max_frames; oversized calls
return capacity errors without modifying output or state. Output storage must
hold frame_count floats. NULL input means silence; input/output may alias.
Zero frames leaves all state unchanged. No plan operations may run concurrently
with processing, including reset, meter reads or destruction.
Unconnected non-oscillator nodes receive external input. Connected inputs
receive only the sum of their upstream buffers. Explicit output nodes are
summed to the caller's output. If none exist, terminal nodes are summed for
compatibility with the original gain-chain API. Disconnected components cannot
contribute when explicit outputs exist, but they are still processed. Floating
point overflow and nonfinite input become silence per sample. No clipping or
normalization is applied to finite samples.
Oscillators start at phase zero and carry phase continuously across blocks;
reset clears phases and meter peaks. Meter reads return the last nonempty
block's absolute peak. These are same-thread reads, not a thread-safe GUI
meter bridge. Rendering is deterministic on the same build/platform regardless
of block partitioning; libm results need not be bit-identical across platforms.
`xtc_graph_process_offline` remains a convenience helper, compiling at 48000 Hz
and destroying the plan after one block. It accepts a finite external-input
gain and requires non-NULL input/output. It now follows routing rather than
multiplying every gain in insertion order. It allocates and is not suitable for
an audio callback. Use a persistent plan for phase continuity and repeated blocks.
Tests in `tests/unit/test_offline.c` cover known oscillator samples, gain and
meter output, reset and partition equivalence, fan-out/fan-in, reverse insertion
order, disconnected nodes, in-place processing, nonfinite input, invalid ports,
cycles, capacity limits, plan lifetime and repeated compilation/cleanup.
Hardware playback, concurrent graph publication and backend stress tests remain
Phase 2 work; this offline API does not claim hardware real-time qualification.

View file

@ -15,7 +15,7 @@ make gui
```
`all` builds the static C library and startup check. `test` runs the existing
core test and startup check. `debug` builds and tests separately in
core and offline graph tests plus the startup check. `debug` builds and tests separately in
`build/debug`. `gui` builds the existing placeholder shell; it does not produce
audio. Generated files remain under `build` and header changes trigger rebuilds.
Compiler, flags, `BUILD_DIR`, `PKG_CONFIG`, `FLTK_CONFIG`, and installation
@ -49,9 +49,8 @@ are changed by these build commands.
## Next milestone
Phase 1 must replace insertion-order gain processing with a validated,
topologically scheduled offline plan, preallocated buffers, oscillator, gain,
meter and output nodes, and deterministic routing tests. The current gain
prototype is not a real-time engine. Public project persistence declarations
also remain unimplemented. Hardware playback and interactive GUI behavior are
outside this baseline.
Phase 1 implements compiled offline plans, preallocated buffers, oscillator,
gain, meter/output nodes and routing tests; see AUDIO_ENGINE.md. Phase 2 adds
a hardware backend and safe cross-thread plan publication/retirement. Public
project persistence declarations remain unimplemented. Hardware playback and
interactive GUI behavior are not verified by the offline tests.

View file

@ -229,23 +229,23 @@ Example API shape (illustrative only; refine after reviewing the actual codebase
extern "C" {
#endif
typedef struct xt_app xt_app;
typedef struct xtc_app xtc_app;
typedef enum {
XT_OK = 0,
XT_ERR_INVALID_ARGUMENT,
XT_ERR_OUT_OF_MEMORY,
XT_ERR_NOT_FOUND,
XT_ERR_INVALID_STATE,
XT_ERR_IO,
XT_ERR_UNSUPPORTED
} xt_result;
XTC_OK = 0,
XTC_ERR_INVALID_ARGUMENT,
XTC_ERR_OUT_OF_MEMORY,
XTC_ERR_NOT_FOUND,
XTC_ERR_INVALID_STATE,
XTC_ERR_IO,
XTC_ERR_UNSUPPORTED
} xtc_result;
xt_result xt_app_create(xt_app **out_app);
void xt_app_destroy(xt_app *app);
xtc_result xtc_app_create(xtc_app **out_app);
void xtc_app_destroy(xtc_app *app);
xt_result xt_transport_play(xt_app *app);
xt_result xt_transport_stop(xt_app *app);
xtc_result xtc_transport_play(xtc_app *app);
xtc_result xtc_transport_stop(xtc_app *app);
#ifdef __cplusplus
}

View file

@ -12,6 +12,11 @@ See [BUILD.md](BUILD.md) for toolchain, commands, licensing and limitations.
Graph data structures, validation, cycle detection, topological ordering,
preallocated buffers, test oscillator, gain and offline render tests.
Implemented: mono ports, transactional cycle rejection, independent compiled
plans, precomputed input routing, oscillator/gain/meter/output, deterministic
variable-block tests and lifecycle/error coverage. See AUDIO_ENGINE.md for API
limits and compatibility behavior. Cross-thread publication remains Phase 2.
## Phase 2 — Real-time backend
Choose and implement a Linux backend. Document callback semantics, start/stop,
device errors, shutdown, and safe graph publication.

View file

@ -11,13 +11,13 @@ extern "C" {
#define XTC_VERSION_MINOR 1
#define XTC_VERSION_PATCH 0
typedef struct xt_app xt_app;
typedef struct xtc_app xtc_app;
/* Creates an application context. On success, *out_app is caller-owned. */
xt_result xt_app_create(xt_app **out_app);
xtc_result xtc_app_create(xtc_app **out_app);
/* Destroys an application context; NULL is accepted. */
void xt_app_destroy(xt_app *app);
void xtc_app_destroy(xtc_app *app);
#ifdef __cplusplus
}

View file

@ -7,33 +7,81 @@
extern "C" {
#endif
typedef struct xt_graph xt_graph;
typedef struct xtc_graph xtc_graph;
typedef enum {
XT_SIGNAL_AUDIO = 1,
XT_SIGNAL_EVENT = 2,
XT_SIGNAL_CONTROL = 3
} xt_signal_type;
XTC_SIGNAL_AUDIO = 1,
XTC_SIGNAL_EVENT = 2,
XTC_SIGNAL_CONTROL = 3
} xtc_signal_type;
typedef enum {
XT_PORT_INPUT = 1,
XT_PORT_OUTPUT = 2
} xt_port_direction;
XTC_PORT_INPUT = 1,
XTC_PORT_OUTPUT = 2
} xtc_port_direction;
typedef struct {
float *samples;
size_t frame_count;
size_t channel_count;
} xt_audio_buffer;
} xtc_audio_buffer;
/* Graph API is intentionally small in this first scaffold. */
xt_result xt_graph_create(size_t max_nodes, xt_graph **out_graph);
void xt_graph_destroy(xt_graph *graph);
xt_result xt_graph_add_gain(xt_graph *graph, float gain, size_t *out_node_id);
xt_result xt_graph_connect(xt_graph *graph, size_t source_node,
typedef struct xtc_plan xtc_plan;
typedef enum {
XTC_NODE_GAIN = 1,
XTC_NODE_OSCILLATOR,
XTC_NODE_METER,
XTC_NODE_OUTPUT
} xtc_node_type;
typedef struct {
xtc_port_direction direction;
xtc_signal_type signal_type;
size_t channel_count;
} xtc_port_info;
/* Control-thread API. Limits: 256 nodes, mono audio only. IDs are stable
* insertion indexes. Output parameters are written only on success, except
* create/compile which set their output pointer to NULL on failure. */
xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph);
void xtc_graph_destroy(xtc_graph *graph);
xtc_result xtc_graph_add_gain(xtc_graph *graph, float gain, size_t *out_node_id);
xtc_result xtc_graph_connect(xtc_graph *graph, size_t source_node,
size_t destination_node);
xt_result xt_graph_validate(const xt_graph *graph);
xt_result xt_graph_process_offline(xt_graph *graph, const float *input,
/* Port 0 is input; port 1 is output. Oscillators have only output 1;
* output nodes have only input 0. All ports carry mono audio. */
xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id,
size_t port_id, xtc_port_info *out_port);
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_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency,
size_t *out_node_id);
xtc_result xtc_graph_add_meter(xtc_graph *graph, size_t *out_node_id);
xtc_result xtc_graph_add_output(xtc_graph *graph, size_t *out_node_id);
/* Compile copies graph state; caller owns the plan. Sample rate must be
* finite and positive, oscillator frequencies <= Nyquist. max_frames: 1..65536.
* Editing/destroying the graph never changes an existing plan. */
xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate,
size_t max_frames, xtc_plan **out_plan);
void xtc_plan_destroy(xtc_plan *plan);
/* Single processing owner, no concurrent calls (including meter reads/reset).
* No allocation, I/O, locking or graph validation. NULL input means silence.
* output must hold frame_count floats; input may equal output. On invalid
* arguments output and DSP state are unchanged. Zero frames is a no-op.
* Sums explicit output nodes; if absent, sums terminal nodes for compatibility.
* Unconnected gain/meter/output inputs receive external input. */
xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output,
size_t frame_count);
/* Resets oscillator phases and meters, without allocation. */
xtc_result xtc_plan_reset(xtc_plan *plan);
/* Last block absolute peak, valid for meter nodes only. */
xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_peak);
xtc_result xtc_graph_validate(const xtc_graph *graph);
/* Allocating compatibility helper: compiles at 48000 Hz and processes once.
* Requires non-NULL input/output and finite input_gain. Not for callbacks. */
xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input,
float *output, size_t frame_count,
float input_gain);

View file

@ -7,13 +7,13 @@
extern "C" {
#endif
typedef struct xt_project xt_project;
typedef struct xtc_project xtc_project;
/* Project persistence is not implemented in this scaffold yet. */
xt_result xt_project_create(xt_project **out_project);
void xt_project_destroy(xt_project *project);
xt_result xt_project_save(const xt_project *project, const char *path);
xt_result xt_project_load(xt_project *project, const char *path);
xtc_result xtc_project_create(xtc_project **out_project);
void xtc_project_destroy(xtc_project *project);
xtc_result xtc_project_save(const xtc_project *project, const char *path);
xtc_result xtc_project_load(xtc_project *project, const char *path);
#ifdef __cplusplus
}

View file

@ -9,18 +9,18 @@ extern "C" {
#endif
typedef enum {
XT_OK = 0,
XT_ERR_INVALID_ARGUMENT,
XT_ERR_OUT_OF_MEMORY,
XT_ERR_NOT_FOUND,
XT_ERR_INVALID_STATE,
XT_ERR_IO,
XT_ERR_UNSUPPORTED,
XT_ERR_CAPACITY,
XT_ERR_GRAPH_CYCLE
} xt_result;
XTC_OK = 0,
XTC_ERR_INVALID_ARGUMENT,
XTC_ERR_OUT_OF_MEMORY,
XTC_ERR_NOT_FOUND,
XTC_ERR_INVALID_STATE,
XTC_ERR_IO,
XTC_ERR_UNSUPPORTED,
XTC_ERR_CAPACITY,
XTC_ERR_GRAPH_CYCLE
} xtc_result;
const char *xt_result_string(xt_result result);
const char *xtc_result_string(xtc_result result);
#ifdef __cplusplus
}

View file

@ -2,45 +2,45 @@
#include <stdlib.h>
struct xt_app {
struct xtc_app {
unsigned int reserved;
};
const char *xt_result_string(xt_result result)
const char *xtc_result_string(xtc_result result)
{
switch (result) {
case XT_OK: return "success";
case XT_ERR_INVALID_ARGUMENT: return "invalid argument";
case XT_ERR_OUT_OF_MEMORY: return "out of memory";
case XT_ERR_NOT_FOUND: return "not found";
case XT_ERR_INVALID_STATE: return "invalid state";
case XT_ERR_IO: return "I/O error";
case XT_ERR_UNSUPPORTED: return "unsupported operation";
case XT_ERR_CAPACITY: return "capacity exceeded";
case XT_ERR_GRAPH_CYCLE: return "graph contains a cycle";
case XTC_OK: return "success";
case XTC_ERR_INVALID_ARGUMENT: return "invalid argument";
case XTC_ERR_OUT_OF_MEMORY: return "out of memory";
case XTC_ERR_NOT_FOUND: return "not found";
case XTC_ERR_INVALID_STATE: return "invalid state";
case XTC_ERR_IO: return "I/O error";
case XTC_ERR_UNSUPPORTED: return "unsupported operation";
case XTC_ERR_CAPACITY: return "capacity exceeded";
case XTC_ERR_GRAPH_CYCLE: return "graph contains a cycle";
default: return "unknown error";
}
}
xt_result xt_app_create(xt_app **out_app)
xtc_result xtc_app_create(xtc_app **out_app)
{
xt_app *app;
xtc_app *app;
if (out_app == NULL) {
return XT_ERR_INVALID_ARGUMENT;
return XTC_ERR_INVALID_ARGUMENT;
}
*out_app = NULL;
app = (xt_app *)calloc(1U, sizeof(*app));
app = (xtc_app *)calloc(1U, sizeof(*app));
if (app == NULL) {
return XT_ERR_OUT_OF_MEMORY;
return XTC_ERR_OUT_OF_MEMORY;
}
*out_app = app;
return XT_OK;
return XTC_OK;
}
void xt_app_destroy(xt_app *app)
void xtc_app_destroy(xtc_app *app)
{
free(app);
}

View file

@ -1,7 +1,7 @@
#include <stddef.h>
/* Kept as a separately testable DSP primitive. */
void xt_dsp_gain_process(const float *input, float *output,
void xtc_dsp_gain_process(const float *input, float *output,
size_t frame_count, float gain)
{
size_t frame;

View file

@ -1,13 +1,165 @@
/*
* Offline engine integration point.
*
* The first scaffold exposes xt_graph_process_offline() from graph.c. This
* file is reserved for the next milestone: a compiled immutable graph plan,
* preallocated node buffers, and block-based processing independent of audio
* hardware. Keep hardware callbacks and real-time constraints out of this
* initial offline implementation.
*/
int xt_offline_engine_translation_unit_anchor(void)
#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)
{
return 0;
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;
size_t node, edge, 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;
if (graph->node_count == 0U) return XTC_ERR_INVALID_STATE;
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->nodes = calloc(plan->node_count, sizeof(*plan->nodes));
plan->order = calloc(plan->node_count, 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(plan->node_count, sizeof(*plan->render));
plan->buffers = calloc(plan->node_count * max_frames, sizeof(*plan->buffers));
plan->peaks = calloc(plan->node_count, sizeof(*plan->peaks));
plan->phases = calloc(plan->node_count, sizeof(*plan->phases));
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;
}
plan->render[node] = (unsigned char)(has_output ?
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;
if (node_id >= plan->node_count) return XTC_ERR_NOT_FOUND;
if (plan->nodes[node_id].type != XTC_NODE_METER) return XTC_ERR_INVALID_ARGUMENT;
*out_peak = plan->peaks[node_id];
return XTC_OK;
}
xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output,
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;
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[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;
}

View file

@ -1,54 +1,27 @@
#include "xtc/xtc_engine.h"
#include "graph_internal.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t source;
size_t destination;
} xt_edge;
typedef struct {
float gain;
} xt_gain_node;
struct xt_graph {
size_t max_nodes;
size_t node_count;
size_t edge_count;
xt_gain_node *nodes;
xt_edge *edges;
};
xt_result xt_graph_create(size_t max_nodes, xt_graph **out_graph)
xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph)
{
xt_graph *graph;
if (out_graph == NULL || max_nodes == 0U) {
return XT_ERR_INVALID_ARGUMENT;
}
xtc_graph *graph;
if (out_graph == NULL) return XTC_ERR_INVALID_ARGUMENT;
*out_graph = NULL;
graph = (xt_graph *)calloc(1U, sizeof(*graph));
if (graph == NULL) {
return XT_ERR_OUT_OF_MEMORY;
}
graph->nodes = (xt_gain_node *)calloc(max_nodes, sizeof(*graph->nodes));
graph->edges = (xt_edge *)calloc(max_nodes * max_nodes, sizeof(*graph->edges));
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));
if (graph->nodes == NULL || graph->edges == NULL) {
free(graph->nodes);
free(graph->edges);
free(graph);
return XT_ERR_OUT_OF_MEMORY;
xtc_graph_destroy(graph);
return XTC_ERR_OUT_OF_MEMORY;
}
graph->max_nodes = max_nodes;
*out_graph = graph;
return XT_OK;
return XTC_OK;
}
void xt_graph_destroy(xt_graph *graph)
void xtc_graph_destroy(xtc_graph *graph)
{
if (graph != NULL) {
free(graph->nodes);
@ -57,133 +30,93 @@ void xt_graph_destroy(xt_graph *graph)
}
}
xt_result xt_graph_add_gain(xt_graph *graph, float gain, size_t *out_node_id)
static xtc_result add_node(xtc_graph *graph, xtc_node_type type, float value,
size_t *out_node_id)
{
if (graph == NULL || out_node_id == NULL) {
return XT_ERR_INVALID_ARGUMENT;
}
if (graph->node_count >= graph->max_nodes) {
return XT_ERR_CAPACITY;
}
if (graph == NULL || out_node_id == NULL || !isfinite(value))
return XTC_ERR_INVALID_ARGUMENT;
if (graph->node_count == graph->max_nodes) return XTC_ERR_CAPACITY;
graph->nodes[graph->node_count].type = type;
graph->nodes[graph->node_count].value = value;
*out_node_id = graph->node_count++;
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); }
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); }
graph->nodes[graph->node_count].gain = gain;
*out_node_id = graph->node_count;
graph->node_count++;
return XT_OK;
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;
if (node_id >= graph->node_count) return XTC_ERR_NOT_FOUND;
type = graph->nodes[node_id].type;
if (port_id > 1U || (port_id == 0U && type == XTC_NODE_OSCILLATOR) ||
(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;
}
xt_result xt_graph_connect(xt_graph *graph, size_t source_node,
size_t destination_node)
xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order)
{
size_t i;
size_t edge_capacity;
if (graph == NULL) {
return XT_ERR_INVALID_ARGUMENT;
}
if (source_node >= graph->node_count || destination_node >= graph->node_count) {
return XT_ERR_NOT_FOUND;
}
edge_capacity = graph->max_nodes * graph->max_nodes;
if (graph->edge_count >= edge_capacity) {
return XT_ERR_CAPACITY;
}
for (i = 0U; i < graph->edge_count; i++) {
if (graph->edges[i].source == source_node &&
graph->edges[i].destination == destination_node) {
return XT_ERR_INVALID_STATE;
}
}
graph->edges[graph->edge_count].source = source_node;
graph->edges[graph->edge_count].destination = destination_node;
graph->edge_count++;
return XT_OK;
}
xt_result xt_graph_validate(const xt_graph *graph)
{
size_t *indegree;
size_t *queue;
size_t head = 0U;
size_t tail = 0U;
size_t visited = 0U;
size_t i;
if (graph == NULL) {
return XT_ERR_INVALID_ARGUMENT;
}
if (graph->node_count == 0U) {
return XT_OK;
}
indegree = (size_t *)calloc(graph->node_count, sizeof(*indegree));
queue = (size_t *)calloc(graph->node_count, sizeof(*queue));
if (indegree == NULL || queue == NULL) {
free(indegree);
free(queue);
return XT_ERR_OUT_OF_MEMORY;
}
for (i = 0U; i < graph->edge_count; i++) {
if (graph->edges[i].source >= graph->node_count ||
graph->edges[i].destination >= graph->node_count) {
free(indegree);
free(queue);
return XT_ERR_INVALID_STATE;
}
indegree[graph->edges[i].destination]++;
}
for (i = 0U; i < graph->node_count; i++) {
if (indegree[i] == 0U) {
queue[tail++] = i;
}
}
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;
while (head < tail) {
size_t node = queue[head++];
visited++;
for (i = 0U; i < graph->edge_count; i++) {
size_t node = order[head++];
for (i = 0U; i < graph->edge_count; ++i) {
if (graph->edges[i].source == node) {
size_t dest = graph->edges[i].destination;
if (indegree[dest] > 0U) {
indegree[dest]--;
if (indegree[dest] == 0U) {
queue[tail++] = dest;
}
}
if (--indegree[dest] == 0U) order[tail++] = dest;
}
}
}
free(indegree);
free(queue);
return visited == graph->node_count ? XT_OK : XT_ERR_GRAPH_CYCLE;
return tail == graph->node_count ? XTC_OK : XTC_ERR_GRAPH_CYCLE;
}
xt_result xt_graph_process_offline(xt_graph *graph, const float *input,
float *output, size_t frame_count,
float input_gain)
xtc_result xtc_graph_validate(const xtc_graph *graph)
{
size_t i;
float cumulative_gain = input_gain;
if (graph == NULL || input == NULL || output == NULL) {
return XT_ERR_INVALID_ARGUMENT;
}
if (xt_graph_validate(graph) != XT_OK) {
return XT_ERR_INVALID_STATE;
}
/* Temporary starter behavior: apply gains in node insertion order.
This is not yet graph-plan execution and is only for offline tests. */
for (i = 0U; i < graph->node_count; i++) {
cumulative_gain *= graph->nodes[i].gain;
}
for (i = 0U; i < frame_count; i++) {
output[i] = input[i] * cumulative_gain;
}
return XT_OK;
size_t order[XTC_MAX_NODES];
return xtc_graph_order(graph, order);
}
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;
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); }

View file

@ -6,16 +6,16 @@
int main(void)
{
xt_app *app = NULL;
xt_result result = xt_app_create(&app);
xtc_app *app = NULL;
xtc_result result = xtc_app_create(&app);
if (result != XT_OK) {
if (result != XTC_OK) {
(void)fprintf(stderr, "Application initialization failed: %s\n",
xt_result_string(result));
xtc_result_string(result));
return EXIT_FAILURE;
}
xt_app_destroy(app);
xtc_app_destroy(app);
if (printf("%s %s: core startup/shutdown OK\n",
XTC_PROJECT_NAME, XTC_VERSION_STRING) < 0) {
return EXIT_FAILURE;

View file

@ -13,31 +13,31 @@
int main(void)
{
xt_app *app = NULL;
xt_graph *graph = NULL;
xtc_app *app = NULL;
xtc_graph *graph = NULL;
size_t node_a = 0U;
size_t node_b = 0U;
float input[4] = {1.0f, -1.0f, 0.5f, 0.0f};
float output[4] = {0.0f, 0.0f, 0.0f, 0.0f};
CHECK(xt_app_create(&app) == XT_OK);
CHECK(xtc_app_create(&app) == XTC_OK);
CHECK(app != NULL);
xt_app_destroy(app);
xtc_app_destroy(app);
CHECK(xt_graph_create(4U, &graph) == XT_OK);
CHECK(xt_graph_add_gain(graph, 0.5f, &node_a) == XT_OK);
CHECK(xt_graph_add_gain(graph, 2.0f, &node_b) == XT_OK);
CHECK(xt_graph_connect(graph, node_a, node_b) == XT_OK);
CHECK(xt_graph_validate(graph) == XT_OK);
CHECK(xt_graph_process_offline(graph, input, output, 4U, 1.0f) == XT_OK);
CHECK(xtc_graph_create(4U, &graph) == XTC_OK);
CHECK(xtc_graph_add_gain(graph, 0.5f, &node_a) == XTC_OK);
CHECK(xtc_graph_add_gain(graph, 2.0f, &node_b) == XTC_OK);
CHECK(xtc_graph_connect(graph, node_a, node_b) == XTC_OK);
CHECK(xtc_graph_validate(graph) == XTC_OK);
CHECK(xtc_graph_process_offline(graph, input, output, 4U, 1.0f) == XTC_OK);
CHECK(fabsf(output[0] - 1.0f) < 0.0001f);
CHECK(fabsf(output[1] + 1.0f) < 0.0001f);
CHECK(fabsf(output[2] - 0.5f) < 0.0001f);
CHECK(xt_graph_connect(graph, node_b, node_a) == XT_OK);
CHECK(xt_graph_validate(graph) == XT_ERR_GRAPH_CYCLE);
CHECK(xtc_graph_connect(graph, node_b, node_a) == XTC_ERR_GRAPH_CYCLE);
CHECK(xtc_graph_validate(graph) == XTC_OK);
xt_graph_destroy(graph);
xtc_graph_destroy(graph);
(void)puts("All xtc core tests passed.");
return 0;
}