1
0
Fork 0
forked from hanez/xtc
This commit is contained in:
Johannes Findeisen 2026-10-26 23:32:04 +01:00
commit 577d80f018
16 changed files with 1100 additions and 41 deletions

View file

@ -1,3 +1,5 @@
.DEFAULT_GOAL := all
CC ?= cc
CXX ?= c++
PKG_CONFIG ?= pkg-config
@ -11,7 +13,15 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/core/xtc.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c
CORE_SOURCES := src/core/xtc.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
JACK_LIBS := $(shell $(PKG_CONFIG) --libs jack)
BACKEND_TEST := $(BUILD_DIR)/test_backend
$(BUILD_DIR)/tests/unit/test_backend.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/src/backend/jack_backend.o: CPPFLAGS += -DXTC_HAVE_JACK $(JACK_CFLAGS)
endif
CORE_OBJECTS := $(CORE_SOURCES:%.c=$(BUILD_DIR)/%.o)
TEST_OBJECT := $(BUILD_DIR)/tests/unit/test_core.o
ifeq ($(shell $(PKG_CONFIG) --exists fltk 2>/dev/null && echo yes),yes)
@ -24,7 +34,7 @@ FLTK_LIBS := $(shell $(FLTK_CONFIG) --ldflags)
GUI_AVAILABLE := yes
endif
.PHONY: all clean test debug gui install
.PHONY: all clean test test-jack debug gui install
all: $(BUILD_DIR)/libxtc.a $(BUILD_DIR)/xtc-check
$(BUILD_DIR)/xtc-check: $(BUILD_DIR)/src/main.o $(BUILD_DIR)/libxtc.a
@ -36,15 +46,15 @@ $(BUILD_DIR)/libxtc.a: $(CORE_OBJECTS)
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD_DIR)/%.o: %.cpp
@mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(FLTK_CXXFLAGS) -MMD -MP -c $< -o $@
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(FLTK_CXXFLAGS) -MMD -MP -c $< -o $@
$(TEST_OBJECT): tests/unit/test_core.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD_DIR)/test_core: $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a -lm
@ -52,7 +62,15 @@ $(BUILD_DIR)/test_core: $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a
$(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_realtime: $(BUILD_DIR)/tests/unit/test_realtime.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -pthread
$(BUILD_DIR)/test_backend: $(BUILD_DIR)/tests/unit/test_backend.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
test: $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
$(if $(BACKEND_TEST),./$(BACKEND_TEST),@echo "JACK fault tests skipped: JACK development files unavailable")
./$(BUILD_DIR)/test_realtime
./$(BUILD_DIR)/test_core
./$(BUILD_DIR)/test_offline
./$(BUILD_DIR)/xtc-check
@ -60,6 +78,19 @@ test: $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
debug:
$(MAKE) BUILD_DIR="$(BUILD_DIR)/debug" CFLAGS="$(CFLAGS) -O0 -g3" CXXFLAGS="$(CXXFLAGS) -O0 -g3" all test
ifeq ($(JACK_AVAILABLE),yes)
$(BUILD_DIR)/tests/integration/test_jack.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/test_jack: $(BUILD_DIR)/tests/integration/test_jack.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm
test-jack: $(BUILD_DIR)/test_jack
./$(BUILD_DIR)/test_jack
else
test-jack:
@echo "JACK development files are required for the live integration test."
@exit 1
endif
ifeq ($(GUI_AVAILABLE),yes)
gui: $(BUILD_DIR)/xtc
@ -79,5 +110,5 @@ install: all
clean:
rm -rf "$(BUILD_DIR)"
-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
-include $(CORE_OBJECTS:.o=.d) $(TEST_OBJECT:.o=.d) $(BUILD_DIR)/src/main.d $(BUILD_DIR)/tests/unit/test_offline.d $(BUILD_DIR)/tests/unit/test_realtime.d $(BUILD_DIR)/tests/unit/test_backend.d \
$(BUILD_DIR)/src/gui/main.d $(BUILD_DIR)/src/gui/main_window.d $(BUILD_DIR)/tests/integration/test_jack.d

View file

@ -17,10 +17,14 @@ This repository is a starter scaffold, not a production DAW. It currently provid
- 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 JACK-compatible mono backend with start/stop, routing and status;
- safe control/audio plan handoff and concurrent replacement tests;
- an optional FLTK GUI shell;
- architecture, format, GUI, roadmap, and decision documents.
Real audio-device integration, recording, MIDI, plugin hosting, project persistence, and the actual sequencer UI are not implemented yet.
The JACK backend has passed software-loopback testing; physical-device playback
is unverified. Recording, MIDI, plugin hosting, project persistence, and the
actual sequencer UI are not implemented yet.
## Build
@ -47,7 +51,11 @@ Run the GUI:
./build/xtc
```
The current GUI is only a shell and does not yet produce sound. The core's offline graph test is the first executable audio-processing milestone.
The current GUI is only a shell and is not connected to the backend yet.
With JACK development files and an existing JACK-compatible server, run the
isolated software-loopback test with `make test-jack`. It connects only test
clients. See [engine documentation](docs/AUDIO_ENGINE.md) for configuration,
routing, ownership and recovery.
Clean generated files:

View file

@ -29,7 +29,9 @@ 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.
`xtc_realtime` now provides a one-pending/one-retired handoff around an
exclusively audio-owned active plan. `xtc_backend` is the C backend boundary;
the optional JACK implementation drives that same realtime engine.
## Ownership and threads
@ -38,5 +40,6 @@ Plan publication across threads and retirement remain Phase 2 work.
- Audio thread owns the active processing plan and DSP state.
- Backend callbacks follow the backend's documented threading contract.
Do not free an active plan until the audio thread has released it. Pick and
document a bounded handoff/reclamation strategy before real-time graph swaps.
Only acknowledged retired plans are reclaimed during processing. Backend close
synchronizes callbacks before reclaiming remaining plans. See decision 0004
for the release/acquire protocol, bounded resource retention and recovery.

View file

@ -77,5 +77,59 @@ 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.
The offline API remains single-owner. The realtime/backend APIs below add
publication and JACK processing; offline tests alone do not qualify hardware.
## Phase 2 backend and realtime bridge
Use `xtc_backend.h` on one serialized control thread:
1. Open a config with `XTC_BACKEND_JACK`, a client name, channel_count 1, and
max_frames 1..65536. It must accommodate the server's current buffer size.
The actual rate and buffer size are returned through get_status. Open does
not start the server or acquire devices directly. Invalid config returns
invalid argument, missing build support returns unsupported, and JACK setup
failures return I/O error. If cleanup itself fails, a non-NULL output handle
is preserved for close retry; no other operation is allowed on that handle.
2. Submit a graph (copied/compiled at the server rate), then start. Starting
an already running client is an invalid-state error; stopping a stopped
client succeeds. With no submitted graph, callbacks produce silence.
3. Enumerate available audio ports with visit_ports. Names are borrowed only
during the visitor; do not reenter backend operations from the visitor.
Connect the client's input to a remote output or its output to a remote
input explicitly. JACK maps device selection to server/device port routes;
ALSA device identifiers are not accepted here. The server controls rate,
period and hardware configuration outside this API. Names/ports can disappear
between enumeration and connection; route failures are returned as I/O errors.
4. During playback, submit edited graphs and periodically collect retired
resources. Capacity means one pending graph is still awaiting adoption;
keep the current graph and retry later. get_status is a nonblocking snapshot
of rate, frames, running/server-loss/rate-change flags, xruns, processed
blocks and last process result. No active DSP memory is exposed.
5. Stop pauses the retained DSP state after deactivation. Restart resumes it.
Close removes the JACK client, synchronizes all callbacks, then destroys
plans. Stop/close failures preserve the handle for recovery/retry. No plan
is freed on the audio thread. Sample-rate change or server loss requires
close/reopen and a fresh submission; there is no automatic reconnect.
Buffers can change size. In-limit changes process normally; oversized blocks
are silent and report capacity errors until the size is supported again.
Rate changes latch silence and invalid-state processing results. A missing input
buffer is treated as unavailable input (silence). A missing output buffer is
reported as an I/O process error. Shutdown callbacks only set the loss flag;
the control thread closes the client. No JACK or GUI types cross the C API.
The standalone `xtc_realtime.h` API allows hardware-independent consumers and
stress tests. Submit compiles on the producer; process adopts at a block
boundary without allocation/free, I/O, locks or waits. All process calls must
come from one consumer thread, and destruction requires that consumer stopped
and joined. Release/acquire publication, retirement backpressure and atomic
requirements are specified in decision 0004. Ordinary project processing and
JACK notification callbacks were audited for prohibited operations; JACK's own
internal realtime guarantees depend on the installed implementation/server.
Validation commands and results are recorded in BUILD.md and the integration
test README. Current limitations: mono input/output, no parameter ramping or
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
no physical-device or long-duration endurance qualification. GUI controls and
status display are Phase 3; the GUI remains a placeholder shell.

View file

@ -15,7 +15,8 @@ make gui
```
`all` builds the static C library and startup check. `test` runs the existing
core and offline graph tests plus the startup check. `debug` builds and tests separately in
core/offline tests, concurrent plan-handoff stress, JACK fault injection
(when JACK headers are available), and 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
@ -29,7 +30,7 @@ Compiler, flags, `BUILD_DIR`, `PKG_CONFIG`, `FLTK_CONFIG`, and installation
| GNU Make | 4.4.1 | Build and test driver |
| pkg-config | 3.0.10 | Development package discovery |
| FLTK | 1.4.5 | Optional GUI; discovered through `fltk-config` |
| JACK | 3.1609.0 (pkg-config) | Candidate backend; not linked yet |
| JACK | 3.1609.0 (pkg-config) | Optional JACK backend (PipeWire compatibility library) |
| ALSA | 1.2.16.1 (pkg-config) | Future MIDI candidate; not linked yet |
| Clang | 23.1.3 | Alternative compiler / diagnostics |
@ -39,18 +40,44 @@ the Makefile tries pkg-config first, then `fltk-config`.
## Dependencies and licensing
No new dependencies are introduced. The core needs the C runtime and math
library. The repository is MIT licensed. The optional GUI uses maintained
The offline core needs the C runtime and math library. Phase 2 adds optional
JACK support, selected with pkg-config; missing JACK does not prevent core/tests
from building. Realtime publication uses GCC/Clang lock-free atomic builtins in
C99. Tests also use POSIX threads. Backend consumers must link JACK explicitly;
backend objects are pulled from the static library only when used. The repository is MIT licensed. The optional GUI uses maintained
FLTK 1.4 (LGPL with FLTK's static-linking exception); its platform link flags
come from the installed package. Distribution still requires review of all
linked dependencies and notices. JACK and ALSA remain candidates only; backend
selection and license review belong to Phase 2. No packages or system settings
linked dependencies and notices. The installed PipeWire JACK library is MIT;
traditional JACK client libraries use LGPL licensing. See decision 0004 for
the dependency choice and caveats. ALSA remains a future MIDI candidate. No packages or system settings
are changed by these build commands.
## Phase 2 verification
```sh
make all test gui debug
make test-jack
# Extended live run after building the integration binary:
./build/test_jack 30
# Verify optional build without JACK metadata, in a separate directory:
make BUILD_DIR=build/nojack PKG_CONFIG=false all test
# Address/undefined sanitizers (non-realtime tests only):
make BUILD_DIR=build/sanitize CC=clang CFLAGS='-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=address,undefined -fno-omit-frame-pointer' LDFLAGS='-fsanitize=address,undefined' all test
# Thread sanitizer for publication/reclamation:
make BUILD_DIR=build/tsan CC=cc CFLAGS='-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=thread' LDFLAGS='-fsanitize=thread' build/tsan/test_realtime
./build/tsan/test_realtime
```
Normal, sanitizer and no-JACK tests pass. ThreadSanitizer reports no race in
2,000 concurrent submissions. The 30-second live loopback test passed at 48 kHz
with zero xruns and ten restart cycles; see tests/integration/README.md. Live
server termination/format changes were not performed on the shared server;
fault-injection tests cover those notification paths. No packages installed
or system/device configuration changed. Sanitizers are not enabled in the live
callback qualification run.
## Next milestone
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.
Phase 3 connects the FLTK shell to the backend C API for engine controls,
status/error reporting and clean shutdown. Physical playback and endurance,
project persistence and sequencing remain unverified/unimplemented as applicable.

View file

@ -15,12 +15,19 @@ 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.
limits and compatibility behavior.
## Phase 2 — Real-time backend
Choose and implement a Linux backend. Document callback semantics, start/stop,
device errors, shutdown, and safe graph publication.
Implemented: optional JACK-compatible mono backend, explicit audio port routing,
format/xrun/loss status, start/stop/reopen, and bounded pending/retired plan
handoff using lock-free atomics. Hardware-independent failure/concurrency tests
and a 30-second software-loopback run pass. See decision 0004 and AUDIO_ENGINE.md.
Physical hardware playback/endurance and live server-loss recovery remain
unverified; no GUI engine integration until Phase 3.
## Phase 3 — FLTK shell
Main window, transport controls, view switching, backend status, clean startup
and shutdown through the C API.

View file

@ -0,0 +1,84 @@
# Decision 0004: JACK backend and bounded plan handoff
**Status:** Accepted for Phase 2
## Backend
Use the JACK C client API. The inspected system provides PipeWire's maintained
JACK compatibility library (`pkg-config jack` reports 3.1609.0), headers, a live
server, and mono audio ports. This is JACK client integration, not native
PipeWire or ALSA PCM. JACK owns device access, timing, sample rate and block
size. The application registers one mono input/output pair and exposes port
inspection and explicit routing. It neither starts the server nor changes its
configuration or automatically connects hardware.
JACK support is optional at build time. Without JACK development metadata,
backend open returns unsupported; offline and handoff tests still work.
Consumers that call backend APIs link `pkg-config --libs jack` as well as `-lm`.
No backend headers appear in public headers. The kind/configuration/status API
is the boundary for adding another backend; there is no speculative plugin
framework or second driver implementation.
The optional dependency provides actual audio callbacks and port routing.
PipeWire's installed JACK library is MIT licensed (`pipewire-jack/COPYING`).
Traditional JACK client libraries use LGPL licensing; review the actual linked
implementation, transitive libraries and notices when distributing. Nothing
is bundled or installed here. ALSA is still a future MIDI candidate.
## C99 and atomic operations
Retain C99 and use GCC/Clang `__atomic` builtins. This is a documented compiler
extension, avoiding a language-standard change and keeping the existing core
C99-compatible on the selected compilers. Initialization requires pointer and
unsigned-int atomics to be always lock-free, or returns unsupported before
callbacks are installed. Naturally aligned fields reside in calloc-allocated
objects. There is no mutex or libatomic fallback in the callback path.
## Ownership and publication
One serialized control producer and one audio consumer share two pointer slots:
- `pending`: producer publishes a fully compiled plan with release; consumer
exchanges it for NULL with acquire at the next eligible block boundary.
- `retired`: consumer releases its previous active plan into this slot only
after its final access. Producer exchanges with NULL using acquire/release,
then destroys that acknowledged plan on the control thread.
The active pointer and all active DSP state belong exclusively to the consumer.
Producer never reads them. Caller graph memory is copied before publication;
callers never receive plan pointers through the realtime/backend API.
A full pending slot returns capacity without replacing anything. A full retired
slot defers adoption while the current plan continues processing. The consumer
performs at most one adoption per block and never loops waiting for the producer.
Producer collection can overlap processing because the retired plan is no
longer reachable from the consumer. Collection clears the slot before freeing;
consumer may then retire another, distinct plan. Publication never immediately
frees the previously active plan.
Submitting collects any already retired plan, so a producer repeatedly submitting
also reclaims memory. If it becomes idle, one pending, one active, and one retired
plan bound retained resources (at most three plans, each with up to 64 MiB of
sample buffers). Periodic collect is recommended. Shutdown closes the JACK client
and synchronizes its callbacks before destroying any active/pending/retired plan.
Standalone realtime-engine callers must stop and join their consumer themselves.
## Status, errors and recovery
Unsigned-int status fields use lock-free atomics. Values are individual snapshots,
not one transactional state. Counters wrap naturally; no string formatting,
logging, allocation, GUI operation or blocking synchronization occurs in project
callbacks. JACK's shutdown notification only stores a flag and leaves cleanup to
the control thread. Other notifications store the current format and xrun count.
Buffer changes within the configured maximum require no allocation. Larger
blocks are fully silenced and reported as capacity errors; smaller blocks can
resume processing. Sample-rate changes latch a flag and silence processing
rather than replaying an oscillator compiled for a different rate. Close/reopen
and resubmit after a rate change or server loss. There is no automatic reconnect.
JACK-deactivate pauses the retained plan; restart continues its oscillator phase.
Plan replacement starts the newly compiled oscillator at phase zero, without
crossfade. Smooth parameter ramps and click-free graph transitions are future work.
No stereo, recording, MIDI, native PipeWire API, GUI engine controls or hardware
configuration dialogs are part of this phase.

63
include/xtc/xtc_backend.h Normal file
View file

@ -0,0 +1,63 @@
#ifndef XTC_BACKEND_H
#define XTC_BACKEND_H
#include "xtc_engine.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct xtc_backend xtc_backend;
typedef enum { XTC_BACKEND_JACK = 1 } xtc_backend_kind;
typedef struct {
xtc_backend_kind kind;
const char *client_name; /* Borrowed for open only; nonempty. */
size_t max_frames; /* 1..65536, at least the server's current block. */
size_t channel_count; /* Currently must be 1. */
} xtc_backend_config;
typedef struct {
unsigned int sample_rate;
unsigned int buffer_frames;
unsigned int xruns; /* Wraps modulo UINT_MAX + 1. */
unsigned int processed_blocks;
int running;
int server_lost;
int sample_rate_changed; /* Sticky: close/reopen and resubmit to recover. */
xtc_result process_result;
} xtc_backend_status;
/* All operations are serialized on one control thread. Public API contains
* no JACK types. JACK is optional at build time (otherwise UNSUPPORTED).
* Opens without starting a server, changing devices, or connecting ports.
* Returns IO if server/client setup fails. On failure *out_backend is NULL,
* except if cleanup itself fails: a non-NULL handle must be closed/retried;
* only close is permitted on that partially initialized handle. */
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend);
/* Stop/close establish callback quiescence before state can be destroyed.
* On failure the handle remains owned by caller; retry close. NULL close OK. */
xtc_result xtc_backend_close(xtc_backend *backend);
xtc_result xtc_backend_start(xtc_backend *backend);
xtc_result xtc_backend_stop(xtc_backend *backend);
/* Submit allowed stopped or running. Copies graph; capacity = pending slot busy. */
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph);
void xtc_backend_collect(xtc_backend *backend);
/* Snapshot fields are individually coherent, not a transactional snapshot. */
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status);
/* Borrowed names valid until close. Direction is the client's own port. */
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction,
const char **out_name);
/* Enumerates available mono audio ports (devices and software routes). Visitor
* receives a borrowed name valid only during call; return nonzero to stop.
* Control-thread only. JACK NULL list means no matching ports. */
typedef int (*xtc_backend_port_visitor)(const char *name, void *context);
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
xtc_backend_port_visitor visitor, void *context);
/* Connect/disconnect this client's port with a remote opposite-direction port.
* own_direction INPUT receives remote output; OUTPUT sends to remote input.
* Explicit user choice only; no automatic physical routing. */
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction own_direction,
const char *remote_name);
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction own_direction,
const char *remote_name);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,38 @@
#ifndef XTC_REALTIME_H
#define XTC_REALTIME_H
#include "xtc_engine.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct xtc_realtime xtc_realtime;
typedef struct {
unsigned int processed_blocks; /* Wraps modulo UINT_MAX + 1. */
xtc_result last_result;
} xtc_realtime_status;
/* Control thread only. A single control producer and single audio consumer.
* C99 with GCC/Clang lock-free __atomic builtins is required; unsupported
* targets return XTC_ERR_UNSUPPORTED. No ownership is shared with callers. */
xtc_result xtc_realtime_create(double sample_rate, size_t max_frames,
xtc_realtime **out_engine);
/* Requires the audio consumer to have stopped and joined. */
void xtc_realtime_destroy(xtc_realtime *engine);
/* Compiles/copies a graph. One pending plan allowed; full mailbox returns
* XTC_ERR_CAPACITY without changing the active or pending plan. */
xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph);
/* Frees acknowledged retired state, never the audio thread's active plan. */
void xtc_realtime_collect(xtc_realtime *engine);
xtc_result xtc_realtime_get_status(const xtc_realtime *engine,
xtc_realtime_status *out_status);
/* Audio consumer only. Adopts at most one plan per block, without allocation,
* freeing, locks or I/O. No plan means silence. On failure writes silence.
* frame_count must be <= configured max_frames and output must hold it.
* Oversized buffers are silenced too; callers must bound their own frame count.
* NULL input means silence. Zero frames is a no-op. */
xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input,
float *output, size_t frame_count);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -1,8 +1,9 @@
# Audio backend
No hardware backend is implemented yet. Keep backend-specific headers out of
public headers. The first backend should be selected after checking the target
Linux environment and its development packages.
`jack_backend.c` implements the public `xtc_backend.h` API using JACK when
available and returns unsupported otherwise. JACK headers stay private.
It registers one mono input and output, supports start/stop and explicit port
routing, and reports server/format/process/xrun status without callback logging.
A backend implementation must document callback threading, buffer sizes,
sample-rate negotiation, device loss, and shutdown semantics.
See `docs/decisions/0004-jack-and-plan-handoff.md` for ownership and licensing,
and `docs/AUDIO_ENGINE.md` for API usage, recovery and test limitations.

