Phase 2.
This commit is contained in:
parent
c0ca1ff396
commit
577d80f018
16 changed files with 1100 additions and 41 deletions
47
Makefile
47
Makefile
|
|
@ -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
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
84
docs/decisions/0004-jack-and-plan-handoff.md
Normal file
84
docs/decisions/0004-jack-and-plan-handoff.md
Normal 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
63
include/xtc/xtc_backend.h
Normal 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
|
||||
38
include/xtc/xtc_realtime.h
Normal file
38
include/xtc/xtc_realtime.h
Normal 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
|
||||
|
|
@ -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
249
src/backend/jack_backend.c
Normal 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
106
src/engine/realtime.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
119
tests/integration/test_jack.c
Normal file
119
tests/integration/test_jack.c
Normal 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
172
tests/unit/test_backend.c
Normal 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;
|
||||
}
|
||||
80
tests/unit/test_realtime.c
Normal file
80
tests/unit/test_realtime.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue