PulseAudio, JACK, and PipeWire compatibility

Compare compatibility servers and application-facing audio interfaces.

14 min readChapter 141 of 168Intermediate → advanced
In this chapter

Before you start#

Read PipeWire core and protocol and graph scheduling and buffers. PipeWire 1.2.0 and PulseAudio 17.0 are separately pinned historical source examples, not a tested compatibility matrix. The exercise is a private arithmetic model and never contacts an audio server.

Learning goals#

Trace a PulseAudio-protocol stream request into PipeWire's compatibility implementation, and distinguish it from a real PulseAudio daemon. Explain how a JACK ABI client obtains process buffers, handles graph changes and meets a conditional callback deadline. Separate protocol compatibility from identical latency or audio output.

Source walkthrough#

Two compatibility interfaces
Pulse protocol
A client requests a stream through a compatible server.
PipeWire pulse module
Maps requests and buffers into PipeWire streams.
JACK ABI
A process callback accesses registered port buffers.
Graph and policy
Negotiation and scheduling still determine outcomes.
Conceptual model. Conditional client paths, not a captured audio session.

A compatibility interface lets an existing application use a familiar API or protocol while another implementation performs the work. A PulseAudio client can use libpulse to connect to a server speaking the PulseAudio native protocol. That server might be the PulseAudio daemon, or it might be PipeWire's module-protocol-pulse compatibility server. Those are different implementations, with different internal graph and scheduling machinery. The client's successful connection identifies neither the physical device nor identical behavior across servers. Configuration, protocol versions, server policy and installed modules all affect the result.

A pa_context represents a libpulse connection and its state. A pa_stream represents a playback, recording or upload stream associated with that context. An asynchronous operation has its own reference and completion callback. The application integrates a PulseAudio mainloop or compatible event-loop adapter, waits for context and stream state transitions, and handles failure or termination rather than assuming that constructing an object establishes a connection. Callback data must remain valid through callback execution; unrefing an operation, stream or context follows the API's ownership rules. The PulseAudio 17.0 client stream implementation provides a source reference for the client side.

The native protocol sends commands and replies with tags for correlation, plus stream data using its negotiated mechanisms. A request can fail before the server creates a stream, or the server can disconnect after local submission. A reply acknowledges the relevant operation; it is not evidence that a listener heard a sample. The server checks protocol version and interprets format, channel map, buffer attributes and flags. These are protocol fields and server-side objects, not a guarantee that a hardware device runs in the requested format. Permission, target availability and policy can reject or redirect a request.

PipeWire's PulseAudio server path

In PipeWire 1.2.0 src/modules/module-protocol-pulse/pulse-server.c, do_create_playback_stream parses a client command into a sample specification, channel map, target and buffer attributes. It accounts for client protocol versions and flags. The implementation can then construct a PipeWire stream with parameters and properties; the graph and session policy determine whether a compatible target can be linked. A protocol shim is not simply a socket name: it has to translate request semantics, stream state, buffer accounting, timing and errors between two models.

From do_create_playback_stream in that file, MIT SPDX license, PipeWire 1.2.0; earlier message parsing and later device lookup are omitted:

c
	if (sink_index != SPA_ID_INVALID && sink_name != NULL)
		goto error_invalid;

The first line detects that both a concrete sink index and a sink name were supplied. The second takes the invalid-request path rather than choosing one arbitrarily. The surrounding function also checks parsing errors and version-specific fields. Its target lookup and format-fixing behavior depend on the parsed flags and discovered device; a valid protocol command can still fail because its target or format is unavailable. Error handling must release any temporary properties and partially created stream according to the implementation's path. The tag associates the eventual reply or error with the request, not with a permanently unique graph object.

A playback stream request through the Pulse module
A playback stream request through the Pulse module1. libpulse client to Pulse server module: CREATE_PLAYBACK_STREAM with a tag. 2. Pulse server module to Pulse server module: parse sample spec, channel map and flags. 3. Pulse server module to PipeWire stream: create stream with properties and buffer attributes. 4. PipeWire stream to Graph and policy: target chosen; format negotiated. 5. Graph and policy to PipeWire stream (return): link active, or negotiation failed. 6. Pulse server module to libpulse client (return): reply or error carrying the same tag.libpulse clientPulse server modulePipeWire streamGraph and policyCREATE_PLAYBACK_STREAM with a tagparse samplespec, channelmap and flagscreate stream withproperties andbuffer attributestarget chosen;format negotiatedlink active, ornegotiation failedreply or errorcarrying the sametag
  1. libpulse clientPulse server module

    CREATE_PLAYBACK_STREAM with a tag

  2. Pulse server modulePulse server module

    parse sample spec, channel map and flags

  3. Pulse server modulePipeWire stream

    create stream with properties and buffer attributes

  4. PipeWire streamGraph and policy

    target chosen; format negotiated

  5. Graph and policyPipeWire stream

    Return. link active, or negotiation failed

  6. Pulse server modulelibpulse client

    Return. reply or error carrying the same tag

Time runs downward. Dashed arrows show returns.

Read the text version
  1. libpulse client → Pulse server module: CREATE_PLAYBACK_STREAM with a tag
  2. Pulse server module → Pulse server module: parse sample spec, channel map and flags
  3. Pulse server module → PipeWire stream: create stream with properties and buffer attributes
  4. PipeWire stream → Graph and policy: target chosen; format negotiated
  5. Graph and policy → PipeWire stream (return): link active, or negotiation failed
  6. Pulse server module → libpulse client (return): reply or error carrying the same tag
Source map. A simplified compatibility path in PipeWire 1.2.0 protocol-pulse, not a captured trace; a PulseAudio daemon follows its own implementation.

For a worked example, suppose a client requests 48,000-frame-per-second stereo signed 16-bit playback. One frame is four bytes. It asks for a target buffer of 4,096 bytes, or 1,024 frames, and a request granularity of 1,024 bytes, or 256 frames. These are fictional values: a server may adjust them based on constraints and flags, and applications must inspect returned attributes. At nominal rate, 256 frames correspond to about 5.333 ms and 1,024 frames to about 21.333 ms of samples. Neither number alone is measured speaker latency. A graph adapter may resample or use a different device quantum, and a server can add buffering.

If the client submits less data than the server needs and the stream underruns, a notification or state change can tell the application that continuity was lost. The application must handle the event and refill or otherwise recover according to the client API and server behavior. It must not assume every byte previously accepted by pa_stream_write reached a physical output. A disconnect while a write or control operation is pending requires resolving or abandoning callbacks under libpulse's rules and freeing each owned operation and stream once. An application that stores a raw pointer to an already destroyed stream in callback data risks a use-after-free even if the server has correctly removed its remote object.

Buffer and timing translation

PulseAudio buffer attributes describe requested buffering and transfer behavior, with values and special constants interpreted by the protocol and server. PipeWire's graph uses its own quantum, node scheduling and buffers. The compatibility server translates between these domains. A request for a particular latency can be clamped, adjusted or rejected, and the effective value can change after a target move or graph reconfiguration. A client should query or receive the effective attributes and timing information through the relevant API rather than treat its initial request as a measured guarantee.

In src/pulse/def.h, PulseAudio 17.0, LGPL-2.1-or-later (surrounding code omitted):

c
typedef struct pa_buffer_attr {
    uint32_t maxlength;
    /**< Maximum length of the buffer in bytes. Setting this to (uint32_t) -1
     * will initialize this to the maximum value supported by server,
     * which is recommended.
     *
     * In strict low-latency playback scenarios you might want to set this to
     * a lower value, likely together with the PA_STREAM_ADJUST_LATENCY flag.
     * If you do so, you ensure that the latency doesn't grow beyond what is
     * acceptable for the use case, at the cost of getting more underruns if
     * the latency is lower than what the server can reliably handle. */

    uint32_t tlength;

The field comments treat requested values as requests. Setting maxlength to (uint32_t) -1 asks the server to choose its supported maximum, so a client that asks for a value does not thereby get it.

In src/modules/module-protocol-pulse/pulse-server.c, PipeWire 1.2.0, MIT SPDX (inside fix_playback_buffer_attr; surrounding code omitted):

c
	minreq = SPA_MIN(minreq, attr->maxlength);

	if (attr->tlength == (uint32_t) -1)
		attr->tlength = frac_to_bytes_round_up(s->default_tlength, &s->ss);
	attr->tlength = SPA_CLAMP(attr->tlength, minreq, attr->maxlength);
	attr->tlength = SPA_ROUND_UP(attr->tlength, frame_size);

A tlength of -1 is replaced with a default, then clamped between the computed minimum request and the maximum length and rounded up to whole frames. The server adjusts the request, so the effective value has to be read back. The excerpt does not show the later latency adjustments or any device constraint.

Requested attributes become server values
Requested attributes become server valuesmaxlength: The client upper bound in bytes; -1 asks the server for its maximum. → Server limit: maxlength is capped at the server maximum and rounded to whole frames. (capped). tlength: The client target buffer length; -1 asks for a server default. → Effective tlength: Clamped between minreq and maxlength, then rounded up to whole frames. (clamped). minreq: The client minimum request; the server raises it to its own floor. → Effective tlength: Clamped between minreq and maxlength, then rounded up to whole frames. (raises lower bound). tlength: The client target buffer length; -1 asks for a server default. → Graph quantum: PipeWire scheduling granularity; a different measure from tlength. (related, not identical)cappedclampedraises lower boundrelated, not identicalmaxlengthThe client upper bound inbytes; -1 asks the serverfor its maximum.tlengthThe client target bufferlength; -1 asks for a serverdefault.minreqThe client minimum request;the server raises it to itsown floor.Server limitmaxlength is capped at theserver maximum and roundedto whole frames.Effective tlengthClamped between minreq andmaxlength, then rounded upto whole frames.Graph quantumPipeWire schedulinggranularity; a differentmeasure from tlength.
maxlengthThe client upper bound in bytes; -1 asks the server for its maximum.
Server limitmaxlength is capped at the server maximum and rounded to whole frames.
tlengthThe client target buffer length; -1 asks for a server default.
Effective tlengthClamped between minreq and maxlength, then rounded up to whole frames.
minreqThe client minimum request; the server raises it to its own floor.
Effective tlengthClamped between minreq and maxlength, then rounded up to whole frames.
tlengthThe client target buffer length; -1 asks for a server default.
Graph quantumPipeWire scheduling granularity; a different measure from tlength.
Read the text version
  • maxlength: The client upper bound in bytes; -1 asks the server for its maximum. → Server limit: maxlength is capped at the server maximum and rounded to whole frames. (capped)
  • tlength: The client target buffer length; -1 asks for a server default. → Effective tlength: Clamped between minreq and maxlength, then rounded up to whole frames. (clamped)
  • minreq: The client minimum request; the server raises it to its own floor. → Effective tlength: Clamped between minreq and maxlength, then rounded up to whole frames. (raises lower bound)
  • tlength: The client target buffer length; -1 asks for a server default. → Graph quantum: PipeWire scheduling granularity; a different measure from tlength. (related, not identical)
Conceptual model. A conceptual mapping from PulseAudio request fields to the server-side steps in the clamp excerpt; the effective values come from the server and no timing is measured.

A client callback can be invoked when the library needs or has data. In a threaded mainloop, the application's access to shared libpulse objects must follow that API's locking and callback rules; a callback can race with application shutdown if the application has not stopped its event loop and synchronized its own state. Memory supplied to a write operation has an ownership contract that depends on the API and any provided free callback. It cannot be freed just because the application sent a command if the library still owns or references it. Conversely, holding a pointer forever after the library releases it leaks resources. A server-side PipeWire buffer has another lifetime entirely.

Corking a stream pauses or resumes its processing according to the protocol; it is not equivalent to destroying it. Moving a stream to another target can change latency and device characteristics. If the target disappears, a policy component may select another target or the stream may fail, depending on settings and permissions. An application should distinguish an underflow event, target change, suspended state and connection termination. Retrying a failed operation without checking whether the context remains ready can produce a loop of immediate failures.

JACK ABI and process callbacks

JACK defines an API and ABI for clients that register ports and process blocks of samples in a graph. PipeWire provides a JACK-compatible library implementation that can map JACK clients and ports into its graph. A program's actual library selection depends on dynamic linking, installation and runtime configuration. Installing a replacement, changing library paths or wrapping a process is a deployment action; this chapter does none of those things. An application can instead use another JACK implementation in another environment. The presence of a PipeWire library on disk does not prove that a particular running process loaded it.

A JACK client normally registers a process callback, creates ports and activates the client. During each callback, it obtains the appropriate port buffer for the current frame count, reads input or writes output, and returns promptly. Port buffers are associated with the current processing cycle and must not be retained as permanent pointers after that cycle. A graph connection can be added or removed while the program runs; callbacks and notifications must update application state safely. A disconnected input may require a defined fallback such as silence, depending on the implementation and application contract.

From PipeWire 1.2.0 pipewire-jack/src/pipewire-jack.c, jack_set_process_callback, MIT SPDX license; null-client validation and later callback assignment are omitted:

c
	if (c->active) {
		pw_log_error("%p: can't set callback on active client", c);
		return -EIO;
	} else if (c->thread_callback) {

The first condition rejects changing the process callback on an active client in this implementation. It reports an error and returns a negative result. The final line begins a separate conflict check for an already configured thread callback; it is not a successful registration. The following source stores the callback and argument only when those checks pass. A program must register at the appropriate time and retain callback data until it has deactivated and synchronized teardown. This excerpt is an implementation example, not a promise that every JACK server uses the same internal structure.

jack_port_get_buffer in the same file validates the port object and current frame count, and can return null on unsuitable state or a missing buffer path. The caller must not assume that an unlinked or removed port yields a usable persistent address. The buffer's format and size are defined by the port type and current cycle. A client should stop new work, deactivate as appropriate, handle outstanding callbacks and only then release port and callback data; closing a client from another thread while an unsynchronized callback uses its state can race.

Assume a graph rate of 48 kHz and a callback of 256 frames. The interval is approximately 5.333 ms. If all relevant graph processing and scheduling must fit in that cycle, an application callback that blocks for 10 ms cannot reliably meet the intended schedule. The budget available to one client can be less than the whole interval because other nodes and overhead also consume time. This is a model, not a measured deadline for a particular graph. Avoid allocation, filesystem access, blocking locks and unbounded logging in time-critical callbacks; move non-real-time work to a control thread through a bounded, correctly synchronized handoff.

Suppose an output port loses its link between two cycles. A graph notification can tell the application that connectivity changed; the process callback still has to handle the current port-buffer contract and not assume a remote peer exists. If a port is unregistered or the client is shutting down, a pointer retained from the previous callback is stale. The program should stop using it, coordinate with the callback thread and release its own state after no callback can reference it. The exact order and notification details depend on the JACK implementation and API version.

JACK transport and timebase APIs expose concepts such as position, rolling state and a timebase master. Latency APIs and metadata expose further information, but implementations can differ in supported features, precision, graph integration and policy. A PulseAudio application and a JACK application can coexist in a PipeWire graph without having identical timing models. A successful ABI call is not proof that every application-specific transport, metadata or latency assumption matches a different implementation.

Compatibility limits and investigation

Compatibility can preserve a protocol or function signature while changing scheduling, buffering, conversion and failure timing. Two servers can accept the same client request yet select different buffer sizes, conversion paths or devices. Even with the same nominal sample rate, asynchronous clocks and resampling can change timing. Sample-identical output would require a controlled end-to-end test with specific data and configuration; it cannot be inferred from a library name. Likewise, a protocol version and successful stream creation do not prove that every extension or edge case is implemented identically.

When diagnosing an application, first identify which library the process loaded and which server endpoint it connected to using an appropriately authorized environment. Then distinguish client state, compatibility-server state, graph links, negotiated format and device behavior. A missing target can be a policy or permission issue rather than a codec issue. In this lesson we do not inspect a live process, list the host graph, change library paths, run a server, or open an audio device. The source path and model provide a reproducible learning route without exposing other clients' audio information.

A useful failure table has separate rows for local client initialization, connection, request parsing, stream creation, graph negotiation, active processing and teardown. A local object can exist even when the server is unavailable; a remote stream can exist before it has a working graph link; a linked stream can underrun later. Each row should name who owns callbacks, buffers and descriptors and what signal or return value marks the transition. This prevents a single “audio works” label from hiding the precise layer that failed.

Practice#

This pure Python model translates the fictional Pulse buffer sizes and checks a hypothetical callback duration. It opens no socket or audio device:

sh
python3 - <<'PY'
rate, channels, sample_bytes = 48000, 2, 2
quantum, requested_bytes = 256, 4096
frame_bytes = channels * sample_bytes
print('buffer frames:', requested_bytes // frame_bytes)
print('quantum ms:', round(1000 * quantum / rate, 3))
callback_ms = 10
print('callback misses model interval:', callback_ms > 1000 * quantum / rate)
PY

Expected output is 1,024 frames, 5.333 ms and True. As a failure case, a requested byte count not divisible by four cannot represent a whole number of these stereo frames without additional rules; the model must not silently claim otherwise. The source-only alternative is the 1,024/256-frame example above: list the requested attributes, effective values if changed by a server, underflow state and cleanup owner. The model is not a Pulse or JACK execution.

Knowledge check#

  1. Does a PulseAudio-protocol client necessarily connect to the PulseAudio daemon?
  2. Why does the PipeWire server reject both a sink index and a sink name in the quoted branch?
  3. What does the JACK callback registration path do for an active client?
  4. Can a JACK port buffer pointer be kept indefinitely after the callback?
  5. Why is a 256-frame callback period not proof of total output latency?

Answers#

  1. No. PipeWire's compatibility module can speak the protocol; the chosen server and implementation depend on configuration.
  2. The shown implementation treats the simultaneous concrete target selectors as invalid instead of guessing which one takes precedence.
  3. It logs the condition and returns an error before storing the new callback.
  4. No. The pointer is tied to the current cycle and port lifecycle; graph changes or teardown can invalidate it.
  5. It is a nominal cycle interval. Additional graph, server, device and conversion buffering can contribute to end-to-end delay.

Checkpoint#

Submit the frame/byte calculation, a Pulse underflow state table and a JACK hot-unlink cleanup sequence. Pass when requested and effective attributes are distinct, callback storage has a lifetime, and compatibility is not labeled identical timing. If not, reread the two implementation excerpts and retry. This is practice, not certification.

Sources#

See PipeWire Pulse protocol server, PipeWire Pulse stream, PipeWire JACK implementation, and PulseAudio client stream. No daemon, device or live graph was contacted.

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.