249
src/backend/jack_backend.c Normal file
View file

@ -0,0 +1,249 @@
#include "xtc/xtc_backend.h"
#include "xtc/xtc_realtime.h"
#include <stdlib.h>
#include <string.h>
#ifdef XTC_HAVE_JACK
#include <jack/jack.h>
#include <errno.h>
struct xtc_backend {
jack_client_t *client;
jack_port_t *input, *output;
xtc_realtime *engine;
size_t max_frames;
unsigned int expected_rate;
unsigned int rate, frames, lost, rate_changed, xruns;
unsigned int process_result;
int ready, running; /* Control thread only. */
};
static int process(jack_nframes_t frames, void *context)
{
xtc_backend *backend = context;
float *output = jack_port_get_buffer(backend->output, frames);
xtc_result result = XTC_OK;
if (output == NULL) {
__atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED);
return 0;
}
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) {
result = XTC_ERR_INVALID_STATE;
} else if ((size_t)frames > backend->max_frames) {
result = XTC_ERR_CAPACITY;
} else {
const float *input = jack_port_get_buffer(backend->input, frames);
result = xtc_realtime_process(backend->engine, input, output, (size_t)frames);
}
if (result != XTC_OK) memset(output, 0, (size_t)frames * sizeof(*output));
__atomic_store_n(&backend->process_result, (unsigned int)result, __ATOMIC_RELAXED);
return 0; /* Keep connected and silent on recoverable processing errors. */
}
static void shutdown_callback(void *context)
{
xtc_backend *backend = context;
__atomic_store_n(&backend->lost, 1U, __ATOMIC_RELEASE);
}
static int rate_callback(jack_nframes_t rate, void *context)
{
xtc_backend *backend = context;
__atomic_store_n(&backend->rate, (unsigned int)rate, __ATOMIC_RELAXED);
if ((unsigned int)rate != backend->expected_rate)
__atomic_store_n(&backend->rate_changed, 1U, __ATOMIC_RELEASE);
return 0;
}
static int buffer_callback(jack_nframes_t frames, void *context)
{
xtc_backend *backend = context;
__atomic_store_n(&backend->frames, (unsigned int)frames, __ATOMIC_RELAXED);
return 0;
}
static int xrun_callback(void *context)
{
xtc_backend *backend = context;
(void)__atomic_fetch_add(&backend->xruns, 1U, __ATOMIC_RELAXED);
return 0;
}
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend)
{
xtc_backend *backend;
xtc_result result;
jack_status_t status = 0;
if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
*out_backend = NULL;
if (config == NULL || config->client_name == NULL || config->client_name[0] == '\0' ||
config->max_frames == 0U || config->max_frames > 65536U || config->channel_count != 1U)
return XTC_ERR_INVALID_ARGUMENT;
if (config->kind != XTC_BACKEND_JACK) return XTC_ERR_UNSUPPORTED;
if (strlen(config->client_name) >= (size_t)jack_client_name_size()) return XTC_ERR_INVALID_ARGUMENT;
backend = calloc(1U, sizeof(*backend));
if (backend == NULL) return XTC_ERR_OUT_OF_MEMORY;
backend->max_frames = config->max_frames;
backend->client = jack_client_open(config->client_name, JackNoStartServer, &status);
if (backend->client == NULL) { free(backend); return XTC_ERR_IO; }
backend->expected_rate = (unsigned int)jack_get_sample_rate(backend->client);
backend->rate = backend->expected_rate;
backend->frames = (unsigned int)jack_get_buffer_size(backend->client);
result = backend->frames == 0U || backend->frames > config->max_frames ? XTC_ERR_CAPACITY :
xtc_realtime_create((double)backend->rate, config->max_frames, &backend->engine);
if (result != XTC_OK) goto fail;
backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U);
backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U);
if (backend->input == NULL || backend->output == NULL) { result = XTC_ERR_IO; goto fail; }
if (jack_set_process_callback(backend->client, process, backend) != 0 ||
jack_set_sample_rate_callback(backend->client, rate_callback, backend) != 0 ||
jack_set_buffer_size_callback(backend->client, buffer_callback, backend) != 0 ||
jack_set_xrun_callback(backend->client, xrun_callback, backend) != 0) {
result = XTC_ERR_IO;
goto fail;
}
jack_on_shutdown(backend->client, shutdown_callback, backend);
backend->ready = 1;
*out_backend = backend;
return XTC_OK;
fail:
/* Client never activated: no processing callback can reference the engine.
* close synchronizes any registered notification callbacks. */
if (jack_client_close(backend->client) != 0) {
*out_backend = backend; /* Preserve callback context for close retry. */
return XTC_ERR_IO;
}
xtc_realtime_destroy(backend->engine);
free(backend);
return result;
}
xtc_result xtc_backend_close(xtc_backend *backend)
{
if (backend == NULL) return XTC_OK;
/* JACK removes this client and joins callbacks before returning success. */
if (jack_client_close(backend->client) != 0) return XTC_ERR_IO;
xtc_realtime_destroy(backend->engine);
free(backend);
return XTC_OK;
}
xtc_result xtc_backend_start(xtc_backend *backend)
{
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready) return XTC_ERR_INVALID_STATE;
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
if (backend->running) return XTC_ERR_INVALID_STATE;
if (jack_activate(backend->client) != 0) return XTC_ERR_IO;
backend->running = 1;
return XTC_OK;
}
xtc_result xtc_backend_stop(xtc_backend *backend)
{
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready) return XTC_ERR_INVALID_STATE;
if (!backend->running) return XTC_OK;
if (jack_deactivate(backend->client) != 0) return XTC_ERR_IO;
backend->running = 0;
xtc_realtime_collect(backend->engine);
return XTC_OK;
}
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
{
if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready) return XTC_ERR_INVALID_STATE;
if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) ||
__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
return xtc_realtime_submit(backend->engine, graph);
}
void xtc_backend_collect(xtc_backend *backend)
{ if (backend != NULL) xtc_realtime_collect(backend->engine); }
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status)
{
xtc_realtime_status engine_status;
if (backend == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready) return XTC_ERR_INVALID_STATE;
(void)xtc_realtime_get_status(backend->engine, &engine_status);
out_status->sample_rate = __atomic_load_n(&backend->rate, __ATOMIC_RELAXED);
out_status->buffer_frames = __atomic_load_n(&backend->frames, __ATOMIC_RELAXED);
out_status->xruns = __atomic_load_n(&backend->xruns, __ATOMIC_RELAXED);
out_status->server_lost = (int)__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE);
out_status->sample_rate_changed = (int)__atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE);
out_status->running = backend->running && !out_status->server_lost;
out_status->processed_blocks = engine_status.processed_blocks;
out_status->process_result = (xtc_result)__atomic_load_n(&backend->process_result, __ATOMIC_RELAXED);
return XTC_OK;
}
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction,
const char **out_name)
{
if (backend == NULL || out_name == NULL ||
(direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
*out_name = jack_port_name(direction == XTC_PORT_INPUT ? backend->input : backend->output);
return *out_name == NULL ? XTC_ERR_IO : XTC_OK;
}
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
xtc_backend_port_visitor visitor, void *context)
{
const char **ports;
size_t i;
if (backend == NULL || visitor == NULL ||
(direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) return XTC_ERR_INVALID_ARGUMENT;
if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE;
ports = jack_get_ports(backend->client, NULL, JACK_DEFAULT_AUDIO_TYPE,
direction == XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput);
if (ports == NULL) return XTC_OK;
for (i = 0U; ports[i] != NULL; ++i) if (visitor(ports[i], context) != 0) break;
jack_free(ports);
return XTC_OK;
}
static xtc_result route(xtc_backend *backend, xtc_port_direction direction,
const char *remote, int disconnect)
{
const char *own = NULL;
const char *source, *dest;
xtc_result result;
int code;
if (remote == NULL || remote[0] == '\0') return XTC_ERR_INVALID_ARGUMENT;
result = xtc_backend_port_name(backend, direction, &own);
if (result != XTC_OK) return result;
if (!backend->running) return XTC_ERR_INVALID_STATE;
source = direction == XTC_PORT_OUTPUT ? own : remote;
dest = direction == XTC_PORT_OUTPUT ? remote : own;
code = disconnect ? jack_disconnect(backend->client, source, dest) :
jack_connect(backend->client, source, dest);
return code == 0 || (!disconnect && code == EEXIST) ? XTC_OK : XTC_ERR_IO;
}
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
{ return route(backend, direction, remote, 0); }
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
{ return route(backend, direction, remote, 1); }
#else
/* Keep core/offline builds available on machines without JACK headers. */
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend)
{
if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT;
*out_backend = NULL;
if (config == NULL) return XTC_ERR_INVALID_ARGUMENT;
return XTC_ERR_UNSUPPORTED;
}
xtc_result xtc_backend_close(xtc_backend *backend)
{ return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_start(xtc_backend *backend)
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_stop(xtc_backend *backend)
{ (void)backend; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
{ (void)backend; (void)graph; return XTC_ERR_UNSUPPORTED; }
void xtc_backend_collect(xtc_backend *backend) { (void)backend; }
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status)
{ (void)backend; (void)out_status; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name)
{ (void)backend; (void)direction; (void)out_name; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction,
xtc_backend_port_visitor visitor, void *context)
{ (void)backend; (void)direction; (void)visitor; (void)context; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote)
{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; }
#endif

106
src/engine/realtime.c Normal file
View file

@ -0,0 +1,106 @@
#include "xtc/xtc_realtime.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
struct xtc_realtime {
double sample_rate;
size_t max_frames;
xtc_plan *pending;
xtc_plan *retired;
xtc_plan *active; /* Audio owner only, until consumer is stopped/joined. */
unsigned int blocks;
unsigned int result;
};
xtc_result xtc_realtime_create(double sample_rate, size_t max_frames,
xtc_realtime **out_engine)
{
xtc_realtime *engine;
if (out_engine == NULL) return XTC_ERR_INVALID_ARGUMENT;
*out_engine = NULL;
if (!isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U ||
max_frames > 65536U) return XTC_ERR_INVALID_ARGUMENT;
if (!__atomic_always_lock_free(sizeof(xtc_plan *), 0) ||
!__atomic_always_lock_free(sizeof(unsigned int), 0)) return XTC_ERR_UNSUPPORTED;
engine = calloc(1U, sizeof(*engine));
if (engine == NULL) return XTC_ERR_OUT_OF_MEMORY;
engine->sample_rate = sample_rate;
engine->max_frames = max_frames;
*out_engine = engine;
return XTC_OK;
}
void xtc_realtime_collect(xtc_realtime *engine)
{
xtc_plan *retired;
if (engine == NULL) return;
retired = __atomic_exchange_n(&engine->retired, NULL, __ATOMIC_ACQ_REL);
xtc_plan_destroy(retired);
}
void xtc_realtime_destroy(xtc_realtime *engine)
{
if (engine == NULL) return;
/* Lifecycle contract: no callback can still execute. */
xtc_plan_destroy(engine->active);
xtc_plan_destroy(engine->pending);
xtc_plan_destroy(engine->retired);
free(engine);
}
xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph)
{
xtc_plan *plan = NULL;
xtc_result result;
if (engine == NULL || graph == NULL) return XTC_ERR_INVALID_ARGUMENT;
xtc_realtime_collect(engine);
if (__atomic_load_n(&engine->pending, __ATOMIC_ACQUIRE) != NULL)
return XTC_ERR_CAPACITY;
result = xtc_graph_compile(graph, engine->sample_rate, engine->max_frames, &plan);
if (result != XTC_OK) return result;
__atomic_store_n(&engine->pending, plan, __ATOMIC_RELEASE);
return XTC_OK;
}
xtc_result xtc_realtime_get_status(const xtc_realtime *engine,
xtc_realtime_status *out_status)
{
if (engine == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT;
out_status->processed_blocks = __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED);
out_status->last_result = (xtc_result)__atomic_load_n(&engine->result, __ATOMIC_RELAXED);
return XTC_OK;
}
xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input,
float *output, size_t frame_count)
{
xtc_result result = XTC_OK;
if (engine == NULL || output == NULL || frame_count > SIZE_MAX / sizeof(*output))
return XTC_ERR_INVALID_ARGUMENT;
if (frame_count == 0U) return XTC_OK;
if (frame_count > engine->max_frames) {
result = XTC_ERR_CAPACITY;
} else {
/* A full retirement slot delays adoption without delaying processing.
* acquire pairs with control's release exchange after its last read. */
if (__atomic_load_n(&engine->retired, __ATOMIC_ACQUIRE) == NULL) {
xtc_plan *next = __atomic_exchange_n(&engine->pending, NULL, __ATOMIC_ACQUIRE);
if (next != NULL) {
xtc_plan *old = engine->active;
engine->active = next;
/* No reference to old is used after this release. */
__atomic_store_n(&engine->retired, old, __ATOMIC_RELEASE);
}
}
if (engine->active != NULL)
result = xtc_plan_process(engine->active, input, output, frame_count);
else
memset(output, 0, frame_count * sizeof(*output));
}
if (result != XTC_OK) memset(output, 0, frame_count * sizeof(*output));
__atomic_store_n(&engine->result, (unsigned int)result, __ATOMIC_RELAXED);
__atomic_store_n(&engine->blocks,
__atomic_load_n(&engine->blocks, __ATOMIC_RELAXED) + 1U, __ATOMIC_RELAXED);
return result;
}

