122 lines
6.1 KiB
Markdown
122 lines
6.1 KiB
Markdown
# Decision 0004: JACK Backend and Bounded Plan Handoff
|
|
|
|
**Status:** Accepted
|
|
|
|
---
|
|
|
|
## 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 included.
|
|
|
|
---
|
|
|
|
## 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.
|