diff --git a/Makefile b/Makefile index b633a54..31dfb71 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 35b8b0d..275a968 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b75c9ec..808e7fc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/AUDIO_ENGINE.md b/docs/AUDIO_ENGINE.md index 7222c43..826c6b1 100644 --- a/docs/AUDIO_ENGINE.md +++ b/docs/AUDIO_ENGINE.md @@ -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. diff --git a/docs/BUILD.md b/docs/BUILD.md index 81066dd..ac2ecf9 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -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. diff --git a/docs/IDEA.md b/docs/IDEA.md index dc5902d..36c4059 100644 --- a/docs/IDEA.md +++ b/docs/IDEA.md @@ -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 } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a5655c4..12d94f6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. diff --git a/include/xtc/xtc.h b/include/xtc/xtc.h index 551b90b..775b726 100644 --- a/include/xtc/xtc.h +++ b/include/xtc/xtc.h @@ -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 } diff --git a/include/xtc/xtc_engine.h b/include/xtc/xtc_engine.h index e5023a5..481599c 100644 --- a/include/xtc/xtc_engine.h +++ b/include/xtc/xtc_engine.h @@ -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); diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index c7d371b..fdcd0d4 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -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 } diff --git a/include/xtc/xtc_types.h b/include/xtc/xtc_types.h index 42298eb..fdf828a 100644 --- a/include/xtc/xtc_types.h +++ b/include/xtc/xtc_types.h @@ -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 } diff --git a/src/core/xtc.c b/src/core/xtc.c index 93a3d51..0f1b579 100644 --- a/src/core/xtc.c +++ b/src/core/xtc.c @@ -2,45 +2,45 @@ #include -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); } diff --git a/src/dsp/gain.c b/src/dsp/gain.c index 635b58e..08640da 100644 --- a/src/dsp/gain.c +++ b/src/dsp/gain.c @@ -1,7 +1,7 @@ #include /* 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; diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c index 8197ca1..b708f59 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -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 +#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; +}; + +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; } diff --git a/src/graph/graph.c b/src/graph/graph.c index 4ac8fea..3e0ca6e 100644 --- a/src/graph/graph.c +++ b/src/graph/graph.c @@ -1,54 +1,27 @@ -#include "xtc/xtc_engine.h" - +#include "graph_internal.h" +#include #include -#include -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); } diff --git a/src/main.c b/src/main.c index f3b3a1a..16dc3aa 100644 --- a/src/main.c +++ b/src/main.c @@ -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; diff --git a/tests/unit/test_core.c b/tests/unit/test_core.c index cad4b67..dea727f 100644 --- a/tests/unit/test_core.c +++ b/tests/unit/test_core.c @@ -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; }