View file

@ -1,11 +1,28 @@
# Integration tests
Planned integration coverage:
- deterministic offline render;
- repeated engine initialization and shutdown;
- graph replacement while processing;
- project save/load round trip;
- backend disconnect/reconnect;
- MIDI panic and transport stop.
`make test` is hardware/server independent: offline render tests, concurrent
plan-replacement stress, and (when JACK headers are present) JACK ABI fault
injection. The fake links against installed declarations without linking
libjack. It does not claim to reproduce JACK scheduling; the separate
threaded stress test exercises publication/reclamation.
Do not mark these tests as passing until they exist and run.
`make test-jack` explicitly runs a five-second live JACK loopback test. Requires
an existing JACK or PipeWire JACK-compatible server; does not start a server,
change its sample rate/block size, or connect physical outputs. Source output
is connected only to the test's capture client, then disconnected and removed.
It checks finite/bounded captured samples at two gains, active plan replacement,
status and ten restart cycles. For a longer run:
```sh
make build/test_jack
./build/test_jack 30
```
On the inspected system the 30-second test passed at 48000 Hz (initial 1024
frames): 704 engine blocks, 716 probe blocks, both gains observed, zero xruns,
and ten restart cycles. This is a limited software-loopback result, not an
endurance or physical-device qualification. No live server termination or global
format change is performed on the shared server. Those paths, missing server,
port/callback setup failure, activation/deactivation/close failure, null buffers,
invalid configuration, oversize buffers, and reopen are tested with injected
JACK callbacks/results in `tests/unit/test_backend.c`.

View file

@ -0,0 +1,119 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_backend.h"
#include <jack/jack.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
/* Optional live test. The only route is xtc's output -> this test's input.
* No ports are connected to hardware. Never changes server configuration. */
typedef struct {
jack_port_t *input;
unsigned int blocks, bad, low, high, lost;
} probe_state;
static int probe_process(jack_nframes_t frames, void *context)
{
probe_state *probe = context;
const float *input = jack_port_get_buffer(probe->input, frames);
jack_nframes_t frame;
float peak = 0.0f;
if (input == NULL) { __atomic_store_n(&probe->bad, 1U, __ATOMIC_RELAXED); return 0; }
for (frame = 0U; frame < frames; ++frame) {
float sample = input[frame];
if (!isfinite(sample) || fabsf(sample) > 0.201f)
__atomic_store_n(&probe->bad, 1U, __ATOMIC_RELAXED);
if (fabsf(sample) > peak) peak = fabsf(sample);
}
if (peak > 0.09f && peak < 0.101f) __atomic_store_n(&probe->low, 1U, __ATOMIC_RELAXED);
if (peak > 0.19f) __atomic_store_n(&probe->high, 1U, __ATOMIC_RELAXED);
(void)__atomic_fetch_add(&probe->blocks, 1U, __ATOMIC_RELAXED);
return 0;
}
static void probe_shutdown(void *context)
{ probe_state *probe = context; __atomic_store_n(&probe->lost, 1U, __ATOMIC_RELEASE); }
static xtc_result tone(float gain, xtc_graph **out_graph)
{
xtc_result result;
size_t osc, level, output;
result = xtc_graph_create(3U, out_graph);
if (result != XTC_OK) return result;
result = xtc_graph_add_oscillator(*out_graph, 440.0f, &osc);
if (result == XTC_OK) result = xtc_graph_add_gain(*out_graph, gain, &level);
if (result == XTC_OK) result = xtc_graph_add_output(*out_graph, &output);
if (result == XTC_OK) result = xtc_graph_connect(*out_graph, osc, level);
if (result == XTC_OK) result = xtc_graph_connect(*out_graph, level, output);
return result;
}
static int count_port(const char *name, void *context)
{ (void)name; ++*(size_t *)context; return 0; }
#define REQUIRE(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); goto cleanup; } } while (0)
int main(int argc, char **argv)
{
long seconds = 5;
char *end;
char name[64];
xtc_backend_config config = {XTC_BACKEND_JACK, name, 4096U, 1U};
xtc_backend *backend = NULL;
xtc_backend_status status;
xtc_graph *low = NULL, *high = NULL;
jack_client_t *probe_client = NULL;
jack_status_t jack_status = 0;
probe_state probe = {0};
struct timespec pause = {0, 50000000L};
size_t port_count = 0U;
long tick;
int result = EXIT_FAILURE;
if (argc > 2) { fprintf(stderr,"Usage: %s [seconds: 1..300]\n",argv[0]); return EXIT_FAILURE; }
if (argc == 2) {
seconds = strtol(argv[1], &end, 10);
if (*end != '\0' || seconds < 1 || seconds > 300) return EXIT_FAILURE;
}
(void)snprintf(name, sizeof(name), "xtc-test-%ld", (long)getpid());
REQUIRE(xtc_backend_open(&config, &backend) == XTC_OK);
REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK);
printf("JACK: %u Hz, %u frames, mono, max 4096 frames\n", status.sample_rate, status.buffer_frames);
REQUIRE(xtc_backend_visit_ports(backend, XTC_PORT_INPUT, count_port, &port_count) == XTC_OK);
REQUIRE(port_count > 0U);
REQUIRE(tone(0.1f, &low) == XTC_OK && tone(0.2f, &high) == XTC_OK);
probe_client = jack_client_open("xtc-test-probe", JackNoStartServer, &jack_status);
REQUIRE(probe_client != NULL);
probe.input = jack_port_register(probe_client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U);
REQUIRE(probe.input != NULL);
REQUIRE(jack_set_process_callback(probe_client, probe_process, &probe) == 0);
jack_on_shutdown(probe_client, probe_shutdown, &probe);
REQUIRE(jack_activate(probe_client) == 0);
REQUIRE(xtc_backend_submit(backend, low) == XTC_OK);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(probe.input)) == XTC_OK);
for (tick = 0; tick < seconds * 20; ++tick) {
(void)nanosleep(&pause, NULL);
REQUIRE(!__atomic_load_n(&probe.lost, __ATOMIC_ACQUIRE));
REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK);
REQUIRE(status.running && !status.sample_rate_changed && status.process_result == XTC_OK);
xtc_backend_collect(backend);
if (tick % 8 == 7) {
xtc_result submitted = xtc_backend_submit(backend, tick % 16 == 7 ? high : low);
REQUIRE(submitted == XTC_OK || submitted == XTC_ERR_CAPACITY);
}
}
REQUIRE(__atomic_load_n(&probe.bad, __ATOMIC_RELAXED) == 0U);
REQUIRE(__atomic_load_n(&probe.low, __ATOMIC_RELAXED) && __atomic_load_n(&probe.high, __ATOMIC_RELAXED));
REQUIRE(status.processed_blocks > 0U);
REQUIRE(xtc_backend_disconnect(backend, XTC_PORT_OUTPUT, jack_port_name(probe.input)) == XTC_OK);
for (tick = 0; tick < 10; ++tick) {
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
(void)nanosleep(&pause, NULL);
}
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
printf("PASS: %ld seconds, %u engine blocks, %u captured blocks, both gains observed, %u xruns; 10 restart cycles\n",
seconds, status.processed_blocks, __atomic_load_n(&probe.blocks, __ATOMIC_RELAXED), status.xruns);
result = EXIT_SUCCESS;
cleanup:
if (xtc_backend_close(backend) != XTC_OK) { fprintf(stderr,"JACK client close failed\n"); result = EXIT_FAILURE; }
if (probe_client != NULL && jack_client_close(probe_client) != 0) result = EXIT_FAILURE;
xtc_graph_destroy(low); xtc_graph_destroy(high);
return result;
}

