108 lines
6.1 KiB
Markdown
108 lines
6.1 KiB
Markdown
# 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/release 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.
|
|
|
|
## Phase 4 adoption acknowledgement and stopped editing
|
|
|
|
The pending pointer has an associated atomic revision. Producer stores the
|
|
revision before release-publishing the pointer. Consumer acquire-loads a
|
|
non-NULL pending pointer, reads its revision, and only then releases that slot
|
|
with an acquire/release exchange to NULL. Thus producer cannot reuse the
|
|
revision field until the previous consumer read is complete. Consumer publishes
|
|
active_revision when selecting that plan; the GUI compares it with the
|
|
producer's submitted_revision. These are wrapping counters and individual
|
|
snapshots, not plan pointers or a multi-field transactional snapshot.
|
|
|
|
While JACK is stopped, a backend submission builds a separate realtime engine,
|
|
submits/compiles the graph there, and replaces the retained engine only on
|
|
success. Process callbacks cannot run on an inactive client or after successful
|
|
deactivation, and notification callbacks do not access the engine pointer.
|
|
The control thread can therefore reclaim all old state safely. Counters and
|
|
oscillator phases restart for that new engine epoch. The standalone realtime
|
|
API keeps its one-pending-slot contract regardless of external stop state.
|
|
|
|
The callback still adopts at most one plan and never allocates/frees. Revision
|
|
ordering is tested alongside the plan's rendered gain under 2,000 concurrent
|
|
submissions and ThreadSanitizer. Stopped compile failure and active pending
|
|
capacity are checked with backend fault injection.
|