172
tests/unit/test_backend.c Normal file
View file

@ -0,0 +1,172 @@
/* JACK ABI fake: deterministic fault injection, linked without libjack.
* Processing is driven synchronously here; realtime concurrency is tested
* separately by test_realtime. No fake header replaces installed declarations. */
#include "xtc/xtc_backend.h"
#include <jack/jack.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
static JackProcessCallback process_cb;
static JackSampleRateCallback rate_cb;
static JackBufferSizeCallback size_cb;
static JackXRunCallback xrun_cb;
static JackShutdownCallback shutdown_cb;
static void *context;
static float input[128], output[128];
static int client_token, input_token, output_token;
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
static int active, closed, port_calls, null_buffer;
static unsigned int server_frames = 32U;
jack_client_t *jack_client_open(const char *name, jack_options_t options, jack_status_t *status, ...)
{
(void)name; (void)status;
if (options != JackNoStartServer || open_fail) return NULL;
port_calls = 0;
process_cb = NULL; rate_cb = NULL; size_cb = NULL; xrun_cb = NULL; shutdown_cb = NULL;
return (jack_client_t *)&client_token;
}
int jack_client_close(jack_client_t *client)
{ (void)client; if (close_fail) return -1; active = 0; ++closed; return 0; }
int jack_client_name_size(void) { return 64; }
jack_nframes_t jack_get_sample_rate(jack_client_t *client) { (void)client; return 48000U; }
jack_nframes_t jack_get_buffer_size(jack_client_t *client) { (void)client; return server_frames; }
jack_port_t *jack_port_register(jack_client_t *client, const char *name, const char *type,
unsigned long flags, unsigned long buffer_size)
{
(void)client; (void)name; (void)type; (void)buffer_size;
++port_calls;
if (register_fail == port_calls) return NULL;
return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token);
}
int jack_set_process_callback(jack_client_t *client, JackProcessCallback callback, void *arg)
{ (void)client; process_cb = callback; context = arg; return callback_fail ? -1 : 0; }
int jack_set_sample_rate_callback(jack_client_t *client, JackSampleRateCallback callback, void *arg)
{ (void)client; (void)arg; rate_cb = callback; return 0; }
int jack_set_buffer_size_callback(jack_client_t *client, JackBufferSizeCallback callback, void *arg)
{ (void)client; (void)arg; size_cb = callback; return 0; }
int jack_set_xrun_callback(jack_client_t *client, JackXRunCallback callback, void *arg)
{ (void)client; (void)arg; xrun_cb = callback; return 0; }
void jack_on_shutdown(jack_client_t *client, JackShutdownCallback callback, void *arg)
{ (void)client; (void)arg; shutdown_cb = callback; }
int jack_activate(jack_client_t *client)
{ (void)client; if (activate_fail) return -1; active = 1; return 0; }
int jack_deactivate(jack_client_t *client)
{ (void)client; if (deactivate_fail) return -1; active = 0; return 0; }
void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames)
{ (void)frames; if (null_buffer) return NULL; return port == (jack_port_t *)&input_token ? input : output; }
const char *jack_port_name(const jack_port_t *port)
{ return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags)
{
const char **list = malloc(2U * sizeof(*list));
(void)client; (void)pattern; (void)type; (void)flags;
if (list != NULL) { list[0] = "fake:audio"; list[1] = NULL; }
return list;
}
void jack_free(void *pointer) { free(pointer); }
int jack_connect(jack_client_t *client, const char *source, const char *dest)
{ (void)client; return strstr(source, "missing") || strstr(dest, "missing") ? -1 : EEXIST; }
int jack_disconnect(jack_client_t *client, const char *source, const char *dest)
{ (void)client; (void)source; (void)dest; return 0; }
static int visit(const char *name, void *arg)
{ if (strcmp(name, "fake:audio") == 0) ++*(int *)arg; return 0; }
int main(void)
{
xtc_backend_config config = {XTC_BACKEND_JACK, "test", 64U, 1U};
xtc_backend *backend = NULL;
xtc_backend_status status;
xtc_graph *graph = NULL;
size_t node, i;
const char *name;
int visits = 0, iteration;
CHECK(xtc_backend_open(NULL, &backend) == XTC_ERR_INVALID_ARGUMENT && backend == NULL);
config.channel_count = 2U;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_INVALID_ARGUMENT);
config.channel_count = 1U;
config.kind = (xtc_backend_kind)999;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_UNSUPPORTED);
config.kind = XTC_BACKEND_JACK;
open_fail = 1;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
open_fail = 0;
server_frames = 128U;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_CAPACITY && backend == NULL);
server_frames = 32U;
register_fail = 2;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
register_fail = 0;
callback_fail = 1;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
close_fail = 1;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend != NULL);
CHECK(xtc_backend_start(backend) == XTC_ERR_INVALID_STATE);
close_fail = 0;
CHECK(xtc_backend_close(backend) == XTC_OK);
callback_fail = 0;
CHECK(xtc_graph_create(1U, &graph) == XTC_OK);
CHECK(xtc_graph_add_gain(graph, 0.5f, &node) == XTC_OK);
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_ERR_CAPACITY);
CHECK(xtc_backend_port_name(backend, XTC_PORT_OUTPUT, &name) == XTC_OK && strcmp(name,"test:output") == 0);
CHECK(xtc_backend_visit_ports(backend, XTC_PORT_INPUT, visit, &visits) == XTC_OK && visits == 1);
CHECK(xtc_backend_connect(backend, XTC_PORT_OUTPUT, "fake:in") == XTC_ERR_INVALID_STATE);
activate_fail = 1;
CHECK(xtc_backend_start(backend) == XTC_ERR_IO);
activate_fail = 0;
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_ERR_INVALID_STATE);
CHECK(xtc_backend_connect(backend, XTC_PORT_OUTPUT, "fake:in") == XTC_OK);
CHECK(xtc_backend_connect(backend, XTC_PORT_INPUT, "missing:out") == XTC_ERR_IO);
CHECK(xtc_backend_disconnect(backend, XTC_PORT_OUTPUT, "fake:in") == XTC_OK);
for (i = 0U; i < 128U; ++i) input[i] = 1.0f;
CHECK(process_cb(32U, context) == 0 && output[0] == 0.5f);
CHECK(size_cb(128U, context) == 0);
CHECK(process_cb(128U, context) == 0 && output[127] == 0.0f);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.process_result == XTC_ERR_CAPACITY);
CHECK(size_cb(16U, context) == 0);
CHECK(process_cb(16U, context) == 0 && output[15] == 0.5f);
CHECK(xrun_cb(context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.xruns == 1U && status.buffer_frames == 16U);
null_buffer = 1;
CHECK(process_cb(16U, context) == 0);
null_buffer = 0;
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.process_result == XTC_ERR_IO);
CHECK(rate_cb(44100U, context) == 0);
CHECK(process_cb(16U, context) == 0 && output[0] == 0.0f);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_rate_changed && status.sample_rate == 44100U);
CHECK(xtc_backend_submit(backend, graph) == XTC_ERR_INVALID_STATE);
deactivate_fail = 1;
CHECK(xtc_backend_stop(backend) == XTC_ERR_IO);
deactivate_fail = 0;
CHECK(xtc_backend_stop(backend) == XTC_OK && !active);
CHECK(xtc_backend_start(backend) == XTC_ERR_INVALID_STATE);
CHECK(xtc_backend_close(backend) == XTC_OK);
for (iteration = 0; iteration < 100; ++iteration) {
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(process_cb(32U, context) == 0);
if (iteration % 2 == 0) {
shutdown_cb(context);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.server_lost && !status.running);
CHECK(xtc_backend_start(backend) == XTC_ERR_INVALID_STATE);
} else {
CHECK(xtc_backend_stop(backend) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK);
}
CHECK(xtc_backend_submit(backend, graph) == (iteration % 2 == 0 ? XTC_ERR_INVALID_STATE : XTC_OK));
close_fail = 1;
CHECK(xtc_backend_close(backend) == XTC_ERR_IO);
close_fail = 0;
CHECK(xtc_backend_close(backend) == XTC_OK);
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(graph);
puts("JACK backend fault-injection tests passed.");
return 0;
}

View file

@ -0,0 +1,80 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_realtime.h"
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
typedef struct { xtc_realtime *engine; unsigned int stop, failed; } worker_state;
static void *worker(void *context)
{
worker_state *state = context;
float input[32], output[32];
size_t i;
for (i = 0U; i < 32U; ++i) input[i] = 1.0f;
while (!__atomic_load_n(&state->stop, __ATOMIC_ACQUIRE)) {
if (xtc_realtime_process(state->engine, input, output, 32U) != XTC_OK)
__atomic_store_n(&state->failed, 1U, __ATOMIC_RELAXED);
/* A whole block must come from one plan. No partially edited gains. */
for (i = 0U; i < 32U; ++i)
if ((output[i] != 0.0f && output[i] != 1.0f && output[i] != 2.0f) || output[i] != output[0])
__atomic_store_n(&state->failed, 1U, __ATOMIC_RELAXED);
sched_yield();
}
return NULL;
}
static int run(void)
{
xtc_realtime *engine = NULL;
xtc_graph *one = NULL, *two = NULL;
xtc_realtime_status status;
size_t node, i;
float input[5] = {1,1,1,1,1}, output[5] = {9,9,9,9,9};
pthread_t thread;
worker_state state;
CHECK(xtc_realtime_create(48000.0, 32U, &engine) == XTC_OK);
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 0.0f);
CHECK(xtc_graph_create(1U, &one) == XTC_OK);
CHECK(xtc_graph_add_gain(one, 1.0f, &node) == XTC_OK);
CHECK(xtc_graph_create(1U, &two) == XTC_OK);
CHECK(xtc_graph_add_gain(two, 2.0f, &node) == XTC_OK);
CHECK(xtc_realtime_submit(engine, one) == XTC_OK);
CHECK(xtc_realtime_submit(engine, two) == XTC_ERR_CAPACITY);
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 1.0f);
CHECK(xtc_realtime_submit(engine, two) == XTC_OK);
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 2.0f);
xtc_realtime_collect(engine);
CHECK(xtc_realtime_process(engine, input, output, 0U) == XTC_OK && output[0] == 2.0f);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.processed_blocks == 3U);
state.engine = engine; state.stop = 0U; state.failed = 0U;
CHECK(pthread_create(&thread, NULL, worker, &state) == 0);
for (i = 0U; i < 2000U; ++i) {
xtc_result result;
do {
result = xtc_realtime_submit(engine, i % 2U ? one : two);
if (result == XTC_ERR_CAPACITY) sched_yield();
} while (result == XTC_ERR_CAPACITY);
CHECK(result == XTC_OK);
}
__atomic_store_n(&state.stop, 1U, __ATOMIC_RELEASE);
CHECK(pthread_join(thread, NULL) == 0);
CHECK(state.failed == 0U);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.processed_blocks > 3U);
xtc_realtime_destroy(engine); /* Also cleans pending and retired plans. */
xtc_graph_destroy(one); xtc_graph_destroy(two);
for (i = 0U; i < 100U; ++i) {
CHECK(xtc_realtime_create(48000.0, 4U, &engine) == XTC_OK);
CHECK(xtc_realtime_process(engine, input, output, 5U) == XTC_ERR_CAPACITY);
CHECK(output[0] == 0.0f && output[4] == 0.0f);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.last_result == XTC_ERR_CAPACITY);
xtc_realtime_destroy(engine);
}
return 0;
}
int main(void)
{
CHECK(run() == 0);
puts("Realtime handoff and concurrent replacement tests passed.");
return 0;
}