From 39dd760c609f3bf9838bda3cd33643eb1371fb95 Mon Sep 17 00:00:00 2001 From: Jonas Holmberg Date: Tue, 20 Jan 2026 15:02:40 +0100 Subject: [PATCH 01/93] impl-port: Free capabilities Free capabilities when destroying port. --- src/pipewire/impl-port.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c index 9d49a8097..ed6fe9f1e 100644 --- a/src/pipewire/impl-port.c +++ b/src/pipewire/impl-port.c @@ -1595,6 +1595,8 @@ void pw_impl_port_destroy(struct pw_impl_port *port) pw_param_clear(&impl->pending_list, SPA_ID_INVALID); free(port->tag[SPA_DIRECTION_INPUT]); free(port->tag[SPA_DIRECTION_OUTPUT]); + free(port->cap[SPA_DIRECTION_INPUT]); + free(port->cap[SPA_DIRECTION_OUTPUT]); pw_map_clear(&port->mix_port_map); From ba3e564e3431078f6fea86190ee339e4f036c885 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 Jan 2026 13:36:32 +0100 Subject: [PATCH 02/93] filter-graph: notify about default numner of in/out The filter graph has, after parsing, a default number of input and output ports. This is based on the description or the first/last element input and output ports. Pass this information in the properties when we emit the info. Don't use the number of configured input/output ports as the default number of channels in filter-chain because this is only determined after activating the graph. Instead, use the default input/output channels. The result is that when you load filter-chain without any channel layout, it will default to the number of input/outputs of the graph instead of 0. This allows for the node to be visible in the pulseaudio API. Fixes #5084 --- spa/plugins/filter-graph/filter-graph.c | 45 ++++++++++++++++++------- src/modules/module-filter-chain.c | 27 +++++++++------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index 9271ace34..bf2a57f7f 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -193,6 +193,9 @@ struct graph { struct volume volume[2]; + uint32_t default_inputs; + uint32_t default_outputs; + uint32_t n_inputs; uint32_t n_outputs; uint32_t inputs_position[MAX_CHANNELS]; @@ -257,16 +260,23 @@ static void emit_filter_graph_info(struct impl *impl, bool full) impl->info.change_mask = impl->info_all; if (impl->info.change_mask || full) { char n_inputs[64], n_outputs[64], latency[64]; + char n_default_inputs[64], n_default_outputs[64]; struct spa_dict_item items[6]; struct spa_dict dict = SPA_DICT(items, 0); char in_pos[MAX_CHANNELS * 8]; char out_pos[MAX_CHANNELS * 8]; + /* these are the current graph inputs/outputs */ snprintf(n_inputs, sizeof(n_inputs), "%d", impl->graph.n_inputs); snprintf(n_outputs, sizeof(n_outputs), "%d", impl->graph.n_outputs); + /* these are the default number of graph inputs/outputs */ + snprintf(n_default_inputs, sizeof(n_default_inputs), "%d", impl->graph.default_inputs); + snprintf(n_default_outputs, sizeof(n_default_outputs), "%d", impl->graph.default_outputs); items[dict.n_items++] = SPA_DICT_ITEM("n_inputs", n_inputs); items[dict.n_items++] = SPA_DICT_ITEM("n_outputs", n_outputs); + items[dict.n_items++] = SPA_DICT_ITEM("n_default_inputs", n_default_inputs); + items[dict.n_items++] = SPA_DICT_ITEM("n_default_outputs", n_default_outputs); if (graph->n_inputs_position) { print_channels(in_pos, sizeof(in_pos), graph->n_inputs_position, graph->inputs_position); @@ -1796,19 +1806,8 @@ static int setup_graph(struct graph *graph) first = spa_list_first(&graph->node_list, struct node, link); last = spa_list_last(&graph->node_list, struct node, link); - /* calculate the number of inputs and outputs into the graph. - * If we have a list of inputs/outputs, just use them. Otherwise - * we count all input ports of the first node and all output - * ports of the last node */ - if (graph->n_input_names != 0) - n_input = graph->n_input_names; - else - n_input = first->desc->n_input; - - if (graph->n_output_names != 0) - n_output = graph->n_output_names; - else - n_output = last->desc->n_output; + n_input = graph->default_inputs; + n_output = graph->default_outputs; /* we allow unconnected ports when not explicitly given and the nodes support * NULL data */ @@ -2083,6 +2082,7 @@ static int load_graph(struct graph *graph, const struct spa_dict *props) struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = NULL; struct spa_json ivolumes, ovolumes, *pivolumes = NULL, *povolumes = NULL; struct spa_json nodes, *pnodes = NULL, links, *plinks = NULL; + struct node *first, *last; const char *json, *val; char key[256]; int res, len; @@ -2232,6 +2232,25 @@ static int load_graph(struct graph *graph, const struct spa_dict *props) } if ((res = setup_graph_controls(graph)) < 0) return res; + + first = spa_list_first(&graph->node_list, struct node, link); + last = spa_list_last(&graph->node_list, struct node, link); + + /* calculate the number of inputs and outputs into the graph. + * If we have a list of inputs/outputs, just use them. Otherwise + * we count all input ports of the first node and all output + * ports of the last node */ + if (graph->n_input_names != 0) + graph->default_inputs = graph->n_input_names; + else + graph->default_inputs = first->desc->n_input; + + if (graph->n_output_names != 0) + graph->default_outputs = graph->n_output_names; + else + graph->default_outputs = last->desc->n_output; + + return 0; } diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index eb6a6c8ef..9a723a859 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -1711,20 +1711,11 @@ static void graph_info(void *object, const struct spa_filter_graph_info *info) { struct impl *impl = object; struct spa_dict *props = info->props; - uint32_t i; - - if (impl->capture_info.channels == 0) - impl->capture_info.channels = info->n_inputs; - if (impl->playback_info.channels == 0) - impl->playback_info.channels = info->n_outputs; + uint32_t i, val = 0; impl->n_inputs = info->n_inputs; impl->n_outputs = info->n_outputs; - if (impl->capture_info.channels == impl->playback_info.channels) { - copy_position(&impl->capture_info, &impl->playback_info); - copy_position(&impl->playback_info, &impl->capture_info); - } for (i = 0; props && i < props->n_items; i++) { const char *k = props->items[i].key; const char *s = props->items[i].value; @@ -1738,6 +1729,22 @@ static void graph_info(void *object, const struct spa_filter_graph_info *info) } } } + else if (spa_streq(k, "n_default_inputs") && + impl->capture_info.channels == 0 && + spa_atou32(s, &val, 0)) { + pw_log_info("using default inputs %d", val); + impl->capture_info.channels = val; + } + else if (spa_streq(k, "n_default_outputs") && + impl->playback_info.channels == 0 && + spa_atou32(s, &val, 0)) { + pw_log_info("using default outputs %d", val); + impl->playback_info.channels = val; + } + } + if (impl->capture_info.channels == impl->playback_info.channels) { + copy_position(&impl->capture_info, &impl->playback_info); + copy_position(&impl->playback_info, &impl->capture_info); } } From a97c4d10af5ee95371717b268a1d5be120244c30 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 Jan 2026 13:51:38 +0100 Subject: [PATCH 03/93] filter-graph: allow 0 input and output ports There is no reason to fail when there is no input or output port. We can simply run the graph with what there is. Even if there is no input or output at all, running one instance of the plugins is possible. Add a busy builtin plugin that has no ports and keeps the CPU IDLE or busy for the give percent. --- spa/plugins/filter-graph/filter-graph.c | 28 +++--- spa/plugins/filter-graph/plugin_builtin.c | 104 ++++++++++++++++++++++ src/modules/module-filter-chain.c | 30 +++++++ 3 files changed, 144 insertions(+), 18 deletions(-) diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index bf2a57f7f..70de19d6b 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -1030,11 +1030,6 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, } } } - if (desc->n_input == 0 && desc->n_output == 0 && desc->n_control == 0 && desc->n_notify == 0) { - spa_log_error(impl->log, "plugin has no input and no output ports"); - res = -ENOTSUP; - goto exit; - } for (i = 0; i < desc->n_control; i++) { p = desc->control[i]; desc->default_control[i] = get_default(impl, desc, p); @@ -1794,7 +1789,7 @@ static int setup_graph(struct graph *graph) struct port *port; struct graph_port *gp; struct graph_hndl *gh; - uint32_t i, j, n, n_input, n_output, n_hndl = 0; + uint32_t i, j, n, n_input, n_output, n_hndl = 0, n_out_hndl; int res; struct descriptor *desc; const struct spa_fga_descriptor *d; @@ -1815,16 +1810,11 @@ static int setup_graph(struct graph *graph) SPA_FLAG_IS_SET(first->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) && SPA_FLAG_IS_SET(last->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA); - if (n_input == 0) { - spa_log_error(impl->log, "no inputs"); - res = -EINVAL; - goto error; - } - if (n_output == 0) { - spa_log_error(impl->log, "no outputs"); - res = -EINVAL; - goto error; - } + if (n_input == 0) + n_input = n_output; + if (n_output == 0) + n_output = n_input; + if (graph->n_inputs == 0) graph->n_inputs = impl->info.n_inputs; if (graph->n_inputs == 0) @@ -1835,12 +1825,14 @@ static int setup_graph(struct graph *graph) /* compare to the requested number of inputs and duplicate the * graph n_hndl times when needed. */ - n_hndl = graph->n_inputs / n_input; + n_hndl = n_input ? graph->n_inputs / n_input : 1; if (graph->n_outputs == 0) graph->n_outputs = n_output * n_hndl; - if (n_hndl != graph->n_outputs / n_output) { + n_out_hndl = n_output ? graph->n_outputs / n_output : 1; + + if (n_hndl != n_out_hndl) { spa_log_error(impl->log, "invalid ports. The input stream has %1$d ports and " "the filter has %2$d inputs. The output stream has %3$d ports " "and the filter has %4$d outputs. input:%1$d / input:%2$d != " diff --git a/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index 8a964ccd7..69ed34fc2 100644 --- a/spa/plugins/filter-graph/plugin_builtin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -3115,6 +3116,107 @@ static const struct spa_fga_descriptor noisegate_desc = { .cleanup = builtin_cleanup, }; +/* busy */ +struct busy_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + + float wait_scale; + float cpu_scale; +}; + +static void *busy_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct busy_impl *impl; + struct spa_json it[1]; + const char *val; + char key[256]; + float wait_percent = 0.0f, cpu_percent = 0.0f; + int len; + + if (config != NULL) { + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "busy:config must be an object"); + return NULL; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "wait-percent")) { + if (spa_json_parse_float(val, len, &wait_percent) <= 0) { + spa_log_error(pl->log, "busy:wait-percent requires a number"); + return NULL; + } + } else if (spa_streq(key, "cpu-percent")) { + if (spa_json_parse_float(val, len, &cpu_percent) <= 0) { + spa_log_error(pl->log, "busy:cpu-percent requires a number"); + return NULL; + } + } else { + spa_log_warn(pl->log, "busy: ignoring config key: '%s'", key); + } + } + if (wait_percent <= 0.0f) + wait_percent = 0.0f; + if (cpu_percent <= 0.0f) + cpu_percent = 0.0f; + } + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->rate = SampleRate; + impl->wait_scale = wait_percent * SPA_NSEC_PER_SEC / (100.0f * SampleRate); + impl->cpu_scale = cpu_percent * SPA_NSEC_PER_SEC / (100.0f * SampleRate); + spa_log_info(impl->log, "wait-percent:%f cpu-percent:%f", wait_percent, cpu_percent); + + return impl; +} + +static void busy_run(void * Instance, unsigned long SampleCount) +{ + struct busy_impl *impl = Instance; + struct timespec ts; + uint64_t busy_nsec; + + if (impl->wait_scale > 0.0f) { + busy_nsec = (uint64_t)(impl->wait_scale * SampleCount); + ts.tv_sec = busy_nsec / SPA_NSEC_PER_SEC; + ts.tv_nsec = busy_nsec % SPA_NSEC_PER_SEC; + clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL); + } + if (impl->cpu_scale > 0.0f) { + clock_gettime(CLOCK_MONOTONIC, &ts); + busy_nsec = SPA_TIMESPEC_TO_NSEC(&ts); + busy_nsec += (uint64_t)(impl->cpu_scale * SampleCount); + do { + clock_gettime(CLOCK_MONOTONIC, &ts); + } while ((uint64_t)SPA_TIMESPEC_TO_NSEC(&ts) < busy_nsec); + } +} + +static const struct spa_fga_descriptor busy_desc = { + .name = "busy", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = 0, + .ports = NULL, + + .instantiate = busy_instantiate, + .connect_port = builtin_connect_port, + .run = busy_run, + .cleanup = builtin_cleanup, +}; + static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) { switch(Index) { @@ -3180,6 +3282,8 @@ static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) return &zeroramp_desc; case 30: return &noisegate_desc; + case 31: + return &busy_desc; } return NULL; } diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 9a723a859..9ab3639bd 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -652,6 +652,36 @@ extern struct spa_handle_factory spa_filter_graph_factory; * of "Attack (s)" seconds. The noise gate stays open for at least "Hold (s)" * seconds before it can close again. * + * ### Busy + * + * The `busy` plugin has no input or output ports and it can be used to keep the + * CPU or graph busy for the given percent of time. + * + * The node requires a `config` section with extra configuration: + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = builtin + * name = ... + * label = busy + * config = { + * wait-percent = 0.0 + * cpu-percent = 50.0 + * } + * ... + * } + * } + * ... + * } + *\endcode + * + * - `wait-percent` the percentage of time to wait. This keeps the graph busy but + * not the CPU. Default 0.0 + * - `cpu-percent` the percentage of time to keep the CPU busy. This keeps both the + * graph and CPU busy. Default 0.0 + * * * ## SOFA filters * From 7f2cce1021acc0733695920b12fcd6b7e5939216 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 Jan 2026 16:14:45 +0100 Subject: [PATCH 04/93] filter-graph: add a null plugin It discards all input. --- spa/plugins/filter-graph/plugin_builtin.c | 31 +++++++++++++++++++++++ src/modules/module-filter-chain.c | 4 +++ 2 files changed, 35 insertions(+) diff --git a/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index 69ed34fc2..3bcde30c9 100644 --- a/spa/plugins/filter-graph/plugin_builtin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -3217,6 +3217,35 @@ static const struct spa_fga_descriptor busy_desc = { .cleanup = builtin_cleanup, }; +/* null */ +static void null_run(void * Instance, unsigned long SampleCount) +{ +} + +static struct spa_fga_port null_ports[] = { + { .index = 0, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, +}; + +static const struct spa_fga_descriptor null_desc = { + .name = "null", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(null_ports), + .ports = null_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = null_run, + .cleanup = builtin_cleanup, +}; + static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) { switch(Index) { @@ -3284,6 +3313,8 @@ static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) return &noisegate_desc; case 31: return &busy_desc; + case 32: + return &null_desc; } return NULL; } diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 9ab3639bd..ddb014315 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -682,6 +682,10 @@ extern struct spa_handle_factory spa_filter_graph_factory; * - `cpu-percent` the percentage of time to keep the CPU busy. This keeps both the * graph and CPU busy. Default 0.0 * + * ### Null + * + * The `null` plugin has one data input "In" and one control input "Control" that + * simply discards the data. * * ## SOFA filters * From 56a4ab5234857f50ac14aa541c8d3f9e3afcc558 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 Jan 2026 16:15:08 +0100 Subject: [PATCH 05/93] filter-chain: support no input or output streams When the graph has no inputs and the channels is set to 0, don't create a capture stream. Likewise, don't create a playback stream when there are no graph outputs and the output channels is 0. You can use this to make a sine source or a null sink. --- src/modules/module-filter-chain.c | 292 +++++++++++++++++------------- 1 file changed, 163 insertions(+), 129 deletions(-) diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index ddb014315..6453baeee 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -193,6 +193,10 @@ extern struct spa_handle_factory spa_filter_graph_factory; * graph will then be duplicated as many times to match the number of input/output * channels of the streams. * + * If the graph has no inputs and the capture channels is set as 0, only the + * playback stream will be created. Likewise, if there are no outputs and the + * playback channels is 0, there will be no capture stream created. + * * ### Volumes * * Normally the volume of the sink/source is handled by the stream software volume. @@ -1249,92 +1253,105 @@ static void capture_destroy(void *d) impl->capture = NULL; } -static void capture_process(void *d) +static void do_process(struct impl *impl) { - struct impl *impl = d; - int res; - if ((res = pw_stream_trigger_process(impl->playback)) < 0) { - pw_log_debug("playback trigger error: %s", spa_strerror(res)); + struct pw_buffer *in, *out; + uint32_t i, n_in = 0, n_out = 0, data_size = 0; + struct spa_data *bd; + const void *cin[128]; + void *cout[128]; + + in = out = NULL; + if (impl->capture) { while (true) { struct pw_buffer *t; if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) break; - /* playback part is not ready, consume, discard and recycle - * the capture buffers */ - pw_stream_queue_buffer(impl->capture, t); + if (in) + pw_stream_queue_buffer(impl->capture, in); + in = t; } + if (in == NULL) { + pw_log_debug("%p: out of capture buffers: %m", impl); + } else { + for (i = 0; i < in->buffer->n_datas; i++) { + uint32_t offs, size; + + bd = &in->buffer->datas[i]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->chunk->size, bd->maxsize - offs); + + cin[n_in++] = SPA_PTROFF(bd->data, offs, void); + + data_size = i == 0 ? size : SPA_MIN(data_size, size); + } + } + } + if (impl->playback) { + out = pw_stream_dequeue_buffer(impl->playback); + if (out == NULL) { + pw_log_debug("%p: out of playback buffers: %m", impl); + } else { + if (data_size == 0) + data_size = out->requested * sizeof(float); + + for (i = 0; i < out->buffer->n_datas; i++) { + bd = &out->buffer->datas[i]; + + data_size = SPA_MIN(data_size, bd->maxsize); + + cout[n_out++] = bd->data; + + bd->chunk->offset = 0; + bd->chunk->size = data_size; + bd->chunk->stride = sizeof(float); + } + } + pw_log_trace_fp("%p: size:%d requested:%"PRIu64, impl, + data_size, out->requested); + } + + for (; n_in < impl->n_inputs; i++) + cin[n_in++] = NULL; + for (; n_out < impl->n_outputs; i++) + cout[n_out++] = NULL; + + if (impl->graph_active) + spa_filter_graph_process(impl->graph, cin, cout, data_size / sizeof(float)); + + if (in != NULL) + pw_stream_queue_buffer(impl->capture, in); + if (out != NULL) + pw_stream_queue_buffer(impl->playback, out); +} + +static void capture_process(void *d) +{ + struct impl *impl = d; + int res; + + if (impl->playback) { + if ((res = pw_stream_trigger_process(impl->playback)) < 0) { + pw_log_debug("playback trigger error: %s", spa_strerror(res)); + while (impl->capture) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) + break; + /* playback part is not ready, consume, discard and recycle + * the capture buffers */ + pw_stream_queue_buffer(impl->capture, t); + } + } + } else { + do_process(impl); } } static void playback_process(void *d) { struct impl *impl = d; - struct pw_buffer *in, *out; - uint32_t i, data_size = 0; - int32_t stride = 0; - struct spa_data *bd; - const void *cin[128]; - void *cout[128]; - - in = NULL; - while (true) { - struct pw_buffer *t; - if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) - break; - if (in) - pw_stream_queue_buffer(impl->capture, in); - in = t; - } - if (in == NULL) - pw_log_debug("%p: out of capture buffers: %m", impl); - - if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL) - pw_log_debug("%p: out of playback buffers: %m", impl); - - if (in == NULL || out == NULL) - goto done; - - for (i = 0; i < in->buffer->n_datas; i++) { - uint32_t offs, size; - - bd = &in->buffer->datas[i]; - - offs = SPA_MIN(bd->chunk->offset, bd->maxsize); - size = SPA_MIN(bd->chunk->size, bd->maxsize - offs); - - cin[i] = SPA_PTROFF(bd->data, offs, void); - - data_size = i == 0 ? size : SPA_MIN(data_size, size); - stride = SPA_MAX(stride, bd->chunk->stride); - } - for (; i < impl->n_inputs; i++) - cin[i] = NULL; - - for (i = 0; i < out->buffer->n_datas; i++) { - bd = &out->buffer->datas[i]; - - data_size = SPA_MIN(data_size, bd->maxsize); - - cout[i] = bd->data; - - bd->chunk->offset = 0; - bd->chunk->size = data_size; - bd->chunk->stride = stride; - } - for (; i < impl->n_outputs; i++) - cout[i] = NULL; - - pw_log_trace_fp("%p: stride:%d size:%d requested:%"PRIu64" (%"PRIu64")", impl, - stride, data_size, out->requested, out->requested * stride); - - if (impl->graph_active) - spa_filter_graph_process(impl->graph, cin, cout, data_size / sizeof(float)); - -done: - if (in != NULL) - pw_stream_queue_buffer(impl->capture, in); - if (out != NULL) - pw_stream_queue_buffer(impl->playback, out); + do_process(impl); } static int activate_graph(struct impl *impl) @@ -1403,6 +1420,9 @@ static void update_latency(struct impl *impl, enum spa_direction direction, bool struct pw_stream *s = direction == SPA_DIRECTION_OUTPUT ? impl->playback : impl->capture; + if (s == NULL) + return; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); latency = impl->latency[direction]; spa_process_latency_info_add(&impl->process_latency, &latency); @@ -1459,10 +1479,13 @@ static void param_tag_changed(struct impl *impl, const struct spa_pod *param, if (param == 0 || spa_tag_parse(param, &tag, &state) < 0) return; - if (tag.direction == SPA_DIRECTION_INPUT) - pw_stream_update_params(impl->capture, params, 1); - else - pw_stream_update_params(impl->playback, params, 1); + if (tag.direction == SPA_DIRECTION_INPUT) { + if (impl->capture) + pw_stream_update_params(impl->capture, params, 1); + } else { + if (impl->playback) + pw_stream_update_params(impl->playback, params, 1); + } } static void capture_state_changed(void *data, enum pw_stream_state old, @@ -1539,8 +1562,7 @@ static void param_changed(struct impl *impl, uint32_t id, const struct spa_pod * return; error: - pw_stream_set_error(direction == SPA_DIRECTION_INPUT ? impl->capture : impl->playback, - res, "can't start graph: %s", spa_strerror(res)); + pw_stream_set_error(stream, res, "can't start graph: %s", spa_strerror(res)); } static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param) @@ -1599,7 +1621,7 @@ static void playback_state_changed(void *data, enum pw_stream_state old, } return; error: - pw_stream_set_error(impl->capture, res, "can't start graph: %s", + pw_stream_set_error(impl->playback, res, "can't start graph: %s", spa_strerror(res)); } @@ -1628,43 +1650,39 @@ static const struct pw_stream_events out_stream_events = { static int setup_streams(struct impl *impl) { int res; - uint32_t i, n_params, *offs; + uint32_t i, n_params, *offs, flags; struct pw_array offsets; const struct spa_pod **params = NULL; struct spa_pod_dynamic_builder b; struct spa_filter_graph *graph = impl->graph; - impl->capture = pw_stream_new(impl->core, - "filter capture", impl->capture_props); - impl->capture_props = NULL; - if (impl->capture == NULL) - return -errno; + if (impl->capture_info.channels > 0) { + impl->capture = pw_stream_new(impl->core, + "filter capture", impl->capture_props); + impl->capture_props = NULL; + if (impl->capture == NULL) + return -errno; - pw_stream_add_listener(impl->capture, - &impl->capture_listener, - &in_stream_events, impl); + pw_stream_add_listener(impl->capture, + &impl->capture_listener, + &in_stream_events, impl); + } - impl->playback = pw_stream_new(impl->core, - "filter playback", impl->playback_props); - impl->playback_props = NULL; - if (impl->playback == NULL) - return -errno; + if (impl->playback_info.channels > 0) { + impl->playback = pw_stream_new(impl->core, + "filter playback", impl->playback_props); + impl->playback_props = NULL; + if (impl->playback == NULL) + return -errno; - pw_stream_add_listener(impl->playback, - &impl->playback_listener, - &out_stream_events, impl); + pw_stream_add_listener(impl->playback, + &impl->playback_listener, + &out_stream_events, impl); + } spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); pw_array_init(&offsets, 512); - if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) == NULL) { - res = -errno; - goto done; - } - *offs = b.b.state.offset; - spa_format_audio_raw_build(&b.b, - SPA_PARAM_EnumFormat, &impl->capture_info); - for (i = 0;; i++) { uint32_t save = b.b.state.offset; if (spa_filter_graph_enum_prop_info(graph, i, &b.b, NULL) != 1) @@ -1687,7 +1705,7 @@ static int setup_streams(struct impl *impl) res = -ENOMEM; goto done; } - if ((params = calloc(n_params, sizeof(struct spa_pod*))) == NULL) { + if ((params = calloc(n_params+1, sizeof(struct spa_pod*))) == NULL) { res = -errno; goto done; } @@ -1696,32 +1714,44 @@ static int setup_streams(struct impl *impl) for (i = 0; i < n_params; i++) params[i] = spa_pod_builder_deref(&b.b, offs[i]); - res = pw_stream_connect(impl->capture, - PW_DIRECTION_INPUT, - PW_ID_ANY, - PW_STREAM_FLAG_AUTOCONNECT | + if (impl->capture) { + params[n_params++] = spa_format_audio_raw_build(&b.b, + SPA_PARAM_EnumFormat, &impl->capture_info); + flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | - PW_STREAM_FLAG_RT_PROCESS | - PW_STREAM_FLAG_ASYNC, - params, n_params); + PW_STREAM_FLAG_RT_PROCESS; + if (impl->playback) + flags |= PW_STREAM_FLAG_ASYNC; - spa_pod_dynamic_builder_clean(&b); - if (res < 0) - goto done; + res = pw_stream_connect(impl->capture, + PW_DIRECTION_INPUT, + PW_ID_ANY, + flags, + params, n_params); - n_params = 0; - spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); - params[n_params++] = spa_format_audio_raw_build(&b.b, - SPA_PARAM_EnumFormat, &impl->playback_info); + spa_pod_dynamic_builder_clean(&b); + if (res < 0) + goto done; - res = pw_stream_connect(impl->playback, - PW_DIRECTION_OUTPUT, - PW_ID_ANY, - PW_STREAM_FLAG_AUTOCONNECT | + n_params = 0; + spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); + } + if (impl->playback) { + params[n_params++] = spa_format_audio_raw_build(&b.b, + SPA_PARAM_EnumFormat, &impl->playback_info); + + flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | - PW_STREAM_FLAG_RT_PROCESS | - PW_STREAM_FLAG_TRIGGER, - params, n_params); + PW_STREAM_FLAG_RT_PROCESS; + if (impl->capture) + flags |= PW_STREAM_FLAG_TRIGGER; + + res = pw_stream_connect(impl->playback, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + flags, + params, n_params); + } spa_pod_dynamic_builder_clean(&b); done: @@ -1801,7 +1831,11 @@ static void graph_props_changed(void *object, enum spa_direction direction) spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); spa_filter_graph_get_props(graph, &b.b, (struct spa_pod **)¶ms[0]); - pw_stream_update_params(impl->capture, params, 1); + if (impl->capture) + pw_stream_update_params(impl->capture, params, 1); + else if (impl->playback) + pw_stream_update_params(impl->playback, params, 1); + spa_pod_dynamic_builder_clean(&b); } From c634ef9610022c21630905c193cc9ecbcfd9a736 Mon Sep 17 00:00:00 2001 From: Sven Ulland Date: Tue, 20 Jan 2026 17:15:36 +0100 Subject: [PATCH 06/93] pipewiresrc: Fix video crop metadata not being set Since commit a1f33a99df changed buffer handling to create new GstBuffers instead of reusing pool buffers, the video crop metadata was silently lost. The code used gst_buffer_get_video_crop_meta() which returns NULL on a fresh buffer, so the crop values from PipeWire were never applied. Change to gst_buffer_add_video_crop_meta() to actually attach the metadata to the buffer. Also remove the now-obsolete call in gst_pipewire_pool_wrap_buffer. This was discovered when using the XDG Desktop Portal's RemoteDesktop interface: the full desktop was being delivered instead of just the selected window, because the crop region metadata was not being propagated to the GStreamer buffer. Fixes: a1f33a99df ("gst: dequeue a shared buffer instead of original pool buffer"), from merge request !1258 CC: @jameshilliard --- src/gst/gstpipewirepool.c | 2 -- src/gst/gstpipewiresrc.c | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c index 5581c64ac..3bcf99de7 100644 --- a/src/gst/gstpipewirepool.c +++ b/src/gst/gstpipewirepool.c @@ -209,8 +209,6 @@ void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b) data->b = b; data->buf = buf; data->crop = spa_buffer_find_meta_data (b->buffer, SPA_META_VideoCrop, sizeof(*data->crop)); - if (data->crop) - gst_buffer_add_video_crop_meta(buf); data->videotransform = spa_buffer_find_meta_data (b->buffer, SPA_META_VideoTransform, sizeof(*data->videotransform)); data->cursor = spa_buffer_find_meta_data (b->buffer, SPA_META_Cursor, sizeof(*data->cursor)); diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index 8bfd799a1..b0de17dfd 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -781,7 +781,7 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) crop = data->crop; if (crop) { - GstVideoCropMeta *meta = gst_buffer_get_video_crop_meta(buf); + GstVideoCropMeta *meta = gst_buffer_add_video_crop_meta(buf); if (meta) { meta->x = crop->region.position.x; meta->y = crop->region.position.y; From f82f4945b907602abf1fca0955178b8d303abf1f Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 21 Jan 2026 18:35:36 +0100 Subject: [PATCH 07/93] filter-graph: improve min/max checks for control Clamp the control values to their min/max. Remove the default_control array, we can just restore the default with the port_set_control_value(). Do this when no value has been set on the port when we set up the graph. The avantage is that we calculate the default, min and max correctly when they depend on the graph rate. Fixes #5088 --- spa/plugins/filter-graph/filter-graph.c | 97 +++++++++++++------------ 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index 70de19d6b..52609f2c6 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -80,7 +80,6 @@ struct descriptor { unsigned long *output; unsigned long *control; unsigned long *notify; - float *default_control; }; struct port { @@ -94,6 +93,9 @@ struct port { uint32_t n_links; uint32_t external; + bool control_initialized; + + float control_current; float control_data[MAX_HNDL]; float *audio_data[MAX_HNDL]; void *audio_mem[MAX_HNDL]; @@ -349,12 +351,6 @@ static int impl_process(void *object, return 0; } -static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p) -{ - struct spa_fga_port *port = &desc->desc->ports[p]; - return port->def; -} - static struct node *find_node(struct graph *graph, const char *name) { struct node *node; @@ -443,6 +439,20 @@ static struct port *find_port(struct node *node, const char *name, int descripto return NULL; } +static void get_ranges(struct impl *impl, struct spa_fga_port *p, + float *def, float *min, float *max) +{ + uint32_t rate = impl->rate ? impl->rate : DEFAULT_RATE; + *def = p->def; + *min = p->min; + *max = p->max; + if (p->hint & SPA_FGA_HINT_SAMPLE_RATE) { + *def *= rate; + *min *= rate; + *max *= rate; + } +} + static int impl_enum_prop_info(void *object, uint32_t idx, struct spa_pod_builder *b, struct spa_pod **param) { @@ -457,7 +467,6 @@ static int impl_enum_prop_info(void *object, uint32_t idx, struct spa_pod_builde struct spa_fga_port *p; float def, min, max; char name[512]; - uint32_t rate = impl->rate ? impl->rate : DEFAULT_RATE; if (idx >= graph->n_control) return 0; @@ -468,15 +477,7 @@ static int impl_enum_prop_info(void *object, uint32_t idx, struct spa_pod_builde d = desc->desc; p = &d->ports[port->p]; - if (p->hint & SPA_FGA_HINT_SAMPLE_RATE) { - def = p->def * rate; - min = p->min * rate; - max = p->max * rate; - } else { - def = p->def; - min = p->min; - max = p->max; - } + get_ranges(impl, p, &def, &min, &max); if (node->name[0] != '\0') snprintf(name, sizeof(name), "%s:%s", node->name, p->name); @@ -575,41 +576,58 @@ static int impl_get_props(void *object, struct spa_pod_builder *b, struct spa_po return 1; } -static int port_set_control_value(struct port *port, float *value, uint32_t id) +static int port_id_set_control_value(struct port *port, uint32_t id, float value) { struct node *node = port->node; struct impl *impl = node->graph->impl; - struct descriptor *desc = node->desc; + struct spa_fga_port *p = &desc->desc->ports[port->p]; float old; bool changed; old = port->control_data[id]; - port->control_data[id] = value ? *value : desc->default_control[port->idx]; + port->control_data[id] = value; + spa_log_info(impl->log, "control %d %d ('%s') from %f to %f", port->idx, id, - desc->desc->ports[port->p].name, old, port->control_data[id]); + p->name, old, value); + changed = old != port->control_data[id]; node->control_changed |= changed; + return changed ? 1 : 0; } +static int port_set_control_value(struct port *port, float *value) +{ + struct node *node = port->node; + struct impl *impl = node->graph->impl; + struct spa_fga_port *p; + float v, def, min, max; + uint32_t i; + int count = 0; + + p = &node->desc->desc->ports[port->p]; + get_ranges(impl, p, &def, &min, &max); + v = SPA_CLAMP(value ? *value : def, min, max); + + port->control_current = v; + port->control_initialized = true; + + for (i = 0; i < node->n_hndl; i++) + count += port_id_set_control_value(port, i, v); + + return count; +} + static int set_control_value(struct node *node, const char *name, float *value) { struct port *port; - int count = 0; - uint32_t i, n_hndl; port = find_port(node, name, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL); if (port == NULL) return -ENOENT; - /* if we don't have any instances yet, set the first control value, we will - * copy to other instances later */ - n_hndl = SPA_MAX(1u, port->node->n_hndl); - for (i = 0; i < n_hndl; i++) - count += port_set_control_value(port, value, i); - - return count; + return port_set_control_value(port, value); } static int parse_params(struct graph *graph, const struct spa_pod *pod) @@ -716,7 +734,7 @@ static int sync_volume(struct graph *graph, struct volume *vol) v = v * (vol->max[n_port] - vol->min[n_port]) + vol->min[n_port]; n_hndl = SPA_MAX(1u, p->node->n_hndl); - res += port_set_control_value(p, &v, i % n_hndl); + res += port_id_set_control_value(p, i % n_hndl, v); } return res; } @@ -935,7 +953,6 @@ static void descriptor_unref(struct descriptor *desc) free(desc->input); free(desc->output); free(desc->control); - free(desc->default_control); free(desc->notify); free(desc); } @@ -946,7 +963,7 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, struct plugin *pl; struct descriptor *desc; const struct spa_fga_descriptor *d; - uint32_t i, n_input, n_output, n_control, n_notify; + uint32_t n_input, n_output, n_control, n_notify; unsigned long p; int res; @@ -1000,7 +1017,6 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, desc->input = calloc(n_input, sizeof(unsigned long)); desc->output = calloc(n_output, sizeof(unsigned long)); desc->control = calloc(n_control, sizeof(unsigned long)); - desc->default_control = calloc(n_control, sizeof(float)); desc->notify = calloc(n_notify, sizeof(unsigned long)); for (p = 0; p < d->n_ports; p++) { @@ -1030,12 +1046,6 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, } } } - for (i = 0; i < desc->n_control; i++) { - p = desc->control[i]; - desc->default_control[i] = get_default(impl, desc, p); - spa_log_info(impl->log, "control %d ('%s') default to %f", i, - d->ports[p].name, desc->default_control[i]); - } spa_list_append(&pl->descriptor_list, &desc->link); return desc; @@ -1415,7 +1425,6 @@ static int load_node(struct graph *graph, struct spa_json *json) port->external = SPA_ID_INVALID; port->p = desc->control[i]; spa_list_init(&port->link_list); - port->control_data[0] = desc->default_control[i]; } for (i = 0; i < desc->n_notify; i++) { struct port *port = &node->notify_port[i]; @@ -2020,11 +2029,9 @@ static int setup_graph(struct graph *graph) } } for (i = 0; i < desc->n_control; i++) { - /* any default values for the controls are set in the first instance - * of the control data. Duplicate this to the other instances now. */ struct port *port = &node->control_port[i]; - for (j = 1; j < n_hndl; j++) - port->control_data[j] = port->control_data[0]; + port_set_control_value(port, + port->control_initialized ? &port->control_current : NULL); } } res = 0; From f978b702b13d042f481232ccc73a27c05257d035 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 22 Jan 2026 13:08:06 +0100 Subject: [PATCH 08/93] pw-cat: also look at simple formats for extension The simple formats contain some common mappings for other extensions such as mp3. Makes pw-record test.mp3 actually write an mp3 instead of a wav file. --- src/tools/pw-cat.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index bdd44a255..ab72fd3d6 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -1700,6 +1700,25 @@ static void format_from_filename(SF_INFO *info, const char *filename) break; } } + if (format == -1) { + if (sf_command(NULL, SFC_GET_SIMPLE_FORMAT_COUNT, &count, sizeof(int)) != 0) + count = 0; + + for (i = 0; i < count; i++) { + SF_FORMAT_INFO fi; + + spa_zero(fi); + fi.format = i; + if (sf_command(NULL, SFC_GET_SIMPLE_FORMAT, &fi, sizeof(fi)) != 0) + continue; + + if (spa_strendswith(filename, fi.extension)) { + format = fi.format; + info->format = 0; + break; + } + } + } if (format == -1) format = spa_streq(filename, "-") ? SF_FORMAT_AU : SF_FORMAT_WAV; if (format == SF_FORMAT_WAV && info->channels > 2) From 04793138b50717cd543cdbdd1f5df663e4623eff Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Wed, 21 Jan 2026 15:23:39 -0800 Subject: [PATCH 09/93] alsa-card-profiles: Add config for a couple of JBL gaming headsets Similar to the existing SteelSeries and Logitech ones, but the order of the playback endpoints is reversed, and only mono input is supported. --- spa/plugins/alsa/90-pipewire-alsa.rules | 5 ++ .../usb-gaming-headset-gamefirst.conf | 70 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset-gamefirst.conf diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules index b2e1f6886..8c986d070 100644 --- a/spa/plugins/alsa/90-pipewire-alsa.rules +++ b/spa/plugins/alsa/90-pipewire-alsa.rules @@ -187,6 +187,11 @@ ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0300", ENV{ACP_PROFILE_SET}="usb-gam # Sennheiser GSP 670 USB headset ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# JBL Quantum One +ATTRS{idVendor}=="0ecb", ATTRS{idProduct}=="203a", ENV{ACP_PROFILE_SET}="usb-gaming-headset-gamefirst.conf" +# JBL Quantum 810 Wireless +ATTRS{idVendor}=="0ecb", ATTRS{idProduct}=="2069", ENV{ACP_PROFILE_SET}="usb-gaming-headset-gamefirst.conf" + # Audioengine HD3 powered speakers support IEC958 but don't actually # have any digital outputs. ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf" diff --git a/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset-gamefirst.conf b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset-gamefirst.conf new file mode 100644 index 000000000..9192c6864 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset-gamefirst.conf @@ -0,0 +1,70 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; USB gaming headset. +; These headsets usually have two output devices. The first one is meant +; for general audio, and the second one is meant for chat. There is also +; a single input device for chat. +; The purpose of this unusual design is to provide separate volume +; controls for voice and other audio, which can be useful in gaming. +; +; Works with: +; JBL Quantum 810 Wireless +; JBL Quantum One +; +; Based on usb-gaming-headset.conf. +; +; See default.conf for an explanation on the directives used here. + +[General] +auto-profiles = yes + +[Mapping mono-chat-output] +description-key = gaming-headset-chat +device-strings = hw:%f,1,0 +channel-map = mono +paths-output = usb-gaming-headset-output-mono +intended-roles = phone + +[Mapping stereo-chat-output] +description-key = gaming-headset-chat +device-strings = hw:%f,1,0 +channel-map = left,right +paths-output = usb-gaming-headset-output-stereo +intended-roles = phone + +[Mapping mono-chat-input] +description-key = gaming-headset-chat +device-strings = hw:%f,0,0 +channel-map = mono +paths-input = usb-gaming-headset-input +intended-roles = phone + +[Mapping stereo-game-output] +description-key = gaming-headset-game +device-strings = hw:%f,0,0 +channel-map = left,right +paths-output = usb-gaming-headset-output-stereo +direction = output + +[Profile output:mono-chat+output:stereo-game+input:mono-chat] +output-mappings = mono-chat-output stereo-game-output +input-mappings = mono-chat-input +priority = 5100 + +[Profile output:stereo-game+output:stereo-chat+input:mono-chat] +output-mappings = stereo-game-output stereo-chat-output +input-mappings = mono-chat-input +priority = 5100 From 8600721de068d1bebce458ee5a97b3942fcd238c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20=C3=85dahl?= Date: Thu, 22 Jan 2026 14:39:46 +0100 Subject: [PATCH 10/93] stream: Change PW_CAPABILITY_DEVICE_ID_NEGOTIATION to a version number This allows easier evolvement of the negotiation protocol. --- doc/dox/internals/dma-buf.dox | 7 ++++--- src/examples/video-play-fixate.c | 7 ++++--- src/examples/video-src-fixate.c | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/dox/internals/dma-buf.dox b/doc/dox/internals/dma-buf.dox index 273be930e..7815a8c42 100644 --- a/doc/dox/internals/dma-buf.dox +++ b/doc/dox/internals/dma-buf.dox @@ -313,12 +313,13 @@ performed. Device ID negotiation needs explicit support by both end points of a stream, thus, the first step of negotiation is discovering whether other peer has support for it. This is done by advertising a \ref SPA_PARAM_Capability with the key \ref -PW_CAPABILITY_DEVICE_ID_NEGOTIATION and value `true` +PW_CAPABILITY_DEVICE_ID_NEGOTIATION and value `1` which corresponds to the +current negotiation API version. ``` spa_param_dict_build_dict(&b, SPA_PARAM_Capability, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "true"))); + SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"))); ``` To do this, when connecting to the stream, the \ref PW_STREAM_FLAG_INACTIVE flag must be @@ -369,7 +370,7 @@ To achieve this, the consumer adds another \ref SPA_PARAM_PeerCapability item wi ``` char *device_ids = ...; /* Base 64 encoding of a dev_t. */. &SPA_DICT_ITEMS( - SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "true"), + SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"), SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_IDS, device_ids))); ``` diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c index 6fdc561e6..2ee71a941 100644 --- a/src/examples/video-play-fixate.c +++ b/src/examples/video-play-fixate.c @@ -438,8 +438,9 @@ discover_capabilities(struct data *data, const struct spa_pod *param) return; spa_dict_for_each(it, &dict) { - if (spa_streq(it->key, PW_CAPABILITY_DEVICE_ID_NEGOTIATION) && - spa_streq(it->value, "true")) { + if (spa_streq(it->key, PW_CAPABILITY_DEVICE_ID_NEGOTIATION)) { + int version = atoi(it->value); + if (version >= 1) data->device_negotiation_supported = true; } else if (spa_streq(it->key, PW_CAPABILITY_DEVICE_IDS)) { collect_device_ids(data, it->value); @@ -787,7 +788,7 @@ int main(int argc, char *argv[]) params[n_params++] = spa_param_dict_build_dict(&b, SPA_PARAM_Capability, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "true"))); + SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"))); #endif /* now connect the stream, we need a direction (input/output), diff --git a/src/examples/video-src-fixate.c b/src/examples/video-src-fixate.c index 0671ab1be..0e072c27b 100644 --- a/src/examples/video-src-fixate.c +++ b/src/examples/video-src-fixate.c @@ -450,8 +450,9 @@ discover_capabilities(struct data *data, const struct spa_pod *param) return; spa_dict_for_each(it, &dict) { - if (spa_streq(it->key, PW_CAPABILITY_DEVICE_ID_NEGOTIATION) && - spa_streq(it->value, "true")) { + if (spa_streq(it->key, PW_CAPABILITY_DEVICE_ID_NEGOTIATION)) { + int version = atoi(it->value); + if (version >= 1) data->device_negotiation_supported = true; } } @@ -799,7 +800,7 @@ int main(int argc, char *argv[]) params[n_params++] = spa_param_dict_build_dict(&b, SPA_PARAM_Capability, - &SPA_DICT_ITEMS(SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "true"), + &SPA_DICT_ITEMS(SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"), #ifdef SUPPORT_DEVICE_IDS_LIST SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_IDS, device_ids) #endif /* SUPPORT_DEVICE_IDS_LIST */ From f2c4452e8dcf2b92f789185e3057b1effb5a3100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20=C3=85dahl?= Date: Thu, 22 Jan 2026 16:55:34 +0100 Subject: [PATCH 11/93] stream: Make capability device IDs an JSON object Also change the encoding of the byte array to use hexadecimal encoding, i.e. the byte array [100, 200] in becomes "64c8". --- doc/dox/internals/dma-buf.dox | 7 +++- src/examples/base64.h | 46 ----------------------- src/examples/utils.h | 60 ++++++++++++++++++++++++++++++ src/examples/video-play-fixate.c | 64 ++++++++++++++++++-------------- src/examples/video-src-fixate.c | 13 ++++--- 5 files changed, 110 insertions(+), 80 deletions(-) delete mode 100644 src/examples/base64.h create mode 100644 src/examples/utils.h diff --git a/doc/dox/internals/dma-buf.dox b/doc/dox/internals/dma-buf.dox index 7815a8c42..5042f285c 100644 --- a/doc/dox/internals/dma-buf.dox +++ b/doc/dox/internals/dma-buf.dox @@ -365,10 +365,13 @@ with. This can be used to reduce the amount of devices that are queried for form metadata, which can be a time consuming task, if devices needs to be woken up. To achieve this, the consumer adds another \ref SPA_PARAM_PeerCapability item with the key -\ref PW_CAPABILITY_DEVICE_IDS set to a string of base 64 encoded `dev_t` device IDs. +\ref PW_CAPABILITY_DEVICE_IDS set to a JSON object describing what device IDs are supported. + +This JSON object as of version 1 contains a single key "available-devices" that contain +a list of hexadecimal encoded `dev_t` device IDs. ``` - char *device_ids = ...; /* Base 64 encoding of a dev_t. */. + char *device_ids = "{\"available-devices\": [\"6464000000000000\",\"c8c8000000000000\"]}"; &SPA_DICT_ITEMS( SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"), SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_IDS, device_ids))); diff --git a/src/examples/base64.h b/src/examples/base64.h deleted file mode 100644 index 50e6d64b2..000000000 --- a/src/examples/base64.h +++ /dev/null @@ -1,46 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -static inline void base64_encode(const uint8_t *data, size_t len, char *enc, char pad) -{ - static const char tab[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - size_t i; - for (i = 0; i < len; i += 3) { - uint32_t v; - v = data[i+0] << 16; - v |= (i+1 < len ? data[i+1] : 0) << 8; - v |= (i+2 < len ? data[i+2] : 0); - *enc++ = tab[(v >> (3*6)) & 0x3f]; - *enc++ = tab[(v >> (2*6)) & 0x3f]; - *enc++ = i+1 < len ? tab[(v >> (1*6)) & 0x3f] : pad; - *enc++ = i+2 < len ? tab[(v >> (0*6)) & 0x3f] : pad; - } - *enc = '\0'; -} - -static inline size_t base64_decode(const char *data, size_t len, uint8_t *dec) -{ - uint8_t tab[] = { - 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, - 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, - -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, - -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; - size_t i, j; - for (i = 0, j = 0; i < len; i += 4) { - uint32_t v; - v = tab[data[i+0]-43] << (3*6); - v |= tab[data[i+1]-43] << (2*6); - v |= (data[i+2] == '=' ? 0 : tab[data[i+2]-43]) << (1*6); - v |= (data[i+3] == '=' ? 0 : tab[data[i+3]-43]); - dec[j++] = (v >> 16) & 0xff; - if (data[i+2] != '=') dec[j++] = (v >> 8) & 0xff; - if (data[i+3] != '=') dec[j++] = v & 0xff; - } - return j; -} diff --git a/src/examples/utils.h b/src/examples/utils.h new file mode 100644 index 000000000..079f22d55 --- /dev/null +++ b/src/examples/utils.h @@ -0,0 +1,60 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Red Hat */ +/* SPDX-License-Identifier: MIT */ + +static inline char * +encode_hex(const uint8_t *data, size_t size) +{ + FILE *ms; + char *encoded = NULL; + size_t encoded_size = 0; + size_t i; + + ms = open_memstream(&encoded, &encoded_size); + for (i = 0; i < size; i++) { + fprintf(ms, "%02x", data[i]); + } + fclose(ms); + + return encoded; +} + +static inline int8_t +ascii_hex_to_hex(uint8_t ascii_hex) +{ + if (ascii_hex >= '0' && ascii_hex <= '9') + return ascii_hex - '0'; + else if (ascii_hex >= 'a' && ascii_hex <= 'f') + return ascii_hex - 'a' + 10; + else if (ascii_hex >= 'A' && ascii_hex <= 'F') + return ascii_hex - 'A' + 10; + else + return -1; +} + +static inline int +decode_hex(const char *encoded, uint8_t *data, size_t size) +{ + size_t length; + size_t i; + + length = strlen(encoded); + + if (size < (length / 2) * sizeof(uint8_t)) + return -1; + + i = 0; + while (i < length) { + int8_t top = ascii_hex_to_hex(encoded[i]); + int8_t bottom = ascii_hex_to_hex(encoded[i + 1]); + + if (top == -1 || bottom == -1) + return -1; + + uint8_t el = top << 4 | bottom; + data[i / 2] = el; + i += 2; + } + + return 1; +} diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c index 2ee71a941..13b617a59 100644 --- a/src/examples/video-play-fixate.c +++ b/src/examples/video-play-fixate.c @@ -29,7 +29,7 @@ #include #include -#include "base64.h" +#include "utils.h" /* Comment out to test device ID negotation backward compatibility. */ #define SUPPORT_DEVICE_ID_NEGOTIATION 1 @@ -372,46 +372,56 @@ collect_device_ids(struct data *data, const char *json) int len; const char *value; struct spa_json sub; + char key[1024]; if ((len = spa_json_begin(&it, json, strlen(json), &value)) <= 0) { fprintf(stderr, "invalid device IDs value\n"); return; } - if (!spa_json_is_array(value, len)) { - fprintf(stderr, "device IDs not array\n"); + if (!spa_json_is_object(value, len)) { + fprintf(stderr, "device IDs not object\n"); return; } spa_json_enter(&it, &sub); - while ((len = spa_json_next(&sub, &value)) > 0) { - char *string; - union { - dev_t device_id; - uint8_t buffer[1024]; - } dec; + while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { + struct spa_json devices_sub; - string = alloca(len + 1); - - if (!spa_json_is_string(value, len)) { - fprintf(stderr, "device ID not string\n"); + if (!spa_json_is_array(value, len)) { + fprintf(stderr, "available-devices not array\n"); return; } - if (spa_json_parse_string(value, len, string) <= 0) { - fprintf(stderr, "invalid device ID string\n"); - return; + spa_json_enter(&sub, &devices_sub); + while ((len = spa_json_next(&devices_sub, &value)) > 0) { + char *string; + union { + dev_t device_id; + uint8_t buffer[1024]; + } dec; + + string = alloca(len + 1); + + if (!spa_json_is_string(value, len)) { + fprintf(stderr, "device ID not string\n"); + return; + } + + if (spa_json_parse_string(value, len, string) <= 0) { + fprintf(stderr, "invalid device ID string\n"); + return; + } + + if (decode_hex(string, dec.buffer, sizeof (dec.buffer)) < 0) { + fprintf(stderr, "invalid device ID string\n"); + return; + } + + fprintf(stderr, "discovered device ID %u:%u\n", + major(dec.device_id), minor(dec.device_id)); + + data->device_ids[data->n_device_ids++] = dec.device_id; } - - if (base64_decode(string, strlen(string), - (uint8_t *)&dec.device_id) < sizeof(dev_t)) { - fprintf(stderr, "invalid device ID\n"); - return; - } - - fprintf(stderr, "discovered device ID %u:%u\n", - major(dec.device_id), minor(dec.device_id)); - - data->device_ids[data->n_device_ids++] = dec.device_id; } } diff --git a/src/examples/video-src-fixate.c b/src/examples/video-src-fixate.c index 0e072c27b..6d08074de 100644 --- a/src/examples/video-src-fixate.c +++ b/src/examples/video-src-fixate.c @@ -30,7 +30,7 @@ #include #include -#include "base64.h" +#include "utils.h" /* Comment out to test device ID negotation backward compatibility. */ #define SUPPORT_DEVICE_ID_NEGOTIATION 1 @@ -784,17 +784,20 @@ int main(int argc, char *argv[]) size_t i; ms = open_memstream(&device_ids, &device_ids_size); - fprintf(ms, "["); + fprintf(ms, "{\"available-devices\": ["); for (i = 0; i < SPA_N_ELEMENTS(devices); i++) { dev_t device_id = makedev(devices[i].major, devices[i].minor); - char device_id_encoded[256]; + char *device_id_encoded; + + device_id_encoded = encode_hex((const uint8_t *) &device_id, sizeof (device_id)); - base64_encode((const uint8_t *) &device_id, sizeof (device_id), device_id_encoded, '\0'); if (i > 0) fprintf(ms, ","); fprintf(ms, "\"%s\"", device_id_encoded); + + free(device_id_encoded); } - fprintf(ms, "]"); + fprintf(ms, "]}"); fclose(ms); #endif /* SUPPORT_DEVICE_IDS_LIST */ From 35817c0d850f71f02b3c0e43037d394c5b892caf Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 22 Jan 2026 16:31:01 +0100 Subject: [PATCH 12/93] pw-cat: support some more formats So that you can give an .oga extension and --format=opus to get an opus ogg file. --- src/tools/pw-cat.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index ab72fd3d6..8bd3e343c 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -211,6 +211,12 @@ static const struct format_info { { "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 }, { "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 }, { "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 }, + + { "mp1", SF_FORMAT_MPEG_LAYER_I, SPA_AUDIO_FORMAT_F32, 1 }, + { "mp2", SF_FORMAT_MPEG_LAYER_II, SPA_AUDIO_FORMAT_F32, 1 }, + { "mp3", SF_FORMAT_MPEG_LAYER_III, SPA_AUDIO_FORMAT_F32, 1 }, + { "vorbis", SF_FORMAT_VORBIS, SPA_AUDIO_FORMAT_F32, 1 }, + { "opus", SF_FORMAT_OPUS, SPA_AUDIO_FORMAT_F32, 1 }, }; static const struct format_info *format_info_by_name(const char *str) @@ -1678,12 +1684,6 @@ static void format_from_filename(SF_INFO *info, const char *filename) int i, count = 0; int format = -1; -#if __BYTE_ORDER == __BIG_ENDIAN - info->format |= SF_ENDIAN_BIG; -#else - info->format |= SF_ENDIAN_LITTLE; -#endif - if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0) count = 0; @@ -1724,12 +1724,18 @@ static void format_from_filename(SF_INFO *info, const char *filename) if (format == SF_FORMAT_WAV && info->channels > 2) format = SF_FORMAT_WAVEX; + switch (format & SF_FORMAT_TYPEMASK) { + case SF_FORMAT_OGG: + case SF_FORMAT_FLAC: + case SF_FORMAT_MPEG: + case SF_FORMAT_AIFF: + info->format |= SF_ENDIAN_FILE; + break; + default: + info->format |= SF_ENDIAN_CPU; + break; + } info->format |= format; - - if (format == SF_FORMAT_OGG || format == SF_FORMAT_FLAC) - info->format = (info->format & ~SF_FORMAT_ENDMASK) | SF_ENDIAN_FILE; - if (format == SF_FORMAT_OGG) - info->format = (info->format & ~SF_FORMAT_SUBMASK) | SF_FORMAT_VORBIS; } #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION From db7c74a042b89feb917313bd4347b6888a754a0c Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Fri, 16 Jan 2026 16:16:22 +0800 Subject: [PATCH 13/93] bluez5/backend-native: Add HFP/HSP hardware offload datapath configuration Add support for configuring the SCO hardware offload data path for HFP/HSP profiles using the Bluetooth SIG-specified procedure. This enables vendor-specific SCO offload integrations. Changes: - Add `bluez5.hw-offload-datapath` configuration property (default: 0) - Implement `sco_offload_btcodec()` to set BT_CODEC socket option - Add `SPA_BT_FEATURE_HW_OFFLOAD` quirk feature flag - Apply offload configuration when creating SCO sockets if quirk enabled - Document new property in pipewire-props.7.md The datapath ID is configurable via device parameters and only applied when the hardware offload feature flag is set in quirks, allowing platform-specific SCO offload implementations. --- doc/dox/config/pipewire-props.7.md | 9 ++++++ spa/plugins/bluez5/backend-native.c | 46 +++++++++++++++++++++++++++++ spa/plugins/bluez5/defs.h | 4 ++- spa/plugins/bluez5/quirks.c | 6 ++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index ed82b2f41..d7e75e33d 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1171,6 +1171,15 @@ in a platform-specific way. See `tests/examples/bt-pinephone.lua` in WirePlumber Do not enable this setting if you don't know what all this means, as it won't work. \endparblock +@PAR@ device-param bluez5.hw-offload-datapath # integer +\parblock +HFP/HSP hardware offload data path ID (default: 0). + +This feature configures the SCO hardware‑offload data path for HFP/HSP using the Bluetooth +SIG–specified procedure. It is intended for advanced setups and vendor integrations. Do not +edit this unless required; incorrect values can disable SCO offload. +\endparblock + @PAR@ monitor-prop bluez5.a2dp.opus.pro.channels = 3 # integer PipeWire Opus Pro audio profile channel count. diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 5c4a2b785..5dde972fc 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -112,6 +112,7 @@ struct impl { int hfp_default_speaker_volume; struct spa_source sco; + unsigned int hfphsp_sco_datapath; const struct spa_bt_quirks *quirks; @@ -297,6 +298,32 @@ static const struct media_codec *codec_list_best(struct impl *backend, struct sp return NULL; } +static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) +{ + int err; + char buffer[255]; + struct bt_codecs *codecs; + + spa_log_info(backend->log, "%s: sock(%d) msbc(%d)", __func__, sock, msbc); + + memset(buffer, 0, sizeof(buffer)); + codecs = (void *)buffer; + if (msbc) + codecs->codecs[0].id = 0x05; + else + codecs->codecs[0].id = 0x02; + codecs->num_codecs = 1; + codecs->codecs[0].data_path_id = backend->hfphsp_sco_datapath; + codecs->codecs[0].num_caps = 0x00; + + err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); + if (err < 0) + spa_log_error(backend->log, "%s: ERROR: %s (%d)", __func__, strerror(errno), errno); + else + spa_log_info(backend->log, "%s: set offload codec succeeded", __func__); + return err; +} + static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) { if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) @@ -2564,6 +2591,7 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte struct sockaddr_sco addr; socklen_t len; bdaddr_t src; + uint32_t bt_features; spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); if (sock < 0) { @@ -2595,6 +2623,11 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte } } + if (backend->quirks && + (spa_bt_quirks_get_features(backend->quirks, NULL, NULL, &bt_features) == 0) && + ((bt_features & (SPA_BT_FEATURE_HW_OFFLOAD)) != 0)) + sco_offload_btcodec(backend, sock, transparent); + return spa_steal_fd(sock); } @@ -4101,6 +4134,18 @@ static void parse_hfp_default_volumes(struct impl *backend, const struct spa_dic backend->hfp_default_speaker_volume = SPA_BT_VOLUME_HS_MAX; } +static void parse_sco_datapath(struct impl *backend, const struct spa_dict *info) +{ + uint32_t tmp; + const char *str; + + backend->hfphsp_sco_datapath = HFP_SCO_DEFAULT_DATAPATH; + + if ((str = spa_dict_lookup(info, "bluez5.hw-offload-datapath")) != NULL && + (tmp = atoi(str)) > 0) + backend->hfphsp_sco_datapath = tmp; +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_native_free, @@ -4163,6 +4208,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, parse_hfp_disable_nrec(backend, info); parse_hfp_default_volumes(backend, info); parse_hfp_pts(backend, info); + parse_sco_datapath(backend, info); #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE if (!dbus_connection_register_object_path(backend->conn, diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 3efec465a..4bc4fa3bb 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -135,7 +135,8 @@ extern "C" { #define PROFILE_HFP_AG "/Profile/HFPAG" #define PROFILE_HFP_HF "/Profile/HFPHF" -#define HSP_HS_DEFAULT_CHANNEL 3 +#define HSP_HS_DEFAULT_CHANNEL 3 +#define HFP_SCO_DEFAULT_DATAPATH 0 #define SOURCE_ID_BLUETOOTH 0x1 /* Bluetooth SIG */ #define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */ @@ -809,6 +810,7 @@ enum spa_bt_feature { SPA_BT_FEATURE_SBC_XQ = (1 << 5), SPA_BT_FEATURE_FASTSTREAM = (1 << 6), SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), + SPA_BT_FEATURE_HW_OFFLOAD = (1 << 8), }; struct spa_bt_quirks; diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c index c4b293e68..2a1c5e860 100644 --- a/spa/plugins/bluez5/quirks.c +++ b/spa/plugins/bluez5/quirks.c @@ -52,6 +52,7 @@ struct spa_bt_quirks { int force_sbc_xq; int force_faststream; int force_a2dp_duplex; + int force_hw_offload; char *device_rules; char *adapter_rules; @@ -69,6 +70,7 @@ static enum spa_bt_feature parse_feature(const char *str) { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, { "faststream", SPA_BT_FEATURE_FASTSTREAM }, { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, + { "hw-offload", SPA_BT_FEATURE_HW_OFFLOAD }, }; SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { if (spa_streq(str, f->key)) @@ -228,6 +230,7 @@ struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct s this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume"); this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); + this->force_hw_offload = parse_force_flag(info, "bluez5.hw-offload-sco"); if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) { spa_log_debug(this->log, "loading session manager provided data"); @@ -385,6 +388,9 @@ static int get_features(const struct spa_bt_quirks *this, if (this->force_a2dp_duplex != -1) SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); + if (this->force_hw_offload != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_OFFLOAD, this->force_hw_offload); + return 0; } From 2d6a7d2186e1ae383eb4a88eed33c55cb51fbe0e Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Sat, 17 Jan 2026 19:44:56 +0800 Subject: [PATCH 14/93] bluez5: fix format string in sco_offload_btcodec log message --- spa/plugins/bluez5/backend-native.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 5dde972fc..cba0fd4f3 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -304,7 +304,7 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) char buffer[255]; struct bt_codecs *codecs; - spa_log_info(backend->log, "%s: sock(%d) msbc(%d)", __func__, sock, msbc); + spa_log_info(backend->log, "sock(%d) msbc(%d)", sock, msbc); memset(buffer, 0, sizeof(buffer)); codecs = (void *)buffer; @@ -318,9 +318,9 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); if (err < 0) - spa_log_error(backend->log, "%s: ERROR: %s (%d)", __func__, strerror(errno), errno); + spa_log_error(backend->log, "ERROR: %s (%d)", strerror(errno), errno); else - spa_log_info(backend->log, "%s: set offload codec succeeded", __func__); + spa_log_info(backend->log, "set offload codec succeeded"); return err; } From 2b5d21da5b9c928f363ca2d6bb78c9278bd5bb0e Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:04:46 +0800 Subject: [PATCH 15/93] bluez5: simplify SCO datapath parsing with spa_atou32 --- spa/plugins/bluez5/backend-native.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index cba0fd4f3..312618081 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -4136,14 +4136,10 @@ static void parse_hfp_default_volumes(struct impl *backend, const struct spa_dic static void parse_sco_datapath(struct impl *backend, const struct spa_dict *info) { - uint32_t tmp; - const char *str; - backend->hfphsp_sco_datapath = HFP_SCO_DEFAULT_DATAPATH; - if ((str = spa_dict_lookup(info, "bluez5.hw-offload-datapath")) != NULL && - (tmp = atoi(str)) > 0) - backend->hfphsp_sco_datapath = tmp; + spa_atou32(spa_dict_lookup(info, "bluez5.hw-offload-datapath"), + &backend->hfphsp_sco_datapath, 10); } static const struct spa_bt_backend_implementation backend_impl = { From 78f16bc04b9444a88e69ac52d2c918d9b2e08761 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:15:26 +0800 Subject: [PATCH 16/93] bluez5: Remove hw-offload feature flag check and associated quirks The sco_offload_btcodec() function now returns void and only skips offload setup when using the default datapath, simplifying the logic and removing the need for explicit feature flag checks. --- spa/plugins/bluez5/backend-native.c | 11 +++++------ spa/plugins/bluez5/defs.h | 1 - spa/plugins/bluez5/quirks.c | 6 ------ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 312618081..e5bd5eb1c 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -298,12 +298,15 @@ static const struct media_codec *codec_list_best(struct impl *backend, struct sp return NULL; } -static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) +static void sco_offload_btcodec(struct impl *backend, int sock, bool msbc) { int err; char buffer[255]; struct bt_codecs *codecs; + if (backend->hfphsp_sco_datapath == HFP_SCO_DEFAULT_DATAPATH) + return; + spa_log_info(backend->log, "sock(%d) msbc(%d)", sock, msbc); memset(buffer, 0, sizeof(buffer)); @@ -321,7 +324,6 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) spa_log_error(backend->log, "ERROR: %s (%d)", strerror(errno), errno); else spa_log_info(backend->log, "set offload codec succeeded"); - return err; } static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) @@ -2623,10 +2625,7 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte } } - if (backend->quirks && - (spa_bt_quirks_get_features(backend->quirks, NULL, NULL, &bt_features) == 0) && - ((bt_features & (SPA_BT_FEATURE_HW_OFFLOAD)) != 0)) - sco_offload_btcodec(backend, sock, transparent); + sco_offload_btcodec(backend, sock, transparent); return spa_steal_fd(sock); } diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 4bc4fa3bb..afc56c920 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -810,7 +810,6 @@ enum spa_bt_feature { SPA_BT_FEATURE_SBC_XQ = (1 << 5), SPA_BT_FEATURE_FASTSTREAM = (1 << 6), SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), - SPA_BT_FEATURE_HW_OFFLOAD = (1 << 8), }; struct spa_bt_quirks; diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c index 2a1c5e860..c4b293e68 100644 --- a/spa/plugins/bluez5/quirks.c +++ b/spa/plugins/bluez5/quirks.c @@ -52,7 +52,6 @@ struct spa_bt_quirks { int force_sbc_xq; int force_faststream; int force_a2dp_duplex; - int force_hw_offload; char *device_rules; char *adapter_rules; @@ -70,7 +69,6 @@ static enum spa_bt_feature parse_feature(const char *str) { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, { "faststream", SPA_BT_FEATURE_FASTSTREAM }, { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, - { "hw-offload", SPA_BT_FEATURE_HW_OFFLOAD }, }; SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { if (spa_streq(str, f->key)) @@ -230,7 +228,6 @@ struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct s this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume"); this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); - this->force_hw_offload = parse_force_flag(info, "bluez5.hw-offload-sco"); if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) { spa_log_debug(this->log, "loading session manager provided data"); @@ -388,9 +385,6 @@ static int get_features(const struct spa_bt_quirks *this, if (this->force_a2dp_duplex != -1) SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); - if (this->force_hw_offload != -1) - SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_OFFLOAD, this->force_hw_offload); - return 0; } From 254620676f929a38bade32fb4293760b9bffeb97 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:17:11 +0800 Subject: [PATCH 17/93] bluez5: Use named constants for Bluetooth codec IDs Replace magic numbers (0x02, 0x05) with named constants BT_CODEC_CVSD and BT_CODEC_MSBC for better code readability. Also remove redundant zero initialization of num_caps field since the buffer is already memset to zero. --- spa/plugins/bluez5/backend-native.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index e5bd5eb1c..a791dec4a 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -63,6 +63,9 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native"); #define RFCOMM_MESSAGE_MAX_LENGTH 256 +#define BT_CODEC_CVSD 0x02 +#define BT_CODEC_MSBC 0x05 + enum { HFP_AG_INITIAL_CODEC_SETUP_NONE = 0, HFP_AG_INITIAL_CODEC_SETUP_SEND, @@ -312,12 +315,11 @@ static void sco_offload_btcodec(struct impl *backend, int sock, bool msbc) memset(buffer, 0, sizeof(buffer)); codecs = (void *)buffer; if (msbc) - codecs->codecs[0].id = 0x05; + codecs->codecs[0].id = BT_CODEC_MSBC; else - codecs->codecs[0].id = 0x02; + codecs->codecs[0].id = BT_CODEC_CVSD; codecs->num_codecs = 1; codecs->codecs[0].data_path_id = backend->hfphsp_sco_datapath; - codecs->codecs[0].num_caps = 0x00; err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); if (err < 0) From 332e35039d93780bfbc21fcb0f42cc29b7d42bbd Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:46:55 +0800 Subject: [PATCH 18/93] doc: Fix bluez5.hw-offload-datapath property type in documentation --- doc/dox/config/pipewire-props.7.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index d7e75e33d..52ac3e369 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1171,7 +1171,7 @@ in a platform-specific way. See `tests/examples/bt-pinephone.lua` in WirePlumber Do not enable this setting if you don't know what all this means, as it won't work. \endparblock -@PAR@ device-param bluez5.hw-offload-datapath # integer +@PAR@ monitor-prop bluez5.hw-offload-datapath # integer \parblock HFP/HSP hardware offload data path ID (default: 0). From bc53b6b34331435d395e17f405af588a85d2aed1 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Tue, 20 Jan 2026 09:58:29 +0800 Subject: [PATCH 19/93] bluez5: Remove unused bt_features variable in sco_create_socket. --- spa/plugins/bluez5/backend-native.c | 1 - 1 file changed, 1 deletion(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index a791dec4a..4d14183e7 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -2595,7 +2595,6 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte struct sockaddr_sco addr; socklen_t len; bdaddr_t src; - uint32_t bt_features; spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); if (sock < 0) { From 703380d62dc913e4b56ef2d060fbe6163403e1c1 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Mon, 26 Jan 2026 14:24:35 -0800 Subject: [PATCH 20/93] pulse-server: Fix querying after setting of mono mixdown --- src/modules/module-protocol-pulse/client.h | 1 + src/modules/module-protocol-pulse/message-handler.c | 3 +++ src/modules/module-protocol-pulse/pulse-server.c | 11 ++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h index 2c413e51c..7998e5deb 100644 --- a/src/modules/module-protocol-pulse/client.h +++ b/src/modules/module-protocol-pulse/client.h @@ -62,6 +62,7 @@ struct client { struct pw_manager_object *metadata_schema_sm_settings; bool have_force_mono_audio; + bool default_force_mono_audio; struct pw_manager_object *metadata_sm_settings; bool force_mono_audio; diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index 48c7c7be3..c3868230d 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -110,12 +110,15 @@ static int core_object_force_mono_output(struct client *client, const char *para if (spa_streq(params, "true")) { ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, METADATA_FEATURES_AUDIO_MONO, "Spa:String:JSON", "true"); + client->force_mono_audio = true; } else if (spa_streq(params, "false")) { ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, METADATA_FEATURES_AUDIO_MONO, "Spa:String:JSON", "false"); + client->force_mono_audio = false; } else if (spa_streq(params, "null")) { ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, METADATA_FEATURES_AUDIO_MONO, NULL, NULL); + client->force_mono_audio = client->default_force_mono_audio; } else { fprintf(response, "Value must be true, false, or clear"); return -EINVAL; diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index c22499718..830fbc371 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -973,8 +973,17 @@ static void manager_metadata(void *data, struct pw_manager_object *o, if (subject == PW_ID_CORE && o == client->metadata_routes) client_update_routes(client, key, value); if (subject == PW_ID_CORE && o == client->metadata_schema_sm_settings) { - if (spa_streq(key, METADATA_FEATURES_AUDIO_MONO)) + char default_[16]; + + if (spa_streq(key, METADATA_FEATURES_AUDIO_MONO)) { client->have_force_mono_audio = true; + + if (spa_json_str_object_find(value, strlen(value), + "default", default_, sizeof(default_)) < 0) + client->default_force_mono_audio = false; + else + client->default_force_mono_audio = spa_streq(default_, "true"); + } } if (subject == PW_ID_CORE && o == client->metadata_sm_settings) { if (spa_streq(key, METADATA_FEATURES_AUDIO_MONO)) From ed59342d28b0dd75a6f70b790cc1e9dd68678377 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Mon, 26 Jan 2026 14:44:30 -0800 Subject: [PATCH 21/93] pipewire-pulse: Expose bluetooth headset autoswitch config as a message Makes it easier for libpulse-based clients to modify this setting if they want. --- src/modules/module-protocol-pulse/client.h | 3 + src/modules/module-protocol-pulse/defs.h | 1 + .../module-protocol-pulse/message-handler.c | 61 ++++++++++++++++--- .../module-protocol-pulse/pulse-server.c | 12 ++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h index 7998e5deb..81d790b51 100644 --- a/src/modules/module-protocol-pulse/client.h +++ b/src/modules/module-protocol-pulse/client.h @@ -63,8 +63,11 @@ struct client { struct pw_manager_object *metadata_schema_sm_settings; bool have_force_mono_audio; bool default_force_mono_audio; + bool have_bluetooth_headset_autoswitch; + bool default_bluetooth_headset_autoswitch; struct pw_manager_object *metadata_sm_settings; bool force_mono_audio; + bool bluetooth_headset_autoswitch; uint32_t connect_tag; diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h index 51e2453ff..c333f21f5 100644 --- a/src/modules/module-protocol-pulse/defs.h +++ b/src/modules/module-protocol-pulse/defs.h @@ -324,5 +324,6 @@ static inline uint32_t port_type_value(const char *port_type) #define METADATA_TARGET_NODE "target.node" #define METADATA_TARGET_OBJECT "target.object" #define METADATA_FEATURES_AUDIO_MONO "node.features.audio.mono" +#define METADATA_BLUETOOTH_HEADSET_AUTOSWITCH "bluetooth.autoswitch-to-headset-profile" #endif /* PULSE_SERVER_DEFS_H */ diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index c3868230d..835a1d303 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -133,6 +133,48 @@ static int core_object_force_mono_output(struct client *client, const char *para } } +static int core_object_bluetooth_headset_autoswitch(struct client *client, const char *params, FILE *response) +{ + if (!client->have_bluetooth_headset_autoswitch) { + /* Not supported, return a null value to indicate that */ + fprintf(response, "null"); + return 0; + } + + if (!params || params[0] == '\0') { + /* No parameter => query the current value */ + fprintf(response, "%s", client->bluetooth_headset_autoswitch ? "true" : "false"); + return 0; + } else { + /* The caller is trying to set a value or clear with a null */ + int ret; + + if (spa_streq(params, "true")) { + ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, + METADATA_BLUETOOTH_HEADSET_AUTOSWITCH, "Spa:String:JSON", "true"); + client->bluetooth_headset_autoswitch = true; + } else if (spa_streq(params, "false")) { + ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, + METADATA_BLUETOOTH_HEADSET_AUTOSWITCH, "Spa:String:JSON", "false"); + client->bluetooth_headset_autoswitch = false; + } else if (spa_streq(params, "null")) { + ret = pw_manager_set_metadata(client->manager, client->metadata_sm_settings, PW_ID_CORE, + METADATA_BLUETOOTH_HEADSET_AUTOSWITCH, NULL, NULL); + client->bluetooth_headset_autoswitch = client->default_bluetooth_headset_autoswitch; + } else { + fprintf(response, "Value must be true, false, or clear"); + return -EINVAL; + } + + if (ret < 0) + fprintf(response, "Could not set metadata: %s", spa_strerror(ret)); + else + fprintf(response, "%s", params); + + return ret; + } +} + static int core_object_message_handler(struct client *client, struct pw_manager_object *o, const char *message, const char *params, FILE *response) { pw_log_debug(": core %p object message:'%s' params:'%s'", o, message, params); @@ -141,14 +183,15 @@ static int core_object_message_handler(struct client *client, struct pw_manager_ fprintf(response, "/core []\n" "available commands:\n" - " help this help\n" - " list-handlers show available object handlers\n" - " pipewire-pulse:malloc-info show malloc_info\n" - " pipewire-pulse:malloc-trim run malloc_trim\n" - " pipewire-pulse:log-level update log level with \n" - " pipewire-pulse:list-modules list all module names\n" - " pipewire-pulse:describe-module describe module info for \n" - " pipewire-pulse:force-mono-output force mono mixdown on all hardware outputs" + " help this help\n" + " list-handlers show available object handlers\n" + " pipewire-pulse:malloc-info show malloc_info\n" + " pipewire-pulse:malloc-trim run malloc_trim\n" + " pipewire-pulse:log-level update log level with \n" + " pipewire-pulse:list-modules list all module names\n" + " pipewire-pulse:describe-module describe module info for \n" + " pipewire-pulse:force-mono-output force mono mixdown on all hardware outputs\n" + " pipewire-pulse:bluetooth-headset-autoswitch use bluetooth headset mic if available" ); } else if (spa_streq(message, "list-handlers")) { bool first = true; @@ -211,6 +254,8 @@ static int core_object_message_handler(struct client *client, struct pw_manager_ } } else if (spa_streq(message, "pipewire-pulse:force-mono-output")) { return core_object_force_mono_output(client, params, response); + } else if (spa_streq(message, "pipewire-pulse:bluetooth-headset-autoswitch")) { + return core_object_bluetooth_headset_autoswitch(client, params, response); } else { return -ENOSYS; } diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 830fbc371..59610ef57 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -984,10 +984,22 @@ static void manager_metadata(void *data, struct pw_manager_object *o, else client->default_force_mono_audio = spa_streq(default_, "true"); } + + if (spa_streq(key, METADATA_BLUETOOTH_HEADSET_AUTOSWITCH)) { + client->have_bluetooth_headset_autoswitch = true; + + if (spa_json_str_object_find(value, strlen(value), + "default", default_, sizeof(default_)) < 0) + client->default_bluetooth_headset_autoswitch = false; + else + client->default_bluetooth_headset_autoswitch = spa_streq(default_, "true"); + } } if (subject == PW_ID_CORE && o == client->metadata_sm_settings) { if (spa_streq(key, METADATA_FEATURES_AUDIO_MONO)) client->force_mono_audio = spa_streq(value, "true"); + if (spa_streq(key, METADATA_BLUETOOTH_HEADSET_AUTOSWITCH)) + client->bluetooth_headset_autoswitch = spa_streq(value, "true"); } } From 2aecb49f508abffbc67774c6d1bff3048f1687c2 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 27 Jan 2026 10:17:34 +0100 Subject: [PATCH 22/93] pulse-server: use null to clear the value The message makes it seem that you can pass 'clear' to clear the setting but in fact you should pass 'null'. --- src/modules/module-protocol-pulse/message-handler.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index 835a1d303..b44d4f422 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -120,7 +120,7 @@ static int core_object_force_mono_output(struct client *client, const char *para METADATA_FEATURES_AUDIO_MONO, NULL, NULL); client->force_mono_audio = client->default_force_mono_audio; } else { - fprintf(response, "Value must be true, false, or clear"); + fprintf(response, "Value must be true, false, or null"); return -EINVAL; } @@ -162,7 +162,7 @@ static int core_object_bluetooth_headset_autoswitch(struct client *client, const METADATA_BLUETOOTH_HEADSET_AUTOSWITCH, NULL, NULL); client->bluetooth_headset_autoswitch = client->default_bluetooth_headset_autoswitch; } else { - fprintf(response, "Value must be true, false, or clear"); + fprintf(response, "Value must be true, false, or null"); return -EINVAL; } From 47dd57faa7253ba7161f4c75e6af472716566a2a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 29 Jan 2026 16:50:52 +0100 Subject: [PATCH 23/93] filter-graph: handle other SOFA errors as errno --- spa/plugins/filter-graph/plugin_sofa.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spa/plugins/filter-graph/plugin_sofa.c b/spa/plugins/filter-graph/plugin_sofa.c index 7ec73ea2b..14005dabd 100644 --- a/spa/plugins/filter-graph/plugin_sofa.c +++ b/spa/plugins/filter-graph/plugin_sofa.c @@ -168,11 +168,14 @@ static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const reason = "Only sources with MC supported"; errno = ENOTSUP; break; - default: case MYSOFA_INTERNAL_ERROR: errno = EIO; reason = "Internal error"; break; + default: + errno = ret; + reason = strerror(errno); + break; } spa_log_error(impl->log, "Unable to load HRTF from %s: %s (%d)", filename, reason, ret); goto error; From 69d882230390df2512f85e77add7b98b068e01c8 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 29 Jan 2026 16:52:02 +0100 Subject: [PATCH 24/93] filter-chain: tweak spatializer gain We're adding two signals together so half the gain to keep it at the same volume. --- src/daemon/filter-chain/spatializer-7.1.conf | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/daemon/filter-chain/spatializer-7.1.conf b/src/daemon/filter-chain/spatializer-7.1.conf index 944ed6205..931e80975 100644 --- a/src/daemon/filter-chain/spatializer-7.1.conf +++ b/src/daemon/filter-chain/spatializer-7.1.conf @@ -118,8 +118,30 @@ context.modules = [ } } - { type = builtin label = mixer name = mixL } - { type = builtin label = mixer name = mixR } + { type = builtin label = mixer name = mixL + control = { + "Gain 1" = 0.5 + "Gain 2" = 0.5 + "Gain 3" = 0.5 + "Gain 4" = 0.5 + "Gain 5" = 0.5 + "Gain 6" = 0.5 + "Gain 7" = 0.5 + "Gain 8" = 0.5 + } + } + { type = builtin label = mixer name = mixR + control = { + "Gain 1" = 0.5 + "Gain 2" = 0.5 + "Gain 3" = 0.5 + "Gain 4" = 0.5 + "Gain 5" = 0.5 + "Gain 6" = 0.5 + "Gain 7" = 0.5 + "Gain 8" = 0.5 + } + } ] links = [ # output From 1a478c7147816db3672ede0ae6cb321512a219e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Fri, 30 Jan 2026 10:36:26 +0100 Subject: [PATCH 25/93] bluez5: Fix stack smashing crash in remote_endpoint_update_props() Commit 2942bae0342259976dcb1344c6aa32fbc1e5534f introduced parsing of "SupportedFeatures" which uses a third DBusMessageIter pointer. *** stack smashing detected ***: terminated ==389050== ==389050== Process terminating with default action of signal 6 (SIGABRT) ==389050== at 0x4F57B2C: __pthread_kill_implementation (pthread_kill.c:44) ==389050== by 0x4F57B2C: __pthread_kill_internal (pthread_kill.c:78) ==389050== by 0x4F57B2C: pthread_kill@@GLIBC_2.34 (pthread_kill.c:89) ==389050== by 0x4EFE27D: raise (raise.c:26) ==389050== by 0x4EE18FE: abort (abort.c:79) ==389050== by 0x4EE27B5: __libc_message_impl.cold (libc_fatal.c:134) ==389050== by 0x4FEFC48: __fortify_fail (fortify_fail.c:24) ==389050== by 0x4FF0ED3: __stack_chk_fail (stack_chk_fail.c:24) ==389050== by 0xBC1D1A1: remote_endpoint_update_props (bluez5-dbus.c:3137) ==389050== by 0xB53609F: ??? ==389050== by 0x1DF: ??? ==389050== by 0x61C17BF: ??? (in /usr/lib/x86_64-linux-gnu/libdbus-1.so.3.32.4) ==389050== by 0x1DF: ??? ==389050== by 0xC5ED113: ??? --- spa/plugins/bluez5/bluez5-dbus.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 926b1b3c4..6b5fb173e 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -3102,11 +3102,13 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en spa_log_debug(monitor->log, "remote_endpoint %p: %s=%"PRIu64, remote_endpoint, key, remote_endpoint->hisyncid); } else if (spa_streq(key, "SupportedFeatures")) { + DBusMessageIter iter; + if (!check_iter_signature(&it[1], "a{sv}")) goto next; - dbus_message_iter_recurse(&it[1], &it[2]); - parse_supported_features(monitor, &it[2], &remote_endpoint->bap_features); + dbus_message_iter_recurse(&it[1], &iter); + parse_supported_features(monitor, &iter, &remote_endpoint->bap_features); } else { unhandled: spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key); From f34a87fe38347e4c85291ec4455c812e754a6de4 Mon Sep 17 00:00:00 2001 From: Alexander Sarmanow Date: Fri, 30 Jan 2026 16:39:25 +0100 Subject: [PATCH 26/93] bluez5: bap: add support for per adapter broadcast config By setting the hci handle (e.g. hci0) of the desired adapter, the BIG config will only applied on that adapter. In case no "adapter" entry in the config is given, it will be applied on all adapters. Signed-off-by: Alexander Sarmanow --- doc/dox/config/pipewire-props.7.md | 1 + spa/plugins/bluez5/bluez5-dbus.c | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index 52ac3e369..9cf3943ed 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1211,6 +1211,7 @@ PipeWire Opus Pro audio profile duplex max bitrate. PipeWire Opus Pro audio profile duplex frame duration (1/10 ms). @PAR@ monitor-prop bluez5.bcast_source.config = [] # JSON +For a per-adapter configuration of multiple BIGs use an "adapter" entry in the BIG with the HCI device name (e.g. hci0). \parblock Example: ``` diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 6b5fb173e..0bb5a1a88 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -194,6 +194,7 @@ struct spa_bt_bis { }; #define BROADCAST_CODE_LEN 16 +#define HCI_DEV_NAME_LEN 8 struct spa_bt_big { struct spa_list link; @@ -202,6 +203,7 @@ struct spa_bt_big { struct spa_list bis_list; int big_id; int sync_factor; + char adapter[HCI_DEV_NAME_LEN]; }; /* @@ -6244,8 +6246,20 @@ static void configure_bcast_source(struct spa_bt_monitor *monitor, { struct spa_bt_big *big; struct spa_bt_bis *bis; + char *pos; /* Configure each BIS from a BIG */ spa_list_for_each(big, &monitor->bcast_source_config_list, link) { + /* Apply per adapter configuration if BIG has an adapter value stated, + * otherwise apply the BIG config angnostically to each adapter + */ + if (strlen(big->adapter) > 0) { + pos = strstr(object_path, big->adapter); + if (pos == NULL) + continue; + + spa_log_debug(monitor->log, "configuring BIG for adapter=%s", big->adapter); + } + spa_list_for_each(bis, &big->bis_list, link) { configure_bis(monitor, codec, conn, object_path, interface_name, big, bis, local_endpoint); @@ -6989,6 +7003,7 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const char bis_key[256]; char qos_key[256]; char bcode[BROADCAST_CODE_LEN + 3]; + char adapter[HCI_DEV_NAME_LEN + 3]; int cursor; int big_id = 0; struct spa_json it[3], it_array[4]; @@ -7024,6 +7039,13 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const goto parse_failed; memcpy(big_entry->broadcast_code, bcode, strlen(bcode)); spa_log_debug(monitor->log, "big_entry->broadcast_code %s", big_entry->broadcast_code); + } else if (spa_streq(key, "adapter")) { + if (spa_json_get_string(&it[1], adapter, sizeof(adapter)) <= 0) + goto parse_failed; + if (strlen(adapter) > HCI_DEV_NAME_LEN) + goto parse_failed; + memcpy(big_entry->adapter, adapter, sizeof(adapter)); + spa_log_debug(monitor->log, "big_entry->adapter %s", big_entry->adapter); } else if (spa_streq(key, "encryption")) { if (spa_json_get_bool(&it[0], &big_entry->encryption) <= 0) goto parse_failed; From a50e9a995e2dbddf31fd97e7ab100b4198f1f35c Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sat, 31 Jan 2026 17:27:24 +0200 Subject: [PATCH 27/93] pulse-server: disconnect from server on EPROTO If we get EPROTO, we likely have missed on some messages from the server, and our state is now out of sync. It's likely we can't recover (e.g. if error is due to fd limit hit), so just drop the server connection in this case, similarly as if we got EPIPE. --- src/modules/module-protocol-pulse/manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-protocol-pulse/manager.c b/src/modules/module-protocol-pulse/manager.c index 5b597f4eb..72d51b020 100644 --- a/src/modules/module-protocol-pulse/manager.c +++ b/src/modules/module-protocol-pulse/manager.c @@ -718,7 +718,7 @@ static void on_core_error(void *data, uint32_t id, int seq, int res, const char { struct manager *m = data; - if (id == PW_ID_CORE && res == -EPIPE) { + if (id == PW_ID_CORE && (res == -EPIPE || res == -EPROTO)) { pw_log_debug("connection error: %d, %s", res, message); manager_emit_disconnect(m); } From f60e03b4efd629591d8d73757b454f5301ed9374 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sat, 31 Jan 2026 15:23:44 +0200 Subject: [PATCH 28/93] pulse-server: use timeout also for creating sample-play streams Add 35 sec timeout for PLAY_SAMPLE streams to start streaming, similar to what we do with normal streams, and fail playback if they don't start. This avoids pending sample playback using up resources indefinitely if streams fail to start for some reason, e.g. session manager is not linking them. --- src/modules/module-protocol-pulse/defs.h | 2 ++ .../module-protocol-pulse/sample-play.c | 23 +++++++++++++++++++ .../module-protocol-pulse/sample-play.h | 3 +++ src/modules/module-protocol-pulse/stream.c | 2 +- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h index c333f21f5..f3e25f137 100644 --- a/src/modules/module-protocol-pulse/defs.h +++ b/src/modules/module-protocol-pulse/defs.h @@ -39,6 +39,8 @@ #define MODULE_INDEX_MASK 0xfffffffu #define MODULE_FLAG (1u << 29) +#define STREAM_CREATE_TIMEOUT (35 * SPA_NSEC_PER_SEC) + #define DEFAULT_SINK "@DEFAULT_SINK@" #define DEFAULT_SOURCE "@DEFAULT_SOURCE@" #define DEFAULT_MONITOR "@DEFAULT_MONITOR@" diff --git a/src/modules/module-protocol-pulse/sample-play.c b/src/modules/module-protocol-pulse/sample-play.c index db3893c0f..09b0e75cc 100644 --- a/src/modules/module-protocol-pulse/sample-play.c +++ b/src/modules/module-protocol-pulse/sample-play.c @@ -17,10 +17,12 @@ #include #include +#include "defs.h" #include "format.h" #include "log.h" #include "sample.h" #include "sample-play.h" +#include "internal.h" static void sample_play_stream_state_changed(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) @@ -30,17 +32,32 @@ static void sample_play_stream_state_changed(void *data, enum pw_stream_state ol switch (state) { case PW_STREAM_STATE_UNCONNECTED: case PW_STREAM_STATE_ERROR: + pw_timer_queue_cancel(&p->timer); sample_play_emit_done(p, -EIO); break; case PW_STREAM_STATE_PAUSED: p->id = pw_stream_get_node_id(p->stream); sample_play_emit_ready(p, p->id); break; + case PW_STREAM_STATE_STREAMING: + pw_timer_queue_cancel(&p->timer); + break; default: break; } } +static void sample_play_start_timeout(void *user_data) +{ + struct sample_play *p = user_data; + + pw_log_info("timeout on sample %s", p->sample->name); + + if (p->stream) + pw_stream_set_active(p->stream, false); + sample_play_emit_done(p, -ETIMEDOUT); +} + static void sample_play_stream_destroy(void *data) { struct sample_play *p = data; @@ -163,6 +180,10 @@ struct sample_play *sample_play_new(struct pw_core *core, if (res < 0) goto error_cleanup; + /* Time out if we don't get a link; same timeout as for normal streams */ + pw_timer_queue_add(sample->impl->timer_queue, &p->timer, NULL, + STREAM_CREATE_TIMEOUT, sample_play_start_timeout, p); + return p; error_cleanup: @@ -181,6 +202,8 @@ void sample_play_destroy(struct sample_play *p) spa_hook_list_clean(&p->hooks); + pw_timer_queue_cancel(&p->timer); + free(p); } diff --git a/src/modules/module-protocol-pulse/sample-play.h b/src/modules/module-protocol-pulse/sample-play.h index a34ca9213..98af7ee29 100644 --- a/src/modules/module-protocol-pulse/sample-play.h +++ b/src/modules/module-protocol-pulse/sample-play.h @@ -11,6 +11,8 @@ #include #include +#include + struct sample; struct pw_core; struct pw_loop; @@ -41,6 +43,7 @@ struct sample_play { uint32_t offset; uint32_t stride; struct spa_hook_list hooks; + struct pw_timer timer; void *user_data; }; diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c index 496bcb52c..a6beb9fd7 100644 --- a/src/modules/module-protocol-pulse/stream.c +++ b/src/modules/module-protocol-pulse/stream.c @@ -107,7 +107,7 @@ struct stream *stream_new(struct client *client, enum stream_type type, uint32_t /* Time out if we don't get a link and can't send a reply to create in 35s. Client will time out in * 30s and clean up its stream anyway. */ pw_timer_queue_add(stream->impl->timer_queue, &stream->timer, NULL, - 35 * SPA_NSEC_PER_SEC, create_stream_timeout, stream); + STREAM_CREATE_TIMEOUT, create_stream_timeout, stream); return stream; From 3c80f0fb3efc46a9690bdf3f4750df04020311dc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 2 Feb 2026 16:21:03 +0100 Subject: [PATCH 29/93] filter-graph: add gain option to sofa So that we can apply the gain to the IR. This is more efficient than doing a volume after the convolution. See #5098 --- spa/plugins/filter-graph/plugin_sofa.c | 20 ++++++++- src/daemon/filter-chain/spatializer-7.1.conf | 43 ++++++++++++-------- src/modules/module-filter-chain.c | 8 ++-- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/spa/plugins/filter-graph/plugin_sofa.c b/spa/plugins/filter-graph/plugin_sofa.c index 14005dabd..2e3a1ea3b 100644 --- a/spa/plugins/filter-graph/plugin_sofa.c +++ b/spa/plugins/filter-graph/plugin_sofa.c @@ -32,6 +32,7 @@ struct spatializer_impl { unsigned long rate; float *port[7]; int n_samples, blocksize, tailsize; + float gain; float *tmp[2]; struct MYSOFA_EASY *sofa; @@ -71,6 +72,7 @@ static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const impl->plugin = pl; impl->dsp = pl->dsp; impl->log = pl->log; + impl->gain = 1.0f; while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { if (spa_streq(key, "blocksize")) { @@ -94,6 +96,13 @@ static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const goto error; } } + else if (spa_streq(key, "gain")) { + if (spa_json_parse_float(val, len, &impl->gain) <= 0) { + spa_log_error(impl->log, "spatializer:gain requires a number"); + errno = EINVAL; + goto error; + } + } } if (!filename[0]) { spa_log_error(impl->log, "spatializer:filename was not given"); @@ -186,8 +195,8 @@ static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const if (impl->tailsize <= 0) impl->tailsize = SPA_CLAMP(4096, impl->blocksize, 32768); - spa_log_info(impl->log, "using n_samples:%u %d:%d blocksize sofa:%s", impl->n_samples, - impl->blocksize, impl->tailsize, filename); + spa_log_info(impl->log, "using n_samples:%u %d:%d blocksize gain:%f sofa:%s", impl->n_samples, + impl->blocksize, impl->tailsize, impl->gain, filename); impl->tmp[0] = calloc(impl->plugin->quantum_limit, sizeof(float)); impl->tmp[1] = calloc(impl->plugin->quantum_limit, sizeof(float)); @@ -253,6 +262,13 @@ static void spatializer_reload(void * Instance) if (impl->r_conv[2]) convolver_free(impl->r_conv[2]); + if (impl->gain != 1.0f) { + for (int i = 0; i < impl->n_samples; i++) { + left_ir[i] *= impl->gain; + right_ir[i] *= impl->gain; + } + } + impl->l_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize, left_ir, impl->n_samples); impl->r_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize, diff --git a/src/daemon/filter-chain/spatializer-7.1.conf b/src/daemon/filter-chain/spatializer-7.1.conf index 931e80975..bb19d5443 100644 --- a/src/daemon/filter-chain/spatializer-7.1.conf +++ b/src/daemon/filter-chain/spatializer-7.1.conf @@ -19,6 +19,8 @@ context.modules = [ name = spFL config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + # The gain depends on the .sofa file in use + gain = 0.5 } control = { "Azimuth" = 30.0 @@ -32,6 +34,7 @@ context.modules = [ name = spFR config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 330.0 @@ -45,6 +48,7 @@ context.modules = [ name = spFC config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 0.0 @@ -58,6 +62,7 @@ context.modules = [ name = spRL config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 150.0 @@ -71,6 +76,7 @@ context.modules = [ name = spRR config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 210.0 @@ -84,6 +90,7 @@ context.modules = [ name = spSL config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 90.0 @@ -97,6 +104,7 @@ context.modules = [ name = spSR config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 270.0 @@ -110,6 +118,7 @@ context.modules = [ name = spLFE config = { filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + gain = 0.5 } control = { "Azimuth" = 0.0 @@ -120,26 +129,28 @@ context.modules = [ { type = builtin label = mixer name = mixL control = { - "Gain 1" = 0.5 - "Gain 2" = 0.5 - "Gain 3" = 0.5 - "Gain 4" = 0.5 - "Gain 5" = 0.5 - "Gain 6" = 0.5 - "Gain 7" = 0.5 - "Gain 8" = 0.5 + # Set individual left mixer gain if needed + #"Gain 1" = 1.0 + #"Gain 2" = 1.0 + #"Gain 3" = 1.0 + #"Gain 4" = 1.0 + #"Gain 5" = 1.0 + #"Gain 6" = 1.0 + #"Gain 7" = 1.0 + #"Gain 8" = 1.0 } } { type = builtin label = mixer name = mixR control = { - "Gain 1" = 0.5 - "Gain 2" = 0.5 - "Gain 3" = 0.5 - "Gain 4" = 0.5 - "Gain 5" = 0.5 - "Gain 6" = 0.5 - "Gain 7" = 0.5 - "Gain 8" = 0.5 + # Set individual right mixer gain if needed + #"Gain 1" = 1.0 + #"Gain 2" = 1.0 + #"Gain 3" = 1.0 + #"Gain 4" = 1.0 + #"Gain 5" = 1.0 + #"Gain 6" = 1.0 + #"Gain 7" = 1.0 + #"Gain 8" = 1.0 } } ] diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 6453baeee..64a92c5ac 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -717,6 +717,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * blocksize = ... * tailsize = ... * filename = ... + * gain = ... * } * control = { * "Azimuth" = ... @@ -733,9 +734,10 @@ extern struct spa_handle_factory spa_filter_graph_factory; * - `blocksize` specifies the size of the blocks to use in the FFT. It is a value * between 64 and 256. When not specified, this value is * computed automatically from the number of samples in the file. - * - `tailsize` specifies the size of the tail blocks to use in the FFT. - * - `filename` The SOFA file to load. SOFA files usually end in the .sofa extension - * and contain the HRTF for the various spatial positions. + * - `tailsize` specifies the size of the tail blocks to use in the FFT. + * - `filename` The SOFA file to load. SOFA files usually end in the .sofa extension + * and contain the HRTF for the various spatial positions. + * - `gain` the overall gain to apply to the IR file. * * - `Azimuth` controls the azimuth, this is the direction the sound is coming from * in degrees between 0 and 360. 0 is straight ahead. 90 is left, 180 From cad1df748ee3b007f37ea08cbe340d59fc0079e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20P=C5=91cze?= Date: Mon, 2 Feb 2026 17:58:13 +0100 Subject: [PATCH 30/93] meson.build: define `SPA_AUDIO_MAX_CHANNELS` for C++ as well Previously the override was only present in `cc_flags`, meaning that C++ source files, like `aec-webrtc.cpp`, would not have it. Fixes: 6d74eee874ac ("spa: bump channels to 128 again") --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index e8b6f38dd..2e308cfa5 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,7 @@ common_flags = [ '-fvisibility=hidden', '-fno-strict-aliasing', '-fno-strict-overflow', + '-DSPA_AUDIO_MAX_CHANNELS=128u', '-Werror=suggest-attribute=format', '-Wsign-compare', '-Wpointer-arith', @@ -115,7 +116,6 @@ cc_flags = common_flags + [ '-Werror=old-style-definition', '-Werror=missing-parameter-type', '-Werror=strict-prototypes', - '-DSPA_AUDIO_MAX_CHANNELS=128u', ] add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') add_project_arguments(cc_native.get_supported_arguments(cc_flags), From a32e6e108c366580a08e00d616f60f299c1d9e8b Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Tue, 27 Jan 2026 21:24:43 +0100 Subject: [PATCH 31/93] Revert "module-rtp: Remove device_delay from timestamp math" This reverts commit dcdc19238be3a2dc354f28adbfb0258042640704. Reverting this because it caused big sync errors of ~62 ms in test setups. Further discussions about this can be found here: https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2666 Followup commits modify the device delay application (by scaling it), which is another reason why this needs to be reverted. --- src/modules/module-rtp/audio.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 13873f29c..0f2936b24 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -98,12 +98,9 @@ static void rtp_audio_process_playback(void *data) * pace of the driver. */ if (impl->io_position) { - /* Use the clock position directly as the read index. - * Do NOT add device_delay here - the sink's DLL handles - * matching its hardware clock to the driver pace. Adding - * device_delay would create a feedback loop since rate - * adjustments affect both ringbuffer and device buffer. */ - timestamp = impl->io_position->clock.position; + /* Shift clock position by stream delay to compensate + * for processing and output delay. */ + timestamp = impl->io_position->clock.position + device_delay; spa_ringbuffer_read_update(&impl->ring, timestamp); } else { /* In the unlikely case that no spa_io_position pointer From 95970e539efe9bd9476f91c00780d9586e431b40 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Sat, 17 Jan 2026 10:55:21 +0100 Subject: [PATCH 32/93] module-rtp: Fix invalid ring buffer read attempts in direct timestamp mode In corner cases where the read and write pointers are very close, it may not be possible to read out all the wanted samples. This can for example happen when there is a jump in the graph driver position. Currently, the code reads the wanted number of samples out of the ring buffer regardless of the write and read pointer positions. It does so even when the read pointer is ahead of the write pointer (that is, an underrun occurs). Fix this by checking the fill level and reading only the available amount of samples if that amount is less than the wanted amount. The remaining space in the target buffer is then filled with nullbytes. --- src/modules/module-rtp/audio.c | 78 +++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 0f2936b24..8dd0cec37 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -61,6 +61,8 @@ static void rtp_audio_process_playback(void *data) * read or write index itself.) */ if (impl->direct_timestamp) { + uint32_t num_samples_to_read; + /* In direct timestamp mode, the focus lies on synchronized playback, not * on a constant latency. The ring buffer fill level is not of interest * here. The code in rtp_audio_receive() writes to the ring buffer at @@ -89,19 +91,28 @@ static void rtp_audio_process_playback(void *data) * timestamp mode, since all of them shift the timestamp by the same * `sess.latency.msec` into the future. * - * "Fill level" makes no sense in this mode, since a constant latency - * is not important in this mode, so no DLL is needed. Also, matching - * the pace of the synchronized clock is done by having the graph - * driver be synchronized to that clock, which will in turn cause - * any output sinks to adjust their DLLs (or similar control loop - * mechanisms) to match the pace of their data consumption with the - * pace of the driver. */ + * Since in this mode, a constant latency is not important, tracking + * the fill level to keep it steady makes no sense. Consequently, + * no DLL is needed. Also, matching the pace of the synchronized clock + * is done by having the graph driver be synchronized to that clock, + * which will in turn cause any output sinks to adjust their DLLs + * (or similar control loop mechanisms) to match the pace of their + * data consumption with the pace of the driver. + * + * The fill level is still important though to correctly handle corner + * cases where the ring buffer is (almost) empty. If fewer samples + * are available than what the read operation wants, the deficit + * has to be compensated with nullbytes. To that end, the "avail" + * quantity tracks how many samples are actually available. */ if (impl->io_position) { + uint32_t dummy_read_index; + /* Shift clock position by stream delay to compensate * for processing and output delay. */ timestamp = impl->io_position->clock.position + device_delay; spa_ringbuffer_read_update(&impl->ring, timestamp); + avail = spa_ringbuffer_get_read_index(&impl->ring, &dummy_read_index); } else { /* In the unlikely case that no spa_io_position pointer * was passed yet by PipeWire to this node, resort to a @@ -109,26 +120,45 @@ static void rtp_audio_process_playback(void *data) * This most likely is not in sync with other nodes, * but _something_ is needed as read index until the * spa_io_position is available. */ - spa_ringbuffer_get_read_index(&impl->ring, ×tamp); + avail = spa_ringbuffer_get_read_index(&impl->ring, ×tamp); } - spa_ringbuffer_read_data(&impl->ring, - impl->buffer, - impl->actual_max_buffer_size, - ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, - d[0].data, wanted * stride); + /* If avail is 0, it means that the ring buffer is empty. <0 means + * that there is an underrun, typically because the PTP time now + * is ahead of the RTP data (this can happen when the PTP master + * changes for example). And in cases where only a little bit of + * data is left, it is important to not try to use more than what + * is actually available. */ + num_samples_to_read = (avail > 0) ? SPA_MIN((uint32_t)avail, wanted) : 0; - /* Clear the bytes that were just retrieved. Since the fill level - * is not tracked in this buffer mode, it is possible that as soon - * as actual playback ends, the RTP source node re-reads old data. - * Make sure it reads silence when no actual new data is present - * and the RTP source node still runs. Do this by filling the - * region of the retrieved data with null bytes. */ - ringbuffer_clear(&impl->ring, - impl->buffer, - impl->actual_max_buffer_size, - ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, - wanted * stride); + if (num_samples_to_read > 0) { + spa_ringbuffer_read_data(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, + d[0].data, num_samples_to_read * stride); + + /* Clear the bytes that were just retrieved. Since the fill level + * is not tracked in this buffer mode, it is possible that as soon + * as actual playback ends, the RTP source node re-reads old data. + * Make sure it reads silence when no actual new data is present + * and the RTP source node still runs. Do this by filling the + * region of the retrieved data with null bytes. */ + ringbuffer_clear(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, + num_samples_to_read * stride); + } + + if (num_samples_to_read < wanted) { + /* If fewer samples were available than what was wanted, + * fill the remaining space in the destination memory + * with nullsamples. */ + void *bytes_to_clear = SPA_PTROFF(d[0].data, num_samples_to_read * stride, void); + size_t num_bytes_to_clear = (wanted - num_samples_to_read) * stride; + spa_memzero(bytes_to_clear, num_bytes_to_clear); + } if (!impl->io_position) { /* In the unlikely case that no spa_io_position pointer From 413f5762c46ff5bbc6084ea4a9c94e9895d41c83 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Fri, 16 Jan 2026 23:40:36 +0100 Subject: [PATCH 33/93] module-rtp: Clear ringbuffer in constant delay mode Clearing the ring buffer is important not only in the direct timestamp mode, but also in the constant delay mode, since missed packets can lead to gaps in the ring buffer. These gaps may have stale data inside if the ringbuffer is not cleared after reading from it. --- src/modules/module-rtp/audio.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 8dd0cec37..4b11edb69 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -249,6 +249,25 @@ static void rtp_audio_process_playback(void *data) ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, d[0].data, wanted * stride); + /* Clear the bytes that were just retrieved. Unlike in the + * direct timestamp mode, here, bytes are always read out + * of the ring buffer in sequence - the read pointer does + * not "jump around" (which can happen in direct timestamp + * mode if the last iteration has been a while ago and the + * driver clock time advanced significantly, or if the driver + * time experienced a discontinuity). However, should there + * be packet loss, it could lead to segments in the ring + * buffer that should have been written to but weren't written + * to. These segments would then contain old stale data. By + * clearing data out of the ring buffer after reading it, it + * is ensured that no stale data can exist - in the packet loss + * case, the outcome would be a gap made of nullsamples instead. */ + ringbuffer_clear(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, + wanted * stride); + timestamp += wanted; spa_ringbuffer_read_update(&impl->ring, timestamp); } From 543000151f67a0a40032fd45c2b4540e3942c324 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Mon, 19 Jan 2026 12:25:51 +0100 Subject: [PATCH 34/93] module-rtp: Handle unsigned 32-bit integer overflow in constant delay mode --- src/modules/module-rtp/audio.c | 38 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 4b11edb69..b1cae86c4 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -380,17 +380,43 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len, * and not _appended_. In this example, `expected_write` would * be 100 (since `expected_write` is the current write index), * `write` would be 90, `samples` would be 10. In this case, - * the inequality below does not hold, so data is being - * _inserted_. By contrast, during normal operation, `write` - * and `expected_write` are equal, so the inequality below - * _does_ hold, meaning that data is being appended. + * the (expected_write < (write + samples)) inequality does + * not hold, so data is being _inserted_. By contrast, during + * normal operation, `write` and `expected_write` are equal, + * so the aforementioned inequality _does_ hold, meaning that + * data is being appended. + * + * The code below handles this, and also handles a 32-bit + * integer overflow corner case where the comparison has + * to be done differently to account for the wrap-around. * * (Note that this write index update is only important if * the constant delay mode is active, or if no spa_io_position * was not provided yet. See the rtp_audio_process_playback() * code for more about this.) */ - if (expected_write < (write + samples)) { - write += samples; + + /* Compute new_write, handling potential 32-bit overflow. + * In unsigned arithmetic, if write + samples exceeds UINT32_MAX, + * it wraps around to a smaller value. We detect this by checking + * if new_write < write (which can only happen on overflow). */ + const uint32_t new_write = write + samples; + const bool wrapped_around = new_write < write; + + /* Determine if new_write is ahead of expected_write. + * We're appending (ahead) if: + * + * 1. Normal case: new_write > expected_write (forward progress) + * 2. Wrap-around case: new_write wrapped around (wrapped_around == true), + * meaning we've cycled through the 32-bit index space and are + * continuing from the beginning. In this case, we're always ahead. + * + * We're NOT appending (inserting/behind) if: + * - new_write <= expected_write AND no wrap-around occurred + * (we're filling a gap or writing behind the current position) */ + const bool is_appending = wrapped_around || (new_write > expected_write); + + if (is_appending) { + write = new_write; spa_ringbuffer_write_update(&impl->ring, write); } } From 9f0dc9c1af1aa2cccae153678b81b7b42633273c Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Mon, 19 Jan 2026 21:15:22 +0100 Subject: [PATCH 35/93] module-rtp: Update ringbuffer indices upon resync with proper API calls --- src/modules/module-rtp/audio.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index b1cae86c4..5730e3e41 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -641,7 +641,8 @@ static void rtp_audio_process_capture(void *data) if (!impl->have_sync) { pw_log_info("(re)sync to timestamp:%u seq:%u ts_offset:%u SSRC:%u", actual_timestamp, impl->seq, impl->ts_offset, impl->ssrc); - impl->ring.readindex = impl->ring.writeindex = actual_timestamp; + spa_ringbuffer_read_update(&impl->ring, actual_timestamp); + spa_ringbuffer_write_update(&impl->ring, actual_timestamp); memset(impl->buffer, 0, BUFFER_SIZE); impl->have_sync = true; expected_timestamp = actual_timestamp; From 5d7f21f1304cb7b8ddb2a8364aec66f22e7a2894 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Mon, 19 Jan 2026 21:03:48 +0100 Subject: [PATCH 36/93] module-rtp: Improve rtp_audio_flush_packets() logging --- src/modules/module-rtp/audio.c | 49 ++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 5730e3e41..045604b72 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -62,6 +62,7 @@ static void rtp_audio_process_playback(void *data) if (impl->direct_timestamp) { uint32_t num_samples_to_read; + uint32_t read_index; /* In direct timestamp mode, the focus lies on synchronized playback, not * on a constant latency. The ring buffer fill level is not of interest @@ -106,13 +107,11 @@ static void rtp_audio_process_playback(void *data) * quantity tracks how many samples are actually available. */ if (impl->io_position) { - uint32_t dummy_read_index; - /* Shift clock position by stream delay to compensate * for processing and output delay. */ timestamp = impl->io_position->clock.position + device_delay; spa_ringbuffer_read_update(&impl->ring, timestamp); - avail = spa_ringbuffer_get_read_index(&impl->ring, &dummy_read_index); + avail = spa_ringbuffer_get_read_index(&impl->ring, &read_index); } else { /* In the unlikely case that no spa_io_position pointer * was passed yet by PipeWire to this node, resort to a @@ -121,6 +120,7 @@ static void rtp_audio_process_playback(void *data) * but _something_ is needed as read index until the * spa_io_position is available. */ avail = spa_ringbuffer_get_read_index(&impl->ring, ×tamp); + read_index = timestamp; } /* If avail is 0, it means that the ring buffer is empty. <0 means @@ -128,8 +128,34 @@ static void rtp_audio_process_playback(void *data) * is ahead of the RTP data (this can happen when the PTP master * changes for example). And in cases where only a little bit of * data is left, it is important to not try to use more than what - * is actually available. */ - num_samples_to_read = (avail > 0) ? SPA_MIN((uint32_t)avail, wanted) : 0; + * is actually available. + * Overruns would happen if the write pointer is further ahead than + * what the ringbuffer size actually allows. This too can happen + * if the PTP time jumps. No actual buffer overflow would happen + * then, since the write operations always apply modulo to the + * timestamps to wrap around the ringbuffer borders. + */ + bool has_underrun = (avail < 0); + bool has_overrun = !has_underrun && ((uint32_t)avail) > impl->actual_max_buffer_size; + num_samples_to_read = has_underrun ? 0 : SPA_MIN((uint32_t)avail, wanted); + + /* Do some additional logging in the under/overrun cases. */ + if (SPA_UNLIKELY(pw_log_topic_enabled(SPA_LOG_LEVEL_TRACE, PW_LOG_TOPIC_DEFAULT))) + { + uint32_t write_index; + int32_t filled = spa_ringbuffer_get_write_index(&impl->ring, &write_index); + + if (has_underrun) { + pw_log_trace("Direct timestamp mode: Read index underrun: write_index: %" + PRIu32 ", read_index: %" PRIu32 ", wanted: %u - filled: %" PRIi32, + write_index, read_index, wanted, filled); + } else if (has_overrun) { + pw_log_trace("Direct timestamp mode: Read index overrun: write_index: %" + PRIu32 ", read_index: %" PRIu32 ", wanted: %u - filled: %" PRIi32 + ", buffer size: %u", write_index, read_index, wanted, filled, + impl->actual_max_buffer_size); + } + } if (num_samples_to_read > 0) { spa_ringbuffer_read_data(&impl->ring, @@ -498,20 +524,27 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin iov[0].iov_len = sizeof(header); while (num_packets > 0) { + uint32_t rtp_timestamp; + if (impl->marker_on_first && impl->first) header.m = 1; else header.m = 0; + + rtp_timestamp = impl->ts_offset + (set_timestamp ? set_timestamp : timestamp); + header.sequence_number = htons(impl->seq); - header.timestamp = htonl(impl->ts_offset + (set_timestamp ? set_timestamp : timestamp)); + header.timestamp = htonl(rtp_timestamp); set_iovec(&impl->ring, impl->buffer, impl->actual_max_buffer_size, ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, &iov[1], tosend * stride); - pw_log_trace("sending %d packet:%d ts_offset:%d timestamp:%d", - tosend, num_packets, impl->ts_offset, timestamp); + pw_log_trace("sending %d packet:%d ts_offset:%d timestamp:%u (%f s)", + tosend, num_packets, impl->ts_offset, timestamp, + (double)timestamp * impl->io_position->clock.rate.num / + impl->io_position->clock.rate.denom); rtp_stream_emit_send_packet(impl, iov, 3); From a16485f8aa21dfb8e038b1c7e6a31adb25be7f85 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Tue, 20 Jan 2026 12:50:35 +0100 Subject: [PATCH 37/93] module-rtp: Extract common RTP code into static library for better reuse --- src/modules/meson.build | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/modules/meson.build b/src/modules/meson.build index 0605900e5..7ad232ac8 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -573,6 +573,22 @@ if build_module_zeroconf_discover endif summary({'zeroconf-discover': build_module_zeroconf_discover}, bool_yn: true, section: 'Optional Modules') +# Several modules (rtp-sink, rtp-source, raop-sink) use the same code +# for actual RTP transport. To not have to recompile the same code +# multiple times, and to make the build script a little more robust +# (by avoiding build script code duplication), create a static library +# that contains that common code. +pipewire_module_rtp_common_lib = static_library('pipewire-module-rtp-common-lib', + [ 'module-rtp/stream.c' ], + include_directories : [configinc], + install : false, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep], +) +pipewire_module_rtp_common_dep = declare_dependency( + link_with: pipewire_module_rtp_common_lib, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep], +) + build_module_raop_discover = avahi_dep.found() if build_module_raop_discover pipewire_module_raop_discover = shared_library('pipewire-module-raop-discover', @@ -605,13 +621,12 @@ build_module_raop = openssl_lib.found() if build_module_raop pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink', [ 'module-raop-sink.c', - 'module-raop/rtsp-client.c', - 'module-rtp/stream.c' ], + 'module-raop/rtsp-client.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep, openssl_lib], + dependencies : [pipewire_module_rtp_common_dep, openssl_lib], ) endif summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') @@ -620,36 +635,33 @@ roc_dep = dependency('roc', version: '>= 0.4.0', required: get_option('roc')) summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons') pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source', - [ 'module-rtp-source.c', - 'module-rtp/stream.c' ], + [ 'module-rtp-source.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep], + dependencies : [pipewire_module_rtp_common_dep], ) pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink', - [ 'module-rtp-sink.c', - 'module-rtp/stream.c' ], + [ 'module-rtp-sink.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, opus_dep], + dependencies : [pipewire_module_rtp_common_dep], ) build_module_rtp_session = avahi_dep.found() if build_module_rtp_session pipewire_module_rtp_session = shared_library('pipewire-module-rtp-session', - [ 'module-rtp/stream.c', - 'module-zeroconf-discover/avahi-poll.c', - 'module-rtp-session.c' ], + [ 'module-zeroconf-discover/avahi-poll.c', + 'module-rtp-session.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep, opus_dep], + dependencies : [pipewire_module_rtp_common_dep, avahi_dep], ) endif From e874f705a9264d7b94e011a3cc5e7f080a56ced1 Mon Sep 17 00:00:00 2001 From: Stanislav Ruzani Date: Sat, 27 Dec 2025 18:37:33 +0100 Subject: [PATCH 38/93] module-rtp: clear ringbuffer when stream stops to prevent old packets Clear the ringbuffer in stream_stop() when processing stops to prevent old invalid packets from being sent when processing resumes via rtp_audio_flush_packets(). This ensures a clean state when the stream restarts. --- src/modules/module-rtp/stream.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c index e19d88a1f..d69b16524 100644 --- a/src/modules/module-rtp/stream.c +++ b/src/modules/module-rtp/stream.c @@ -454,6 +454,10 @@ static int stream_stop(struct impl *impl) * meaning that the timer was no longer running, and the connection * could be closed. */ if (!timer_running) { + /* Clear the ringbuffer to prevent old invalid packets from being + * sent when processing resumes via rtp_audio_flush_packets() */ + if (impl->reset_ringbuffer) + impl->reset_ringbuffer(impl); set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STOPPED); pw_log_info("stream stopped"); } From 6192c110385457df5c1655c313bd701595aac56d Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Mon, 19 Jan 2026 23:24:04 +0100 Subject: [PATCH 39/93] module-rtp: Convert clock pos to RTP time in RTP source and factor in ASRC This can easily be overlooked if the RTP rate equals the clock rate, which is fairly common (for example, RTP rate and clock rate both being 48 kHz). And, if an ASRC is active, and converting the output of the RTP source node, the resampler's delay need to be taken into the account as well. --- src/modules/module-rtp/audio.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 045604b72..8c5517e46 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -22,6 +22,15 @@ static void ringbuffer_clear(struct spa_ringbuffer *rbuf SPA_UNUSED, memset(iov[1].iov_base, 0, iov[1].iov_len); } +static inline uint64_t scale_u64(uint64_t val, uint32_t num, uint32_t denom) +{ +#if 0 + return ((__uint128_t)val * num) / denom; +#else + return (uint64_t)((double)val / denom * num); +#endif +} + static void rtp_audio_process_playback(void *data) { struct impl *impl = data; @@ -107,9 +116,15 @@ static void rtp_audio_process_playback(void *data) * quantity tracks how many samples are actually available. */ if (impl->io_position) { - /* Shift clock position by stream delay to compensate - * for processing and output delay. */ - timestamp = impl->io_position->clock.position + device_delay; + uint32_t clock_rate = impl->io_position->clock.rate.denom; + + /* Translate the clock position to an RTP timestamp and + * shift it to compensate for device delay and ASRC delay. + * The device delay is scaled along with the clock position, + * since both are expressed in clock sample units, while + * pwt.buffered is expressed in stream time. */ + timestamp = scale_u64(impl->io_position->clock.position + device_delay, + impl->rate, clock_rate) + pwt.buffered; spa_ringbuffer_read_update(&impl->ring, timestamp); avail = spa_ringbuffer_get_read_index(&impl->ring, &read_index); } else { From 642504f5f981440761f1cf2cd3873e283d8ea07e Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Wed, 21 Jan 2026 21:50:14 +0100 Subject: [PATCH 40/93] module-rtp: Compensate for stream resampler effects in RTP sink node --- src/modules/module-rtp/audio.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 8c5517e46..d20e9a37c 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -620,6 +620,7 @@ static void rtp_audio_process_capture(void *data) uint32_t pending, num_queued; struct spa_io_position *pos; uint64_t next_nsec, quantum; + struct pw_time pwt; if (impl->separate_sender) { /* apply the DLL rate */ @@ -637,6 +638,8 @@ static void rtp_audio_process_capture(void *data) stride = impl->stride; wanted = size / stride; + pw_stream_get_time_n(impl->stream, &pwt, sizeof(pwt)); + filled = spa_ringbuffer_get_write_index(&impl->ring, &expected_timestamp); pos = impl->io_position; @@ -653,6 +656,21 @@ static void rtp_audio_process_capture(void *data) impl->sink_resamp_delay = impl->io_rate_match->delay; impl->sink_quantum = (uint64_t)(pos->clock.duration * SPA_NSEC_PER_SEC / rate); } + + /* Compensate for the stream resampler's delay. */ + actual_timestamp -= pwt.buffered; + + /* If we got a request for less than quantum worth of samples, it indicates that there + * is a gap created by the resampler. We have to skip it to avoid timestamp discontinuity. */ + if (pwt.buffered > 0) { + int32_t ideal_quantum = (int32_t)scale_u64(pos->clock.duration, impl->rate, rate); + if (wanted < ideal_quantum) { + int32_t num_samples_to_skip = ideal_quantum - wanted; + pw_log_info("wanted: %" PRId32 " < ideal quantum: %" PRId32 " - skipping %" + PRId32" samples", wanted, ideal_quantum, num_samples_to_skip); + actual_timestamp += num_samples_to_skip; + } + } } else { actual_timestamp = expected_timestamp; next_nsec = 0; From ac7728097fadc64e0c235962acd25d4aad38dac0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 3 Feb 2026 09:59:16 +0100 Subject: [PATCH 41/93] snapcast: support newer snapcast service type Newer snapcast servers publish the service as _snapcast-ctrl._tcp so listen for that as well. Fixes #5104 --- src/modules/module-snapcast-discover.c | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/modules/module-snapcast-discover.c b/src/modules/module-snapcast-discover.c index 596d5677b..3568d82d4 100644 --- a/src/modules/module-snapcast-discover.c +++ b/src/modules/module-snapcast-discover.c @@ -162,7 +162,8 @@ static const struct spa_dict_item module_props[] = { { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; -#define SERVICE_TYPE_CONTROL "_snapcast-jsonrpc._tcp" +#define SERVICE_TYPE_JSONRPC "_snapcast-jsonrpc._tcp" +#define SERVICE_TYPE_CONTROL "_snapcast-ctrl._tcp" struct impl { struct pw_context *context; @@ -176,7 +177,8 @@ struct impl { AvahiPoll *avahi_poll; AvahiClient *client; - AvahiServiceBrowser *sink_browser; + AvahiServiceBrowser *jsonrpc_browser; + AvahiServiceBrowser *ctrl_browser; struct spa_list tunnel_list; uint32_t id; @@ -252,8 +254,10 @@ static void impl_free(struct impl *impl) spa_list_consume(t, &impl->tunnel_list, link) free_tunnel(t); - if (impl->sink_browser) - avahi_service_browser_free(impl->sink_browser); + if (impl->jsonrpc_browser) + avahi_service_browser_free(impl->jsonrpc_browser); + if (impl->ctrl_browser) + avahi_service_browser_free(impl->ctrl_browser); if (impl->client) avahi_client_free(impl->client); if (impl->avahi_poll) @@ -818,9 +822,13 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda case AVAHI_CLIENT_S_REGISTERING: case AVAHI_CLIENT_S_RUNNING: case AVAHI_CLIENT_S_COLLISION: - if (impl->sink_browser == NULL) - impl->sink_browser = make_browser(impl, SERVICE_TYPE_CONTROL); - if (impl->sink_browser == NULL) + if (impl->ctrl_browser == NULL) + impl->ctrl_browser = make_browser(impl, SERVICE_TYPE_CONTROL); + if (impl->ctrl_browser == NULL) + goto error; + if (impl->jsonrpc_browser == NULL) + impl->jsonrpc_browser = make_browser(impl, SERVICE_TYPE_JSONRPC); + if (impl->jsonrpc_browser == NULL) goto error; break; case AVAHI_CLIENT_FAILURE: @@ -829,9 +837,13 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda SPA_FALLTHROUGH; case AVAHI_CLIENT_CONNECTING: - if (impl->sink_browser) { - avahi_service_browser_free(impl->sink_browser); - impl->sink_browser = NULL; + if (impl->ctrl_browser) { + avahi_service_browser_free(impl->ctrl_browser); + impl->ctrl_browser = NULL; + } + if (impl->jsonrpc_browser) { + avahi_service_browser_free(impl->jsonrpc_browser); + impl->jsonrpc_browser = NULL; } break; default: From 2c0988ab4c19a04b1bd4044228559e5402d0c164 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 3 Feb 2026 10:16:22 +0100 Subject: [PATCH 42/93] bluez-dbus: fix adapter memcpy length sizeof(adapter) is larger than the big_entry->adapter and so the code would copy too much. Instead only copy the strlen of the parsed adapter, which we checked above to be smaller than the available size. This doesn't copy the 0 byte because the memory is assumed to be 0 filled already by the calloc. If the address is exactly the HCI_DEV_NAME_LEN, it will result in a non-0 terminated string, which may or may not be a problem... --- spa/plugins/bluez5/bluez5-dbus.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 0bb5a1a88..a17eedc84 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -7044,7 +7044,7 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const goto parse_failed; if (strlen(adapter) > HCI_DEV_NAME_LEN) goto parse_failed; - memcpy(big_entry->adapter, adapter, sizeof(adapter)); + memcpy(big_entry->adapter, adapter, strlen(adapter)); spa_log_debug(monitor->log, "big_entry->adapter %s", big_entry->adapter); } else if (spa_streq(key, "encryption")) { if (spa_json_get_bool(&it[0], &big_entry->encryption) <= 0) From 57efceeb02bf72e372a3005a369218adebd855ff Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Mon, 2 Feb 2026 22:16:11 +0100 Subject: [PATCH 43/93] Implement socket activation without libsystemd Socket activation uses sd_listen_fds from libsystemd, and can only be compiled on systems with systemd. This is an issue for Alpine / postmarketOS, where upstream has no systemd package, but downstream depends on upstream's pipewire package and wants to rely on socket activation. This also prevents using socket-activation on other non-systemd distributions, including non-Linux. Implement equivalent functionality without a dependency on libsystemd. --- src/modules/meson.build | 4 -- src/modules/module-protocol-native.c | 15 ++--- src/modules/module-protocol-pulse/server.c | 22 ++----- src/modules/network-utils.h | 71 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/src/modules/meson.build b/src/modules/meson.build index 7ad232ac8..6b215f3a0 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -276,10 +276,6 @@ pipewire_module_link_factory = shared_library('pipewire-module-link-factory', pipewire_module_protocol_deps = [mathlib, dl_lib, pipewire_dep] -if systemd_dep.found() - pipewire_module_protocol_deps += systemd_dep -endif - if selinux_dep.found() pipewire_module_protocol_deps += selinux_dep endif diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c index 96e99f35e..2be92a847 100644 --- a/src/modules/module-protocol-native.c +++ b/src/modules/module-protocol-native.c @@ -34,10 +34,6 @@ #include #include -#ifdef HAVE_SYSTEMD -#include -#endif - #ifdef HAVE_SELINUX #include #endif @@ -45,6 +41,7 @@ #include #include +#include "network-utils.h" #include "pipewire/private.h" #include "modules/module-protocol-native/connection.h" @@ -909,13 +906,12 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc int fd = -1, res; bool activated = false; -#ifdef HAVE_SYSTEMD { - int i, n = sd_listen_fds(0); + int i, n = listen_fd(); for (i = 0; i < n; ++i) { - if (sd_is_socket_unix(SD_LISTEN_FDS_START + i, SOCK_STREAM, - 1, s->addr.sun_path, 0) > 0) { - fd = SD_LISTEN_FDS_START + i; + if (is_socket_unix(LISTEN_FDS_START + i, SOCK_STREAM, + s->addr.sun_path) > 0) { + fd = LISTEN_FDS_START + i; activated = true; pw_log_info("server %p: Found socket activation socket for '%s'", s, s->addr.sun_path); @@ -923,7 +919,6 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc } } } -#endif if (fd < 0) { struct stat socket_stat; diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c index 4e744e33f..aeab710b0 100644 --- a/src/modules/module-protocol-pulse/server.c +++ b/src/modules/module-protocol-pulse/server.c @@ -21,9 +21,6 @@ #include #include -#ifdef HAVE_SYSTEMD -#include -#endif #include #include @@ -577,26 +574,19 @@ static bool is_stale_socket(int fd, const struct sockaddr_un *addr_un) return false; } -#ifdef HAVE_SYSTEMD -static int check_systemd_activation(const char *path) +static int check_socket_activation(const char *path) { - const int n = sd_listen_fds(0); + const int n = listen_fd(); for (int i = 0; i < n; i++) { - const int fd = SD_LISTEN_FDS_START + i; + const int fd = LISTEN_FDS_START + i; - if (sd_is_socket_unix(fd, SOCK_STREAM, 1, path, 0) > 0) + if (is_socket_unix(fd, SOCK_STREAM, path) > 0) return fd; } return -1; } -#else -static inline int check_systemd_activation(SPA_UNUSED const char *path) -{ - return -1; -} -#endif static int start_unix_server(struct server *server, const struct sockaddr_storage *addr) { @@ -606,10 +596,10 @@ static int start_unix_server(struct server *server, const struct sockaddr_storag spa_assert(addr_un->sun_family == AF_UNIX); - fd = check_systemd_activation(addr_un->sun_path); + fd = check_socket_activation(addr_un->sun_path); if (fd >= 0) { server->activated = true; - pw_log_info("server %p: found systemd socket activation socket for '%s'", + pw_log_info("server %p: found socket activation socket for '%s'", server, addr_un->sun_path); goto done; } diff --git a/src/modules/network-utils.h b/src/modules/network-utils.h index 16f9d9273..a89b7d3bd 100644 --- a/src/modules/network-utils.h +++ b/src/modules/network-utils.h @@ -7,6 +7,12 @@ #include #include #include +#include +#include +#include +#include + +#include #ifdef __FreeBSD__ #define ifr_ifindex ifr_index @@ -131,5 +137,70 @@ static inline bool pw_net_addr_is_any(struct sockaddr_storage *addr) return false; } +#ifndef LISTEN_FDS_START +#define LISTEN_FDS_START 3 +#endif + +/* Returns the number of file descriptors passed for socket activation. + * Returns 0 if none, -1 on error. */ +static inline int listen_fd(void) +{ + uint32_t n; + int i, flags; + + if (!spa_atou32(getenv("LISTEN_FDS"), &n, 10) || n > INT_MAX - LISTEN_FDS_START) { + errno = EINVAL; + return -1; + } + + for (i = 0; i < (int)n; i++) { + flags = fcntl(LISTEN_FDS_START + i, F_GETFD); + if (flags == -1) + return -1; + if (fcntl(LISTEN_FDS_START + i, F_SETFD, flags | FD_CLOEXEC) == -1) + return -1; + } + + unsetenv("LISTEN_FDS"); + + return (int)n; +} + +/* Check if the fd is a listening unix socket of the given type, + * optionally bound to the given path. */ +static inline int is_socket_unix(int fd, int type, const char *path) +{ + struct sockaddr_un addr; + int val; + socklen_t len = sizeof(val); + + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &val, &len) < 0) + return -errno; + if (val != type) + return 0; + + if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &val, &len) < 0) + return -errno; + if (!val) + return 0; + + if (path) { + len = sizeof(addr); + memset(&addr, 0, sizeof(addr)); + if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0) + return -errno; + if (addr.sun_family != AF_UNIX) + return 0; + size_t length = strlen(path); + if (length > 0) { + if (len < offsetof(struct sockaddr_un, sun_path) + length) + return 0; + if (memcmp(addr.sun_path, path, length) != 0) + return 0; + } + } + + return 1; +} #endif /* NETWORK_UTILS_H */ From 84bfbd92a14fb239bf5193b4ac29fc14301edb1d Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Tue, 3 Feb 2026 19:10:25 +0200 Subject: [PATCH 44/93] bluez5: bap: prefer high-reliability qos for 44.1/48 kHz On some device combinations (MT7925 / Sony LinkBuds S) the low-latency 48 kHz QoS crackle. It's probably better to default to high-reliability for those, until we have proper quality vs. latency configuration in place. --- spa/plugins/bluez5/bap-codec-lc3.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index be08fd5a5..2581ba5d7 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -132,14 +132,14 @@ static const struct bap_qos bap_qos_configs[] = { BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2), /* 24_2_2 */ BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13), /* 32_1_2 */ BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3), /* 32_2_2 */ - BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 14), /* 441_1_2 */ - BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 4), /* 441_2_2 */ - BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 15), /* 48_1_2 */ - BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 5), /* 48_2_2 */ - BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 16), /* 48_3_2 */ - BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 6), /* 48_4_2 */ - BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 17), /* 48_5_2 */ - BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 7), /* 48_6_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 54), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 44), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 55), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 45), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 56), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 46), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 57), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 47), /* 48_6_2 */ }; static const struct bap_qos bap_bcast_qos_configs[] = { From bac776f8b4a58c88d439e7fa0c2c0dd6f9662657 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 5 Feb 2026 10:33:49 +0100 Subject: [PATCH 45/93] doc: spa: Explain the nsec and next_nsec values in the driver docs better --- doc/dox/internals/driver.dox | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/doc/dox/internals/driver.dox b/doc/dox/internals/driver.dox index 4ab0d229b..fb717ab8b 100644 --- a/doc/dox/internals/driver.dox +++ b/doc/dox/internals/driver.dox @@ -32,8 +32,11 @@ updated as follows: - \ref spa_io_clock::nsec : Must be set to the time (according to the monotonic system clock) when the cycle that the driver is about to trigger started. To - minimize jitter, it is usually a good idea to increment this by a fixed amount + minimize jitter, it is usually a good idea to increment this by a computed + amount (instead of sampling a timestamp from the monotonic system clock) except for when the driver starts and when discontinuities occur in its clock. + The computed amount can be fixed, or varying over time, for example due to + adjustments made by a DLL (more on that below). - \ref spa_io_clock::rate : Set to a value that can translate samples to nanoseconds. - \ref spa_io_clock::position : Current cycle position, in samples. This is the ideal position of the graph cycle (this is explained in greater detail further below). @@ -52,7 +55,7 @@ updated as follows: some cases, this may actually be in the past relative to nsec, for example, when some internal driver clock experienced a discontinuity. Consider setting the \ref SPA_IO_CLOCK_FLAG_DISCONT flag in such a case. Just like with nsec, to - minimize jitter, it is usually a good idea to increment this by a fixed amount + minimize jitter, it is usually a good idea to increment this by a computed amount except for when the driver starts and when discontinuities occur in its clock. The driver node signals the start of the graph cycle by calling \ref spa_node_call_ready @@ -60,6 +63,11 @@ with the \ref SPA_STATUS_HAVE_DATA and \ref SPA_STATUS_NEED_DATA flags passed to that function call. That call must happen inside the thread that runs the data loop assigned to the driver node. +Drivers must make sure that the next cycle is started at the time indicated by +the \ref spa_io_clock::next_nsec timestamp. They do not have to use the monotonic +clock itself for scheduling the next cycle. If for example the internal clock +can directly be used with \c timerfd , the next cycle can be triggered that way. + As mentioned above, the \ref spa_io_clock::position field is the _ideal_ position of the graph cycle, in samples. This contrasts with \ref spa_io_clock::nsec, which is the moment in monotonic clock time when the cycle _actually_ happens. This is an @@ -103,11 +111,12 @@ expected position (in samples) and the actual position (derived from the current of the driver's internal clock), passes the delta between these two quantities into the DLL, and the DLL computes a correction factor (2.0 in the above example) which is used for scaling durations between \c timerfd timeouts. This forms a control loop, since the -correction factor causes the durations between the timeouts to be adjusted such that the -difference between the expected position and the actual position reaches zero. Keep in -mind the notes above about \ref spa_io_clock::position being the ideal position of the -graph cycle, meaning that even in this case, the duration it is incremented by is -_not_ scaled by the correction factor; the duration in samples remains unchanged. +correction factor causes the \ref spa_io_clock::next_nsec increments (that is, the +durations between the timerfd timeouts) to be adjusted such that, over time, the difference +between the expected position and the actual position reaches zero. Keep in mind the +notes above about \ref spa_io_clock::position being the ideal position of the graph +cycle, meaning that even in this case, the duration it is incremented by is _not_ +scaled by the correction factor; the duration in samples remains unchanged. (Other popular control loop mechanisms that are suitable alternatives to the DLL are PID controllers and Kalman filters.) From 64e0a9cbd96f4eacf3b9f0435f59fbeda792f6f9 Mon Sep 17 00:00:00 2001 From: Stanislav Ruzani Date: Sun, 28 Dec 2025 23:42:57 +0100 Subject: [PATCH 46/93] alsa-pcm: set rate_match rate to 1.0 when not matching Only set rate_match rate to DLL correction when matching is active. When ALSA is driver and not matching, set rate to 1.0 to indicate no rate adjustment needed. DLL still runs for buffer level management but rate_match should not expose correction when matching is inactive to avoid confusion during debugging. Signed-off-by: Stanislav Ruzani --- spa/plugins/alsa/alsa-pcm.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c index a12b2601a..ca31366dc 100644 --- a/spa/plugins/alsa/alsa-pcm.c +++ b/spa/plugins/alsa/alsa-pcm.c @@ -3040,10 +3040,17 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram } if (state->rate_match) { - if (state->stream == SND_PCM_STREAM_PLAYBACK) - state->rate_match->rate = corr; - else - state->rate_match->rate = 1.0/corr; + /* Only set rate_match rate when matching is active. When not matching, + * set it to 1.0 to indicate no rate adjustment needed, even though DLL + * may still be running for buffer level management. */ + if (state->matching) { + if (state->stream == SND_PCM_STREAM_PLAYBACK) + state->rate_match->rate = corr; + else + state->rate_match->rate = 1.0/corr; + } else { + state->rate_match->rate = 1.0; + } if (state->pitch_elem && state->matching) spa_alsa_update_rate_match(state); From 2770143f506a92e242f8a5b8b870929b515b48c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20P=C5=91cze?= Date: Sun, 8 Feb 2026 22:44:46 +0100 Subject: [PATCH 47/93] gst: pool: fix buffer release race condition A call to `release_buffer()` may happen in a gstreamer thread concurrently with the pipewire stream emitting the `remove_buffer` event in the thread loop, which, in pipewiresink calls `gst_pipewire_pool_remove_buffer()`, which in turn modifies the `GstPipeWirePoolData` object. Thus a data race occurs when accessing its members, which can lead to `pw_stream_return_buffer()` being called with a null pointer. Fix that by locking the thread loop before checking the conditions. Fixes: c0a6a7ea32f6 ("gst: handle flush event in pipewiresink") --- src/gst/gstpipewirepool.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c index 3bcf99de7..5e201d315 100644 --- a/src/gst/gstpipewirepool.c +++ b/src/gst/gstpipewirepool.c @@ -432,26 +432,25 @@ release_buffer (GstBufferPool * pool, GstBuffer *buffer) GST_LOG_OBJECT (pool, "release buffer %p", buffer); GstPipeWirePoolData *data = gst_pipewire_pool_get_data(buffer); + GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); + g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&p->stream); GST_OBJECT_LOCK (pool); + pw_thread_loop_lock (s->core->loop); if (!data->queued && data->b != NULL) { - GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); - g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&p->stream); int res; - pw_thread_loop_lock (s->core->loop); - if ((res = pw_stream_return_buffer (s->pwstream, data->b)) < 0) { GST_ERROR_OBJECT (pool,"can't return buffer %p; gstbuffer : %p, %s",data->b, buffer, spa_strerror(res)); } else { data->queued = TRUE; GST_DEBUG_OBJECT (pool, "returned buffer %p; gstbuffer:%p", data->b, buffer); } - - pw_thread_loop_unlock (s->core->loop); } + + pw_thread_loop_unlock (s->core->loop); GST_OBJECT_UNLOCK (pool); } From c4992550983c51d5f0add29f2050aaded39703c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Wed, 4 Feb 2026 17:03:53 +0100 Subject: [PATCH 48/93] bluez5: bap: Add device property to force target latency Some PTS tests (e.g. BAP/UCL/SCC/BV-046-C or BAP/UCL/SCC/BV-077-C) requests to select QoS from low-latency or high-reliabilty. The bluez5.bap.force-target-latency device property allows to force it. For other values than low-latency or high-reliabilty the QoS selection will use both tables to find the more appropriate configuration. --- doc/dox/config/pipewire-props.7.md | 6 ++ spa/plugins/bluez5/bap-codec-lc3.c | 145 +++++++++++++++-------------- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index 9cf3943ed..cbefba992 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1373,6 +1373,12 @@ BAP QoS framing that needs to be applied for vendor defined preset This property is experimental. Default: as per QoS preset. +@PAR@ device-prop bluez5.bap.force-target-latency = "balanced" # string +BAP QoS target latency profile forced for QoS configuration selection. +If not set or set to "balanced", both low-latency and high-reliabilty QoS configuration table are used. +This property is experimental. +Available: low-latency, high-reliabilty, balanced + ## Node properties @PAR@ node-prop bluez5.media-source-role # string diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index 2581ba5d7..74761f26b 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -55,6 +55,7 @@ struct settings { int latency; int64_t delay; int framing; + char *force_target_latency; }; struct pac_data { @@ -76,6 +77,7 @@ struct bap_qos { uint16_t latency; uint32_t delay; unsigned int priority; + char *tag; }; typedef struct { @@ -96,50 +98,50 @@ struct config_data { struct settings settings; }; -#define BAP_QOS(name_, rate_, duration_, framing_, framelen_, rtn_, latency_, delay_, priority_) \ +#define BAP_QOS(name_, rate_, duration_, framing_, framelen_, rtn_, latency_, delay_, priority_, tag_) \ ((struct bap_qos){ .name = (name_), .rate = (rate_), .frame_duration = (duration_), .framing = (framing_), \ .framelen = (framelen_), .retransmission = (rtn_), .latency = (latency_), \ - .delay = (delay_), .priority = (priority_) }) + .delay = (delay_), .priority = (priority_), .tag = (tag_) }) static const struct bap_qos bap_qos_configs[] = { /* Priority: low-latency > high-reliability, 7.5ms > 10ms, * bigger frequency and sdu better */ /* BAP v1.0.1 Table 5.2; low-latency */ - BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30), /* 8_1_1 */ - BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20), /* 8_2_1 */ - BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31), /* 16_1_1 */ - BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21), /* 16_2_1 (mandatory) */ - BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32), /* 24_1_1 */ - BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22), /* 24_2_1 */ - BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33), /* 32_1_1 */ - BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23), /* 32_2_1 */ - BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 5, 24, 40000, 34), /* 441_1_1 */ - BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 5, 31, 40000, 24), /* 441_2_1 */ - BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 5, 15, 40000, 35), /* 48_1_1 */ - BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 5, 20, 40000, 25), /* 48_2_1 */ - BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 5, 15, 40000, 36), /* 48_3_1 */ - BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 5, 20, 40000, 26), /* 48_4_1 */ - BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 5, 15, 40000, 37), /* 48_5_1 */ - BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 5, 20, 40000, 27), /* 48_6_1 */ + BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30, "low-latency"), /* 8_1_1 */ + BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20, "low-latency"), /* 8_2_1 */ + BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31, "low-latency"), /* 16_1_1 */ + BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21, "low-latency"), /* 16_2_1 (mandatory) */ + BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32, "low-latency"), /* 24_1_1 */ + BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22, "low-latency"), /* 24_2_1 */ + BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33, "low-latency"), /* 32_1_1 */ + BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23, "low-latency"), /* 32_2_1 */ + BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 5, 24, 40000, 34, "low-latency"), /* 441_1_1 */ + BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 5, 31, 40000, 24, "low-latency"), /* 441_2_1 */ + BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 5, 15, 40000, 35, "low-latency"), /* 48_1_1 */ + BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 5, 20, 40000, 25, "low-latency"), /* 48_2_1 */ + BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 5, 15, 40000, 36, "low-latency"), /* 48_3_1 */ + BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 5, 20, 40000, 26, "low-latency"), /* 48_4_1 */ + BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 5, 15, 40000, 37, "low-latency"), /* 48_5_1 */ + BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 5, 20, 40000, 27, "low-latency"), /* 48_6_1 */ /* BAP v1.0.1 Table 5.2; high-reliability */ - BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 13, 75, 40000, 10), /* 8_1_2 */ - BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 13, 95, 40000, 0), /* 8_2_2 */ - BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 13, 75, 40000, 11), /* 16_1_2 */ - BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 13, 95, 40000, 1), /* 16_2_2 */ - BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 13, 75, 40000, 12), /* 24_1_2 */ - BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2), /* 24_2_2 */ - BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13), /* 32_1_2 */ - BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3), /* 32_2_2 */ - BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 54), /* 441_1_2 */ - BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 44), /* 441_2_2 */ - BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 55), /* 48_1_2 */ - BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 45), /* 48_2_2 */ - BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 56), /* 48_3_2 */ - BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 46), /* 48_4_2 */ - BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 57), /* 48_5_2 */ - BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 47), /* 48_6_2 */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 13, 75, 40000, 10, "high-reliabilty"), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 13, 95, 40000, 0, "high-reliabilty"), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 13, 75, 40000, 11, "high-reliabilty"), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 13, 95, 40000, 1, "high-reliabilty"), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 13, 75, 40000, 12, "high-reliabilty"), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2, "high-reliabilty"), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13, "high-reliabilty"), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3, "high-reliabilty"), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 54, "high-reliabilty"), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 44, "high-reliabilty"), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 55, "high-reliabilty"), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 45, "high-reliabilty"), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 56, "high-reliabilty"), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 46, "high-reliabilty"), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 57, "high-reliabilty"), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 47, "high-reliabilty"), /* 48_6_2 */ }; static const struct bap_qos bap_bcast_qos_configs[] = { @@ -147,40 +149,40 @@ static const struct bap_qos bap_bcast_qos_configs[] = { * bigger frequency and sdu better */ /* BAP v1.0.1 Table 6.4; low-latency */ - BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30), /* 8_1_1 */ - BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20), /* 8_2_1 */ - BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31), /* 16_1_1 */ - BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21), /* 16_2_1 (mandatory) */ - BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32), /* 24_1_1 */ - BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22), /* 24_2_1 */ - BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33), /* 32_1_1 */ - BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23), /* 32_2_1 */ - BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 24, 40000, 34), /* 441_1_1 */ - BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 31, 40000, 24), /* 441_2_1 */ - BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 15, 40000, 35), /* 48_1_1 */ - BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 20, 40000, 25), /* 48_2_1 */ - BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 15, 40000, 36), /* 48_3_1 */ - BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 20, 40000, 26), /* 48_4_1 */ - BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 15, 40000, 37), /* 48_5_1 */ - BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 20, 40000, 27), /* 48_6_1 */ + BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30, "low-latency"), /* 8_1_1 */ + BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20, "low-latency"), /* 8_2_1 */ + BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31, "low-latency"), /* 16_1_1 */ + BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21, "low-latency"), /* 16_2_1 (mandatory) */ + BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32, "low-latency"), /* 24_1_1 */ + BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22, "low-latency"), /* 24_2_1 */ + BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33, "low-latency"), /* 32_1_1 */ + BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23, "low-latency"), /* 32_2_1 */ + BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 24, 40000, 34, "low-latency"), /* 441_1_1 */ + BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 31, 40000, 24, "low-latency"), /* 441_2_1 */ + BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 15, 40000, 35, "low-latency"), /* 48_1_1 */ + BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 20, 40000, 25, "low-latency"), /* 48_2_1 */ + BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 15, 40000, 36, "low-latency"), /* 48_3_1 */ + BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 20, 40000, 26, "low-latency"), /* 48_4_1 */ + BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 15, 40000, 37, "low-latency"), /* 48_5_1 */ + BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 20, 40000, 27, "low-latency"), /* 48_6_1 */ /* BAP v1.0.1 Table 6.4; high-reliability */ - BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 4, 45, 40000, 10), /* 8_1_2 */ - BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 4, 60, 40000, 0), /* 8_2_2 */ - BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 4, 45, 40000, 11), /* 16_1_2 */ - BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 4, 60, 40000, 1), /* 16_2_2 */ - BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 4, 45, 40000, 12), /* 24_1_2 */ - BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 4, 60, 40000, 2), /* 24_2_2 */ - BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 4, 45, 40000, 13), /* 32_1_2 */ - BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 4, 60, 40000, 3), /* 32_2_2 */ - BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 54, 40000, 14), /* 441_1_2 */ - BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 60, 40000, 4), /* 441_2_2 */ - BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 50, 40000, 15), /* 48_1_2 */ - BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 65, 40000, 5), /* 48_2_2 */ - BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 50, 40000, 16), /* 48_3_2 */ - BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 65, 40000, 6), /* 48_4_2 */ - BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 50, 40000, 17), /* 48_5_2 */ - BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 65, 40000, 7), /* 48_6_2 */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 4, 45, 40000, 10, "high-reliabilty"), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 4, 60, 40000, 0, "high-reliabilty"), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 4, 45, 40000, 11, "high-reliabilty"), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 4, 60, 40000, 1, "high-reliabilty"), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 4, 45, 40000, 12, "high-reliabilty"), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 4, 60, 40000, 2, "high-reliabilty"), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 4, 45, 40000, 13, "high-reliabilty"), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 4, 60, 40000, 3, "high-reliabilty"), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 54, 40000, 14, "high-reliabilty"), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 60, 40000, 4, "high-reliabilty"), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 50, 40000, 15, "high-reliabilty"), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 65, 40000, 5, "high-reliabilty"), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 50, 40000, 16, "high-reliabilty"), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 65, 40000, 6, "high-reliabilty"), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 50, 40000, 17, "high-reliabilty"), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 65, 40000, 7, "high-reliabilty"), /* 48_6_2 */ }; static unsigned int get_rate_mask(uint8_t rate) { @@ -476,6 +478,9 @@ static bool select_bap_qos(struct bap_qos *conf, else if (c.priority < conf->priority) continue; + if (s->force_target_latency && !spa_streq(s->force_target_latency, c.tag)) + continue; + if (s->retransmission >= 0) c.retransmission = s->retransmission; if (s->latency >= 0) @@ -844,6 +849,9 @@ static void parse_settings(struct settings *s, const struct spa_dict *settings, if ((str = spa_dict_lookup(settings, "bluez5.bap.preset"))) s->qos_name = strdup(str); + if ((str = spa_dict_lookup(settings, "bluez5.bap.force-target-latency"))) + s->force_target_latency = strdup(str); + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.rtn"), &value, 0)) s->retransmission = value; @@ -881,11 +889,11 @@ static void parse_settings(struct settings *s, const struct spa_dict *settings, spa_debugc(&debug_ctx->ctx, "BAP LC3 settings: preset:%s rtn:%d latency:%d delay:%d framing:%d " - "locations:%x chnalloc:%x sink:%d duplex:%d", + "locations:%x chnalloc:%x sink:%d duplex:%d force-target-latency:%s", s->qos_name ? s->qos_name : "auto", s->retransmission, s->latency, (int)s->delay, s->framing, (unsigned int)s->locations, (unsigned int)s->channel_allocation, - (int)s->sink, (int)s->duplex); + (int)s->sink, (int)s->duplex, s->force_target_latency); } static void free_config_data(struct config_data *d) @@ -893,6 +901,7 @@ static void free_config_data(struct config_data *d) if (!d) return; free(d->settings.qos_name); + free(d->settings.force_target_latency); free(d); } From 0470f96887cba29c52d47352dbf1bb6a666533c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Danis?= Date: Fri, 6 Feb 2026 12:49:58 +0100 Subject: [PATCH 49/93] bluez: bap: Select correct settings for select_config() Depending on the codec kind, select appropriate settings to pass to select_config(). This allows to pass the bluez5.bap.force-target-latency property, and so to select the same configuration. --- spa/plugins/bluez5/bluez5-dbus.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index a17eedc84..651d410c9 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -736,6 +736,11 @@ static void bap_features_clear(struct bap_features *feat) spa_zero(*feat); } +const struct spa_dict *get_device_codec_settings(struct spa_bt_device *device, bool bap) +{ + return bap ? device->settings : &device->monitor->global_settings; +} + static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { struct spa_bt_monitor *monitor = userdata; @@ -2760,6 +2765,7 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX }, }; bool is_a2dp = codec->kind == MEDIA_CODEC_A2DP; + bool is_bap = codec->kind == MEDIA_CODEC_BAP; size_t i; codec_target_profile = get_codec_target_profile(monitor, codec); @@ -2801,7 +2807,8 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru continue; if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len, - &ep->monitor->default_audio_info, &monitor->global_settings)) + &ep->monitor->default_audio_info, + get_device_codec_settings(device, is_bap))) return true; } @@ -4655,7 +4662,7 @@ static bool codec_switch_check_endpoint(struct spa_bt_remote_endpoint *ep, if (!media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len, &ep->device->monitor->default_audio_info, - &ep->device->monitor->global_settings)) + get_device_codec_settings(ep->device, codec->kind == MEDIA_CODEC_BAP))) return false; if (ep_profile & (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK)) { From a2df2820869110775dffa1d474ac682815ab8f53 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 9 Feb 2026 13:44:40 +0100 Subject: [PATCH 50/93] pw-cat: add a container option and some --list options Add a container option to override the extension check and force a container when saving. Add some more formats that are supported by libsndfile. Add some options to list all supported formats, extensions/containers, layouts and channel names. Fixes #5117 --- doc/dox/programs/pw-cat.1.md | 72 +++++++++++++---- src/tools/pw-cat.c | 145 +++++++++++++++++++++++++++++++---- 2 files changed, 188 insertions(+), 29 deletions(-) diff --git a/doc/dox/programs/pw-cat.1.md b/doc/dox/programs/pw-cat.1.md index b6e259a6f..b681e54a1 100644 --- a/doc/dox/programs/pw-cat.1.md +++ b/doc/dox/programs/pw-cat.1.md @@ -24,9 +24,9 @@ Play and record media with PipeWire **pw-cat** is a simple tool for playing back or capturing raw or encoded media files on a PipeWire server. It understands all audio file formats -supported by `libsndfile` for PCM capture and playback. When capturing -PCM, the filename extension is used to guess the file format with the -WAV file format as the default. +supported by `libsndfile` for PCM capture and playback. When no container +is specified for capturing PCM, the filename extension is used to guess +the file format with the WAV file format as the default. It understands standard MIDI files and MIDI 2.0 clip files for playback and recording. This tool will not render MIDI files, it will simply make @@ -37,8 +37,15 @@ DSD playback is supported with the DSF file format. This tool will only work with native DSD capable hardware and will produce an error when no such hardware was found. -When the *FILE* is - input and output will be raw data from STDIN and -STDOUT respectively. +When the *FILE* is - input will be from STDIN. If no format is specified, +libsndfile will attempt to parse and stream the format from STDIN. For +some formats, this is not possible and libsndfile will give an error. +Raw, MIDI and DSD formats are all streamable from STDIN. + +When the *FILE* is - output will be to STDOUT. If no format is specified, +libsndfile is instructed to output the .au format, which is streamble and +preserves the format, rate and channels. +Raw and DSD formats are all streamable to STDOUT. # OPTIONS @@ -87,6 +94,11 @@ DSD mode. *FILE* is a DSF file. If the tool is called under the name render the DSD audio. You need a DSD capable device to play DSD content or this program will exit with an error. +\par -s | \--sysex +SysEx mode. *FILE* is a File that contains a raw SysEx MIDI message. +If the tool is called under the name **pw-sysex** this is the default. +The File is read and sent as a MIDI control message into the graph. + \par \--media-type=VALUE Set the media type property (default Audio/Midi depending on mode). The media type is used by the session manager to select a suitable target to @@ -138,6 +150,17 @@ does not match the samplerate of the server, the data will be resampled. Higher quality uses more CPU. Values between 0 and 15 are allowed, the default quality is 4. +\par -a | \--raw +Raw samples will be read or written. The \--rate, \--format, \--channels +and \--channelmap can be used to specify the raw format. + +\par -M | \--force-midi +Force midi format, one of "midi" or "ump", (default ump). +When reading or writing midi, for one of midi or UMP. + +\par -n | \--sample-count=COUNT +Stop after COUNT samples. + \par \--rate=VALUE The sample rate, default 48000. @@ -145,19 +168,38 @@ The sample rate, default 48000. The number of channels, default 2. \par \--channel-map=VALUE -The channelmap. Possible values include: **mono**, **stereo**, +The channelmap. Possible values include are either a channel layout +such as **mono**, **stereo**, **surround-21**, **quad**, **surround-22**, **surround-40**, -**surround-31**, **surround-41**, **surround-50**, **surround-51**, -**surround-51r**, **surround-70**, **surround-71** or a comma separated -list of channel names: **FL**, **FR**, **FC**, **LFE**, **SL**, **SR**, -**FLC**, **FRC**, **RC**, **RL**, **RR**, **TC**, **TFL**, **TFC**, -**TFR**, **TRL**, **TRC**, **TRR**, **RLC**, **RRC**, **FLW**, **FRW**, -**LFE2**, **FLH**, **FCH**, **FRH**, **TFLC**, **TFRC**, **TSL**, -**TSR**, **LLFR**, **RLFE**, **BC**, **BLC**, **BRC** +or comma separated array of channel names such as **FL,FR**. +See \--list-layouts and \--list-channel-names to get a complete +list of possible values. + +\par \--list-layouts +List all known channel layouts. One of these can be used as the +\--channel-map value. + +\par \--list-channel-names +List all known channel names. An array of these can be used as the +\--channel-map value. \par \--format=VALUE -The sample format to use. One of: **u8**, **s8**, **s16** (default), -**s24**, **s32**, **f32**, **f64**. +The sample format to use. Some possible values include: **u8**, **s8**, +**s16** (default), **s24**, **s32**, **f32**, **f64**. See +\--list-formats to get a complete list of values. + +\par \--list-formats +List all known format values. + +\par \--container=VALUE +Specify the container to use when saving. This is usually guessed from +the filename extension but can be specified explicitly. When using +STDOUT and no container is specified, the AU container will be used. +Then using a filename and the container was not specified and it could +not be derived from the filename, the WAV container is used. + +\par \--list-containers +List all known container values. \par \--volume=VALUE The stream volume, default 1.000. Depending on the locale you have diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index 8bd3e343c..f90ebffc9 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -120,6 +120,7 @@ struct data { const char *media_role; const char *channel_map; const char *format; + const char *container; const char *target; const char *latency; struct pw_properties *props; @@ -194,8 +195,6 @@ struct data { uint64_t samples_processed; }; -#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)" - static const struct format_info { const char *name; int sf_format; @@ -217,6 +216,29 @@ static const struct format_info { { "mp3", SF_FORMAT_MPEG_LAYER_III, SPA_AUDIO_FORMAT_F32, 1 }, { "vorbis", SF_FORMAT_VORBIS, SPA_AUDIO_FORMAT_F32, 1 }, { "opus", SF_FORMAT_OPUS, SPA_AUDIO_FORMAT_F32, 1 }, + + { "ima-adpcm", SF_FORMAT_IMA_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, + { "ms-adpcm", SF_FORMAT_MS_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, + { "nms-adpcm-16", SF_FORMAT_NMS_ADPCM_16, SPA_AUDIO_FORMAT_F32, 1 }, + { "nms-adpcm-24", SF_FORMAT_NMS_ADPCM_24, SPA_AUDIO_FORMAT_F32, 1 }, + { "nms-adpcm-32", SF_FORMAT_NMS_ADPCM_32, SPA_AUDIO_FORMAT_F32, 1 }, + + { "alac-16", SF_FORMAT_ALAC_16, SPA_AUDIO_FORMAT_F32, 1 }, + { "alac-20", SF_FORMAT_ALAC_20, SPA_AUDIO_FORMAT_F32, 1 }, + { "alac-24", SF_FORMAT_ALAC_24, SPA_AUDIO_FORMAT_F32, 1 }, + { "alac-32", SF_FORMAT_ALAC_32, SPA_AUDIO_FORMAT_F32, 1 }, + + { "gsm610", SF_FORMAT_GSM610, SPA_AUDIO_FORMAT_F32, 1 }, + { "g721-32", SF_FORMAT_G721_32, SPA_AUDIO_FORMAT_F32, 1 }, + { "g723-24", SF_FORMAT_G723_24, SPA_AUDIO_FORMAT_F32, 1 }, + { "g723-40", SF_FORMAT_G723_40, SPA_AUDIO_FORMAT_F32, 1 }, + { "dwvw-12", SF_FORMAT_DWVW_12, SPA_AUDIO_FORMAT_F32, 1 }, + { "dwvw-16", SF_FORMAT_DWVW_16, SPA_AUDIO_FORMAT_F32, 1 }, + { "dwvw-24", SF_FORMAT_DWVW_24, SPA_AUDIO_FORMAT_F32, 1 }, + { "vox", SF_FORMAT_VOX_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, + { "dpcm-16", SF_FORMAT_DPCM_16, SPA_AUDIO_FORMAT_F32, 1 }, + { "dpcm-8", SF_FORMAT_DPCM_8, SPA_AUDIO_FORMAT_F32, 1 }, + }; static const struct format_info *format_info_by_name(const char *str) @@ -236,6 +258,14 @@ static const struct format_info *format_info_by_sf_format(int format) return NULL; } +static void list_formats(struct data *d) +{ + + fprintf(stdout, _("Supported formats:\n")); + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) + fprintf(stdout, " %s\n", i->name); +} + static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames, bool *null_frame) { sf_count_t rn; @@ -714,6 +744,34 @@ static int parse_channelmap(const char *channel_map, struct spa_audio_layout_inf return 0; } +static void list_layouts(struct data *d) +{ + fprintf(stderr, _("Supported channel layouts:\n")); + SPA_FOR_EACH_ELEMENT_VAR(spa_type_audio_layout_info, i) { + if (i->name == NULL) + break; + fprintf(stdout, " %s: [", i->name); + for (uint32_t j = 0; j < i->layout.n_channels; j++) + fprintf(stdout, "%s%s", j == 0 ? " " : ", ", + spa_type_audio_channel_to_short_name(i->layout.position[j])); + fprintf(stdout, " ]\n"); + } + fprintf(stderr, _("Supported channel layout aliases:\n")); + SPA_FOR_EACH_ELEMENT_VAR(maps, m) + fprintf(stdout, _(" %s -> %s\n"), m->name, m->alias); +} + +static void list_channel_names(struct data *d) +{ + fprintf(stderr, _("Supported channel names:\n")); + SPA_FOR_EACH_ELEMENT_VAR(spa_type_audio_channel, i) { + if (i->name == NULL || SPA_AUDIO_CHANNEL_IS_AUX(i->type)) + break; + fprintf(stdout, " %s\n", spa_type_short_name(i->name)); + } + fprintf(stderr, " AUX0 ... AUX4095\n"); +} + static int channelmap_default(struct spa_audio_layout_info *map, int n_channels) { switch(n_channels) { @@ -1054,6 +1112,11 @@ enum { OPT_CHANNELMAP, OPT_FORMAT, OPT_VOLUME, + OPT_CONTAINER, + OPT_LISTFORMATS, + OPT_LISTCONTAINERS, + OPT_LISTLAYOUTS, + OPT_LISTCHANNELNAMES, }; #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION @@ -1088,13 +1151,18 @@ static const struct option long_options[] = { { "rate", required_argument, NULL, OPT_RATE }, { "channels", required_argument, NULL, OPT_CHANNELS }, { "channel-map", required_argument, NULL, OPT_CHANNELMAP }, + { "list-layouts", no_argument, NULL, OPT_LISTLAYOUTS }, + { "list-channel-names", no_argument, NULL, OPT_LISTCHANNELNAMES }, { "format", required_argument, NULL, OPT_FORMAT }, + { "list-formats", no_argument, NULL, OPT_LISTFORMATS }, + { "container", required_argument, NULL, OPT_CONTAINER }, + { "list-containers", no_argument, NULL, OPT_LISTCONTAINERS }, { "volume", required_argument, NULL, OPT_VOLUME }, { "quality", required_argument, NULL, 'q' }, - { "raw", no_argument, NULL, 'a' }, + { "raw", no_argument, NULL, 'a' }, { "force-midi", required_argument, NULL, 'M' }, { "sample-count", required_argument, NULL, 'n' }, - { "midi-clip", no_argument, NULL, 'c' }, + { "midi-clip", no_argument, NULL, 'c' }, { NULL, 0, NULL, 0 } }; @@ -1131,12 +1199,17 @@ static void show_usage(const char *name, bool is_error) DEFAULT_TARGET, DEFAULT_LATENCY_PLAY); fprintf(fp, - _(" --rate Sample rate (req. for rec) (default %u)\n" - " --channels Number of channels (req. for rec) (default %u)\n" + _(" --rate Sample rate (default %u)\n" + " --channels Number of channels (default %u)\n" " --channel-map Channel map\n" - " one of: \"Stereo\", \"5.1\",... or\n" + " a channel layout: \"Stereo\", \"5.1\",... or\n" " comma separated list of channel names: eg. \"FL,FR\"\n" - " --format Sample format %s (req. for rec) (default %s)\n" + " --list-layouts List supported channel layouts\n" + " --list-channel-names List supported channel maps\n" + " --format Sample format (default %s)\n" + " --list-formats List supported sample formats\n" + " --container Container format\n" + " --list-containers List supported containers and extensions\n" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default %d)\n" " -a, --raw RAW mode\n" @@ -1145,7 +1218,7 @@ static void show_usage(const char *name, bool is_error) "\n"), DEFAULT_RATE, DEFAULT_CHANNELS, - STR_FMTS, DEFAULT_FORMAT, + DEFAULT_FORMAT, DEFAULT_VOLUME, DEFAULT_QUALITY); @@ -1679,11 +1752,20 @@ static int fill_properties(struct data *data) return 0; } -static void format_from_filename(SF_INFO *info, const char *filename) +static void format_from_filename(SF_INFO *info, const char *filename, const char *container) { int i, count = 0; int format = -1; + const char *extension; + if (spa_streq(filename, "-")) + extension = container ? container : "au"; + else if (container) + extension = container; + else + extension = filename; + + fprintf(stderr, "%s\n", filename); if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0) count = 0; @@ -1695,7 +1777,7 @@ static void format_from_filename(SF_INFO *info, const char *filename) if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0) continue; - if (spa_strendswith(filename, fi.extension)) { + if (spa_strendswith(extension, fi.extension)) { format = fi.format; break; } @@ -1712,7 +1794,7 @@ static void format_from_filename(SF_INFO *info, const char *filename) if (sf_command(NULL, SFC_GET_SIMPLE_FORMAT, &fi, sizeof(fi)) != 0) continue; - if (spa_strendswith(filename, fi.extension)) { + if (spa_strendswith(extension, fi.extension)) { format = fi.format; info->format = 0; break; @@ -1720,7 +1802,7 @@ static void format_from_filename(SF_INFO *info, const char *filename) } } if (format == -1) - format = spa_streq(filename, "-") ? SF_FORMAT_AU : SF_FORMAT_WAV; + format = SF_FORMAT_WAV; if (format == SF_FORMAT_WAV && info->channels > 2) format = SF_FORMAT_WAVEX; @@ -1738,6 +1820,26 @@ static void format_from_filename(SF_INFO *info, const char *filename) info->format |= format; } +static void list_containers(struct data *d) +{ + int i, count = 0; + + fprintf(stderr, _("Supported containers and extensions:\n")); + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0) + count = 0; + + for (i = 0; i < count; i++) { + SF_FORMAT_INFO fi; + + spa_zero(fi); + fi.format = i; + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0) + continue; + + fprintf(stderr, " %s: %s\n", fi.extension, fi.name); + } +} + #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION static int setup_encodedfile(struct data *data) { @@ -1859,7 +1961,7 @@ static int setup_sndfile(struct data *data) info.samplerate = data->rate; info.channels = data->channels; info.format = fi->sf_format; - format_from_filename(&info, data->filename); + format_from_filename(&info, data->filename, data->container); } data->sndfile.file = sf_open(data->filename, @@ -2220,6 +2322,9 @@ int main(int argc, char *argv[]) case OPT_FORMAT: data.format = optarg; break; + case OPT_CONTAINER: + data.container = optarg; + break; case OPT_VOLUME: if (!spa_atof(optarg, &data.volume)) @@ -2231,6 +2336,18 @@ int main(int argc, char *argv[]) case 'c': data.data_type = TYPE_MIDI2; break; + case OPT_LISTFORMATS: + list_formats(&data); + return EXIT_SUCCESS; + case OPT_LISTCONTAINERS: + list_containers(&data); + return EXIT_SUCCESS; + case OPT_LISTLAYOUTS: + list_layouts(&data); + return EXIT_SUCCESS; + case OPT_LISTCHANNELNAMES: + list_channel_names(&data); + return EXIT_SUCCESS; default: goto error_usage; } From 494d72710872a594f919739f8c3492e98a3e6649 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 10 Feb 2026 13:18:01 +0100 Subject: [PATCH 51/93] channelmix: improve debug Instead of printing lines and lines of numbers, format everything as a matrix. Only do the work when debug is enabled. --- spa/plugins/audioconvert/channelmix-ops.c | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c index 4aa9e58c8..d832edf8c 100644 --- a/spa/plugins/audioconvert/channelmix-ops.c +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -825,7 +825,6 @@ static void impl_channelmix_set_volume(struct channelmix *mix, float volume, boo for (i = 0; i < dst_chan; i++) { for (j = 0; j < src_chan; j++) { float v = mix->matrix[i][j]; - spa_log_debug(mix->log, "%d %d: %f", i, j, v); if (i == 0 && j == 0) t = v; else if (t != v) @@ -840,7 +839,31 @@ static void impl_channelmix_set_volume(struct channelmix *mix, float volume, boo SPA_FLAG_UPDATE(mix->flags, CHANNELMIX_FLAG_IDENTITY, dst_chan == src_chan && SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_COPY)); - spa_log_debug(mix->log, "flags:%08x", mix->flags); + if (SPA_UNLIKELY(spa_log_level_topic_enabled(mix->log, + SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) { + char str1[1024], str2[1024]; + struct spa_strbuf sb1, sb2; + spa_strbuf_init(&sb2, str2, sizeof(str2)); + for (i = 0; i < dst_chan; i++) { + spa_strbuf_init(&sb1, str1, sizeof(str1)); + for (j = 0; j < src_chan; j++) { + float v = mix->matrix[i][j]; + if (i == 0) + spa_strbuf_append(&sb2, " %03d ", j); + if (v == 0.0f) + spa_strbuf_append(&sb1, " "); + else + spa_strbuf_append(&sb1, "%1.3f ", v); + } + if (i == 0 && sb2.pos > 0) + spa_log_debug(mix->log, " %s", str2); + if (sb1.pos > 0) + spa_log_debug(mix->log, "%03d %s %03d", i, str1, i); + } + if (sb2.pos > 0) + spa_log_debug(mix->log, " %s", str2); + spa_log_debug(mix->log, "flags:%08x", mix->flags); + } } static void impl_channelmix_free(struct channelmix *mix) From 47de1e15a47b058e0bc434a35b002535af4b2aa6 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 10 Feb 2026 13:34:44 +0100 Subject: [PATCH 52/93] channelmix: handle more than 64 channels When setting up the mix matrix, don't just iterate over the first 64 (CHANNEL_BITS) positions because then we will never be able to configure more than 64 channels in the matrix. Instead iterate until we have filled all src and dst entries in the matrix. For the first 64 positions we might need to check the channel mask to get the right positions in our source matrix. Fixes the channel mixer for >64 channels where the positions above 64 where 0 in the matrix and muted. Fixes #5118 --- spa/plugins/audioconvert/channelmix-ops.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c index d832edf8c..12edb4b5a 100644 --- a/spa/plugins/audioconvert/channelmix-ops.c +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -720,7 +720,7 @@ done: if (src_paired == 0) src_paired = ~0LU; - for (jc = 0, ic = 0, i = 0; i < CHANNEL_BITS; i++) { + for (jc = 0, ic = 0, i = 0; ic < dst_chan; i++) { float sum = 0.0f; char str1[1024], str2[1024]; struct spa_strbuf sb1, sb2; @@ -728,12 +728,10 @@ done: spa_strbuf_init(&sb1, str1, sizeof(str1)); spa_strbuf_init(&sb2, str2, sizeof(str2)); - if ((dst_paired & (1UL << i)) == 0) + if (i < CHANNEL_BITS && (dst_paired & (1UL << i)) == 0) continue; - for (jc = 0, j = 0; j < CHANNEL_BITS; j++) { - if ((src_paired & (1UL << j)) == 0) - continue; - if (ic >= dst_chan || jc >= src_chan) + for (jc = 0, j = 0; jc < src_chan; j++) { + if (j < CHANNEL_BITS && (src_paired & (1UL << j)) == 0) continue; if (ic == 0) @@ -752,7 +750,7 @@ done: if (sb2.pos > 0) spa_log_info(mix->log, " %s", str2); if (sb1.pos > 0) { - spa_log_info(mix->log, "%-4.4s %s %f", + spa_log_info(mix->log, "%03d %-4.4s %s %f", ic, dst_mask == 0 ? "UNK" : spa_debug_type_find_short_name(spa_type_audio_channel, i + _SH), str1, sum); From 12fb9ab831795acc928985dde00a4bec942a5fe7 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Wed, 11 Feb 2026 18:53:32 +0200 Subject: [PATCH 53/93] bluez5: use correct A2DP profile in codec switch We can only switch within currently connected A2DP profiles, as generally remote endpoints are available only for the connected ones. --- spa/plugins/bluez5/bluez5-device.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c index a63361146..fc659f655 100644 --- a/spa/plugins/bluez5/bluez5-device.c +++ b/spa/plugins/bluez5/bluez5-device.c @@ -1485,7 +1485,7 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a profiles = this->bt_dev->profiles & SPA_BT_PROFILE_BAP_DUPLEX; break; case DEVICE_PROFILE_A2DP: - profiles = this->bt_dev->profiles & SPA_BT_PROFILE_A2DP_DUPLEX; + profiles = this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_DUPLEX; break; default: profiles = 0; From 11d5e071ecacf7aaf27e73b065dd50c8ba685054 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 12 Feb 2026 12:20:14 +0100 Subject: [PATCH 54/93] stream: return -EIO when doing get_time in != STREAMING The stream should be streaming before the get_time call is meaningful. Various places in the code already check this an fall back to a default value, we just need to return an error here. --- src/pipewire/filter.c | 3 ++- src/pipewire/stream.c | 3 ++- src/pipewire/stream.h | 5 +++-- src/tests/test-stream.c | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c index bb23c9d5c..85d16a6b1 100644 --- a/src/pipewire/filter.c +++ b/src/pipewire/filter.c @@ -1986,7 +1986,8 @@ int pw_filter_get_time(struct pw_filter *filter, struct pw_time *time) pw_log_trace("%p: %"PRIi64" %"PRIi64" %"PRIu64" %d/%d ", filter, time->now, time->delay, time->ticks, time->rate.num, time->rate.denom); - return 0; + + return filter->state == PW_FILTER_STATE_STREAMING ? 0 : -EIO; } SPA_EXPORT diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c index 477861f61..4ee2138bc 100644 --- a/src/pipewire/stream.c +++ b/src/pipewire/stream.c @@ -2504,7 +2504,8 @@ int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t impl->dequeued.outcount, impl->dequeued.incount, impl->queued.outcount, impl->queued.incount, avail_buffers, impl->n_buffers); - return 0; + + return stream->state == PW_STREAM_STATE_STREAMING ? 0 : -EIO; } SPA_EXPORT diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h index 28b4eba1f..d5cd87188 100644 --- a/src/pipewire/stream.h +++ b/src/pipewire/stream.h @@ -293,7 +293,8 @@ struct pw_stream_control { * Use pw_stream_get_time_n() to get an updated time snapshot of the stream. * The time snapshot can give information about the time in the driver of the * graph, the delay to the edge of the graph and the internal queuing in the - * stream. + * stream. This function should only be called in the STREAMING state and will + * return an error when called in any other state. * * pw_time.ticks gives a monotonic increasing counter of the time in the graph * driver. I can be used to generate a timeline to schedule samples as well @@ -594,7 +595,7 @@ const struct pw_stream_control *pw_stream_get_control(struct pw_stream *stream, /** Set control values */ int pw_stream_set_control(struct pw_stream *stream, uint32_t id, uint32_t n_values, float *values, ...); -/** Query the time on the stream, RT safe */ +/** Query the time on the stream. Returns an error when the stream is not running. RT safe */ int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size); /** Get the current time in nanoseconds. This value can be compared with diff --git a/src/tests/test-stream.c b/src/tests/test-stream.c index d56b6c85b..b5061e927 100644 --- a/src/tests/test-stream.c +++ b/src/tests/test-stream.c @@ -151,7 +151,7 @@ static void test_create(void) /* check id, only when connected */ spa_assert_se(pw_stream_get_node_id(stream) == SPA_ID_INVALID); - spa_assert_se(pw_stream_get_time_n(stream, &tm, sizeof(tm)) == 0); + spa_assert_se(pw_stream_get_time_n(stream, &tm, sizeof(tm)) == -EIO); spa_assert_se(tm.now == 0); spa_assert_se(tm.rate.num == 0); spa_assert_se(tm.rate.denom == 0); From 7f08c0d404728a31c191a0f4bdfa315c7b0efbf4 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 12 Feb 2026 13:46:04 +0100 Subject: [PATCH 55/93] doc: fix a typo --- doc/dox/programs/pw-top.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dox/programs/pw-top.1.md b/doc/dox/programs/pw-top.1.md index e1a866b55..1207e3205 100644 --- a/doc/dox/programs/pw-top.1.md +++ b/doc/dox/programs/pw-top.1.md @@ -13,7 +13,7 @@ node and device statistics. A hierarchical view is shown of Driver nodes and follower nodes. The Driver nodes are actively using a timer to schedule dataflow in the -followers. The followers of a driver node as shown below their driver +followers. The followers of a driver node are shown below their driver with a + sign (or = for async nodes) in a tree-like representation. The columns presented are as follows: From ca4fa88598fadcf723abd3c989ee93e43ac7d722 Mon Sep 17 00:00:00 2001 From: Jonas Holmberg Date: Thu, 12 Feb 2026 17:15:33 +0100 Subject: [PATCH 56/93] context: set time in position for drivers Set time in position for drivers to make sure an old time isn't copied by followers before the driver is started. --- src/pipewire/context.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pipewire/context.c b/src/pipewire/context.c index ddcdd2cba..1c30be70a 100644 --- a/src/pipewire/context.c +++ b/src/pipewire/context.c @@ -1800,6 +1800,9 @@ again: n->target_rate = n->rt.position->clock.target_rate; } + if (n->info.state < PW_NODE_STATE_RUNNING) + n->rt.position->clock.nsec = get_time_ns(n->rt.target.system); + SPA_FLAG_UPDATE(n->rt.position->clock.flags, SPA_IO_CLOCK_FLAG_LAZY, have_request && n->supports_lazy > 0); From b0b6f6ca37fe758b77dbb93e58fe1498766e2418 Mon Sep 17 00:00:00 2001 From: lumingzh Date: Fri, 13 Feb 2026 09:40:40 +0800 Subject: [PATCH 57/93] update Chinese translation --- po/zh_CN.po | 157 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 60 deletions(-) diff --git a/po/zh_CN.po b/po/zh_CN.po index 4181ef3df..8be588b03 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -6,15 +6,15 @@ # Cheng-Chia Tseng , 2010, 2012. # Frank Hill , 2015. # Mingye Wang (Arthur2e5) , 2015. -# lumingzh , 2024-2025. +# lumingzh , 2024-2026. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2025-11-25 15:35+0000\n" -"PO-Revision-Date: 2025-11-26 10:19+0800\n" +"POT-Creation-Date: 2026-02-11 16:53+0000\n" +"PO-Revision-Date: 2026-02-13 09:36+0800\n" "Last-Translator: lumingzh \n" "Language-Team: Chinese (China) \n" "Language: zh_CN\n" @@ -65,21 +65,46 @@ msgstr "虚拟输出" msgid "Tunnel for %s@%s" msgstr "用于 %s@%s 的隧道" -#: src/modules/module-zeroconf-discover.c:320 +#: src/modules/module-zeroconf-discover.c:326 msgid "Unknown device" msgstr "未知设备" -#: src/modules/module-zeroconf-discover.c:332 +#: src/modules/module-zeroconf-discover.c:338 #, c-format msgid "%s on %s@%s" msgstr "%2$s@%3$s 上的 %1$s" -#: src/modules/module-zeroconf-discover.c:336 +#: src/modules/module-zeroconf-discover.c:342 #, c-format msgid "%s on %s" msgstr "%2$s 上的 %1$s" -#: src/tools/pw-cat.c:1088 +#: src/tools/pw-cat.c:264 +#, c-format +msgid "Supported formats:\n" +msgstr "支持的格式:\n" + +#: src/tools/pw-cat.c:749 +#, c-format +msgid "Supported channel layouts:\n" +msgstr "支持的声道布局:\n" + +#: src/tools/pw-cat.c:759 +#, c-format +msgid "Supported channel layout aliases:\n" +msgstr "支持的声道布局别名:\n" + +#: src/tools/pw-cat.c:761 +#, c-format +msgid " %s -> %s\n" +msgstr " %s -> %s\n" + +#: src/tools/pw-cat.c:766 +#, c-format +msgid "Supported channel names:\n" +msgstr "支持的声道名称:\n" + +#: src/tools/pw-cat.c:1177 #, c-format msgid "" "%s [options] [|-]\n" @@ -94,7 +119,7 @@ msgstr "" " -v, --verbose 输出详细操作\n" "\n" -#: src/tools/pw-cat.c:1095 +#: src/tools/pw-cat.c:1184 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -126,20 +151,23 @@ msgstr "" " -P --properties 设置节点属性\n" "\n" -#: src/tools/pw-cat.c:1113 +#: src/tools/pw-cat.c:1202 #, c-format msgid "" -" --rate Sample rate (req. for rec) (default " -"%u)\n" -" --channels Number of channels (req. for rec) " -"(default %u)\n" +" --rate Sample rate (default %u)\n" +" --channels Number of channels (default %u)\n" " --channel-map Channel map\n" -" one of: \"Stereo\", \"5.1\",... " -"or\n" +" a channel layout: \"Stereo\", " +"\"5.1\",... or\n" " comma separated list of channel " "names: eg. \"FL,FR\"\n" -" --format Sample format %s (req. for rec) " -"(default %s)\n" +" --list-layouts List supported channel layouts\n" +" --list-channel-names List supported channel maps\n" +" --format Sample format (default %s)\n" +" --list-formats List supported sample formats\n" +" --container Container format\n" +" --list-containers List supported containers and " +"extensions\n" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" @@ -149,15 +177,19 @@ msgid "" " -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" -" --rate 采样率 (录制模式需要) (默认 %u)\n" -" --channels 通道数 (录制模式需要) (默认 %u)\n" -" --channel-map 通道映射\n" -" \"stereo\", \"5.1\",... 中的其一" -"或\n" -" 以英文逗号分隔的通道名列表: 如 " +" --rate 采样率 (默认 %u)\n" +" --channels 声道数 (默认 %u)\n" +" --channel-map 声道映射\n" +" 声道布局:\"stereo\", " +"\"5.1\",... 或\n" +" 以英文逗号分隔的声道名列表: 如 " "\"FL,FR\"\n" -" --format 采样格式 %s (录制模式需要) (默认 " -"%s)\n" +" --list-layouts 列出支持的声道布局\n" +" --list-channel-names 列出支持的声道映射\n" +" --format 采样格式 (默认 %s)\n" +" --list-formats 列出支持的采样格式\n" +" --container 容器格式\n" +" --list-containers 列出支持的容器和扩展\n" " --volume 媒体流音量 0-1.0 (默认 %.3f)\n" " -q --quality 重采样质量 (0 - 15) (默认 %d)\n" " -a, --raw 原生模式\n" @@ -166,7 +198,7 @@ msgstr "" " -n, --sample-count COUNT 计数采样后停止\n" "\n" -#: src/tools/pw-cat.c:1133 +#: src/tools/pw-cat.c:1227 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" @@ -186,6 +218,11 @@ msgstr "" " -c, --midi-clip MIDI 剪辑模式\n" "\n" +#: src/tools/pw-cat.c:1827 +#, c-format +msgid "Supported containers and extensions:\n" +msgstr "支持的容器和扩展:\n" + #: src/tools/pw-cli.c:2386 #, c-format msgid "" @@ -209,7 +246,7 @@ msgid "Pro Audio" msgstr "专业音频" #: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 -#: spa/plugins/bluez5/bluez5-device.c:1976 +#: spa/plugins/bluez5/bluez5-device.c:2021 msgid "Off" msgstr "关" @@ -241,7 +278,7 @@ msgstr "输入插孔" #: spa/plugins/alsa/acp/alsa-mixer.c:2726 #: spa/plugins/alsa/acp/alsa-mixer.c:2810 -#: spa/plugins/bluez5/bluez5-device.c:2374 +#: spa/plugins/bluez5/bluez5-device.c:2422 msgid "Microphone" msgstr "话筒" @@ -307,15 +344,15 @@ msgid "No Bass Boost" msgstr "无重低音增强" #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2380 +#: spa/plugins/bluez5/bluez5-device.c:2428 msgid "Speaker" msgstr "扬声器" #. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2742 #: spa/plugins/alsa/acp/alsa-mixer.c:2820 -#: spa/plugins/bluez5/bluez5-device.c:2386 -#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2434 +#: spa/plugins/bluez5/bluez5-device.c:2501 msgid "Headphones" msgstr "模拟耳机" @@ -425,7 +462,7 @@ msgstr "立体声" #: spa/plugins/alsa/acp/alsa-mixer.c:4535 #: spa/plugins/alsa/acp/alsa-mixer.c:4693 -#: spa/plugins/bluez5/bluez5-device.c:2362 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Headset" msgstr "耳机" @@ -620,101 +657,101 @@ msgstr "内置音频" msgid "Modem" msgstr "调制解调器" -#: spa/plugins/bluez5/bluez5-device.c:1987 +#: spa/plugins/bluez5/bluez5-device.c:2032 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)" -#: spa/plugins/bluez5/bluez5-device.c:2016 +#: spa/plugins/bluez5/bluez5-device.c:2061 msgid "Audio Streaming for Hearing Aids (ASHA Sink)" msgstr "助听器音频流 (ASHA 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2059 +#: spa/plugins/bluez5/bluez5-device.c:2104 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "高保真回放 (A2DP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2062 +#: spa/plugins/bluez5/bluez5-device.c:2107 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2070 +#: spa/plugins/bluez5/bluez5-device.c:2115 msgid "High Fidelity Playback (A2DP Sink)" msgstr "高保真回放 (A2DP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2072 +#: spa/plugins/bluez5/bluez5-device.c:2117 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "高保真双工 (A2DP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2146 +#: spa/plugins/bluez5/bluez5-device.c:2194 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "高保真回放 (BAP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2151 +#: spa/plugins/bluez5/bluez5-device.c:2199 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "高保真输入 (BAP 信源, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2155 +#: spa/plugins/bluez5/bluez5-device.c:2203 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "高保真双工 (BAP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2164 +#: spa/plugins/bluez5/bluez5-device.c:2212 msgid "High Fidelity Playback (BAP Sink)" msgstr "高保真回放 (BAP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2168 +#: spa/plugins/bluez5/bluez5-device.c:2216 msgid "High Fidelity Input (BAP Source)" msgstr "高保真输入 (BAP 信源)" -#: spa/plugins/bluez5/bluez5-device.c:2171 +#: spa/plugins/bluez5/bluez5-device.c:2219 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "高保真双工 (BAP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2211 +#: spa/plugins/bluez5/bluez5-device.c:2259 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "头戴式耳机单元 (HSP/HFP, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2363 -#: spa/plugins/bluez5/bluez5-device.c:2368 -#: spa/plugins/bluez5/bluez5-device.c:2375 -#: spa/plugins/bluez5/bluez5-device.c:2381 -#: spa/plugins/bluez5/bluez5-device.c:2387 -#: spa/plugins/bluez5/bluez5-device.c:2393 -#: spa/plugins/bluez5/bluez5-device.c:2399 -#: spa/plugins/bluez5/bluez5-device.c:2405 #: spa/plugins/bluez5/bluez5-device.c:2411 +#: spa/plugins/bluez5/bluez5-device.c:2416 +#: spa/plugins/bluez5/bluez5-device.c:2423 +#: spa/plugins/bluez5/bluez5-device.c:2429 +#: spa/plugins/bluez5/bluez5-device.c:2435 +#: spa/plugins/bluez5/bluez5-device.c:2441 +#: spa/plugins/bluez5/bluez5-device.c:2447 +#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2459 msgid "Handsfree" msgstr "免提" -#: spa/plugins/bluez5/bluez5-device.c:2369 +#: spa/plugins/bluez5/bluez5-device.c:2417 msgid "Handsfree (HFP)" msgstr "免提(HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2392 +#: spa/plugins/bluez5/bluez5-device.c:2440 msgid "Portable" msgstr "便携式" -#: spa/plugins/bluez5/bluez5-device.c:2398 +#: spa/plugins/bluez5/bluez5-device.c:2446 msgid "Car" msgstr "车内" -#: spa/plugins/bluez5/bluez5-device.c:2404 +#: spa/plugins/bluez5/bluez5-device.c:2452 msgid "HiFi" msgstr "高保真" -#: spa/plugins/bluez5/bluez5-device.c:2410 +#: spa/plugins/bluez5/bluez5-device.c:2458 msgid "Phone" msgstr "电话" -#: spa/plugins/bluez5/bluez5-device.c:2417 +#: spa/plugins/bluez5/bluez5-device.c:2465 msgid "Bluetooth" msgstr "蓝牙" -#: spa/plugins/bluez5/bluez5-device.c:2418 +#: spa/plugins/bluez5/bluez5-device.c:2466 msgid "Bluetooth Handsfree" msgstr "蓝牙免提" From 5f12dd99a3b0da3ee2768ea8b12b7018571b58d0 Mon Sep 17 00:00:00 2001 From: Alexander Sarmanow Date: Tue, 10 Feb 2026 18:34:27 +0100 Subject: [PATCH 58/93] bluez5: add adapter reference to remote_endpoint This should make adapter members easier accessible via remote endpoints. --- spa/plugins/bluez5/bluez5-dbus.c | 42 +++++++++++++++++++++++++------- spa/plugins/bluez5/defs.h | 1 + 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 651d410c9..43a108b2d 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -148,6 +148,7 @@ struct spa_bt_monitor { struct spa_bt_remote_endpoint { struct spa_list link; struct spa_list device_link; + struct spa_list adapter_link; struct spa_bt_monitor *monitor; char *path; char *transport_path; @@ -155,6 +156,7 @@ struct spa_bt_remote_endpoint { char *uuid; unsigned int codec; struct spa_bt_device *device; + struct spa_bt_adapter *adapter; uint8_t *capabilities; size_t capabilities_len; uint8_t *metadata; @@ -1641,6 +1643,8 @@ static struct spa_bt_adapter *adapter_create(struct spa_bt_monitor *monitor, con d->monitor = monitor; d->path = strdup(path); + spa_list_init(&d->remote_endpoint_list); + spa_list_prepend(&monitor->adapter_list, &d->link); adapter_init_bus_type(monitor, d); @@ -1655,6 +1659,7 @@ static void adapter_free(struct spa_bt_adapter *adapter) { struct spa_bt_monitor *monitor = adapter->monitor; struct spa_bt_device *d, *td; + struct spa_bt_remote_endpoint *ep, *tep; spa_log_debug(monitor->log, "%p", adapter); @@ -1663,6 +1668,13 @@ static void adapter_free(struct spa_bt_adapter *adapter) if (d->adapter == adapter) device_free(d); + spa_list_for_each_safe(ep, tep, &adapter->remote_endpoint_list, adapter_link) { + if (ep->adapter == adapter) { + spa_list_remove(&ep->adapter_link); + ep->adapter = NULL; + } + } + spa_bt_player_destroy(adapter->dummy_player); spa_list_remove(&adapter->link); @@ -3024,19 +3036,31 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en } else if (spa_streq(key, "Device")) { struct spa_bt_device *device; + struct spa_bt_adapter *adapter; device = spa_bt_device_find(monitor, value); - if (device == NULL) - goto next; + adapter = adapter_find(monitor, value); + if (device != NULL) { + spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device); - spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device); + if (remote_endpoint->device != device) { + if (remote_endpoint->device != NULL) + spa_list_remove(&remote_endpoint->device_link); + remote_endpoint->device = device; + if (device != NULL) + spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); + } + } + if (adapter != NULL) { + spa_log_debug(monitor->log, "remote_endpoint %p: adapter -> %p", remote_endpoint, adapter); - if (remote_endpoint->device != device) { - if (remote_endpoint->device != NULL) - spa_list_remove(&remote_endpoint->device_link); - remote_endpoint->device = device; - if (device != NULL) - spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); + if (remote_endpoint->adapter != adapter) { + if (remote_endpoint->adapter != NULL) + spa_list_remove(&remote_endpoint->adapter_link); + remote_endpoint->adapter = adapter; + if (adapter != NULL) + spa_list_append(&adapter->remote_endpoint_list, &remote_endpoint->adapter_link); + } } } else if (spa_streq(key, "Transport")) { /* For ASHA */ diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index afc56c920..24a85a5c9 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -379,6 +379,7 @@ struct spa_bt_adapter { unsigned int has_media1_interface:1; unsigned int le_audio_bcast_supported:1; unsigned int tx_timestamping_supported:1; + struct spa_list remote_endpoint_list; }; enum spa_bt_form_factor { From 5c9b3ee05a18cfca72a7d161ed6cbb1ecf845cbe Mon Sep 17 00:00:00 2001 From: Alexander Sarmanow Date: Tue, 10 Feb 2026 18:36:45 +0100 Subject: [PATCH 59/93] bluez5: bap: use BD address for per-adapter BIG config HCI indexed names are not stable. The adapters BD address is here a better approach to map the configuration. --- doc/dox/config/pipewire-props.7.md | 2 +- spa/plugins/bluez5/bluez5-dbus.c | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index cbefba992..e561979d5 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1211,7 +1211,7 @@ PipeWire Opus Pro audio profile duplex max bitrate. PipeWire Opus Pro audio profile duplex frame duration (1/10 ms). @PAR@ monitor-prop bluez5.bcast_source.config = [] # JSON -For a per-adapter configuration of multiple BIGs use an "adapter" entry in the BIG with the HCI device name (e.g. hci0). +For a per-adapter configuration of multiple BIGs use an "adapter" entry in the BIG with the BD address. \parblock Example: ``` diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 43a108b2d..21ab69dcf 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -196,7 +196,7 @@ struct spa_bt_bis { }; #define BROADCAST_CODE_LEN 16 -#define HCI_DEV_NAME_LEN 8 +#define BD_ADDR_STR_LEN 17 struct spa_bt_big { struct spa_list link; @@ -205,7 +205,7 @@ struct spa_bt_big { struct spa_list bis_list; int big_id; int sync_factor; - char adapter[HCI_DEV_NAME_LEN]; + char adapter[BD_ADDR_STR_LEN + 3]; }; /* @@ -6269,6 +6269,7 @@ static void configure_bis(struct spa_bt_monitor *monitor, } static void configure_bcast_source(struct spa_bt_monitor *monitor, + struct spa_bt_remote_endpoint *ep, const struct media_codec *codec, DBusConnection *conn, const char *object_path, @@ -6277,15 +6278,19 @@ static void configure_bcast_source(struct spa_bt_monitor *monitor, { struct spa_bt_big *big; struct spa_bt_bis *bis; - char *pos; + /* Configure each BIS from a BIG */ spa_list_for_each(big, &monitor->bcast_source_config_list, link) { /* Apply per adapter configuration if BIG has an adapter value stated, * otherwise apply the BIG config angnostically to each adapter */ - if (strlen(big->adapter) > 0) { - pos = strstr(object_path, big->adapter); - if (pos == NULL) + if ((strlen(big->adapter) > 0) && (ep->adapter != NULL)) { + if (!ep->adapter->address) { + spa_log_warn(monitor->log, "this adapter is not associated with any BD address. BIG config will applied agnostically to any adapter!"); + continue; + } + + if (strcasecmp(ep->adapter->address, big->adapter)) continue; spa_log_debug(monitor->log, "configuring BIG for adapter=%s", big->adapter); @@ -6414,7 +6419,7 @@ static void interface_added(struct spa_bt_monitor *monitor, } if (local_endpoint != NULL) - configure_bcast_source(monitor, monitor->media_codecs[i], conn, object_path, interface_name, local_endpoint); + configure_bcast_source(monitor, ep, monitor->media_codecs[i], conn, object_path, interface_name, local_endpoint); } } } @@ -7034,7 +7039,6 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const char bis_key[256]; char qos_key[256]; char bcode[BROADCAST_CODE_LEN + 3]; - char adapter[HCI_DEV_NAME_LEN + 3]; int cursor; int big_id = 0; struct spa_json it[3], it_array[4]; @@ -7071,11 +7075,8 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const memcpy(big_entry->broadcast_code, bcode, strlen(bcode)); spa_log_debug(monitor->log, "big_entry->broadcast_code %s", big_entry->broadcast_code); } else if (spa_streq(key, "adapter")) { - if (spa_json_get_string(&it[1], adapter, sizeof(adapter)) <= 0) + if (spa_json_get_string(&it[1], big_entry->adapter, sizeof(big_entry->adapter)) <= 0) goto parse_failed; - if (strlen(adapter) > HCI_DEV_NAME_LEN) - goto parse_failed; - memcpy(big_entry->adapter, adapter, strlen(adapter)); spa_log_debug(monitor->log, "big_entry->adapter %s", big_entry->adapter); } else if (spa_streq(key, "encryption")) { if (spa_json_get_bool(&it[0], &big_entry->encryption) <= 0) From 63129dd3dc2eda1a41ee5935e3412be5cc246249 Mon Sep 17 00:00:00 2001 From: Alexander Sarmanow Date: Thu, 12 Feb 2026 10:51:38 +0100 Subject: [PATCH 60/93] fixup --- spa/plugins/bluez5/bluez5-dbus.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 21ab69dcf..4b06093fa 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -3040,6 +3040,7 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en device = spa_bt_device_find(monitor, value); adapter = adapter_find(monitor, value); + if (device != NULL) { spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device); @@ -3047,8 +3048,7 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en if (remote_endpoint->device != NULL) spa_list_remove(&remote_endpoint->device_link); remote_endpoint->device = device; - if (device != NULL) - spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); + spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); } } if (adapter != NULL) { @@ -3058,8 +3058,7 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en if (remote_endpoint->adapter != NULL) spa_list_remove(&remote_endpoint->adapter_link); remote_endpoint->adapter = adapter; - if (adapter != NULL) - spa_list_append(&adapter->remote_endpoint_list, &remote_endpoint->adapter_link); + spa_list_append(&adapter->remote_endpoint_list, &remote_endpoint->adapter_link); } } } else if (spa_streq(key, "Transport")) { @@ -3197,6 +3196,9 @@ static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint) if (remote_endpoint->device) spa_list_remove(&remote_endpoint->device_link); + if (remote_endpoint->adapter) + spa_list_remove(&remote_endpoint->adapter_link); + bap_features_clear(&remote_endpoint->bap_features); spa_list_remove(&remote_endpoint->link); From 7dd2c60b12b8295206bfbe231fcbaa4e3a6f9adb Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Mon, 24 Nov 2025 14:36:32 +0100 Subject: [PATCH 61/93] bluez5: synchronize transport state after acquire of an acquired transport With keepalive enabled, we need to emit state change event on acquire similarly as we do if refcount was already positive. Co-authored-by: Martin Geier --- spa/plugins/bluez5/bluez5-dbus.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 4b06093fa..21a5e53de 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -3419,8 +3419,12 @@ int spa_bt_transport_acquire(struct spa_bt_transport *transport, bool optional) if (!transport->acquired) res = spa_bt_transport_impl(transport, acquire, 0, optional); - else - res = 0; + else { + /* keepalive */ + transport->acquire_refcount = 1; + spa_bt_transport_emit_state_changed(transport, transport->state, transport->state); + return 0; + } if (res >= 0) { transport->acquire_refcount = 1; From fc723d7b156ccd0d37036d267e3a024e6963801e Mon Sep 17 00:00:00 2001 From: Yedaya Katsman Date: Sun, 15 Feb 2026 10:18:34 +0000 Subject: [PATCH 62/93] pw-mon: Fix help message for --hide-params --- src/tools/pw-mon.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/pw-mon.c b/src/tools/pw-mon.c index e66de979c..b4601b906 100644 --- a/src/tools/pw-mon.c +++ b/src/tools/pw-mon.c @@ -780,7 +780,7 @@ static void show_help(const char *name, bool error) " -N, --no-colors disable color output\n" " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n" " -o, --hide-props hide node properties\n" - " -a, --hide-params hide node properties\n" + " -a, --hide-params hide node parameters\n" " -p, --print-separator print empty line after every event to help streaming parser\n", name); } From de778e623504d614f4ccca21a5acefce95ed6565 Mon Sep 17 00:00:00 2001 From: filmsi Date: Sun, 15 Feb 2026 15:28:34 +0000 Subject: [PATCH 63/93] Replace sl.po --- po/sl.po | 155 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 60 deletions(-) diff --git a/po/sl.po b/po/sl.po index ae85f043f..cf92eaffc 100644 --- a/po/sl.po +++ b/po/sl.po @@ -2,23 +2,21 @@ # Copyright (C) 2024 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # -# Martin , 2024, 2025. +# Martin , 2024, 2025, 2026. # msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" -"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" -"issues\n" -"POT-Creation-Date: 2025-12-04 15:34+0000\n" -"PO-Revision-Date: 2025-12-07 08:53+0100\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues\n" +"POT-Creation-Date: 2026-02-09 12:55+0000\n" +"PO-Revision-Date: 2026-02-15 16:18+0100\n" "Last-Translator: Martin Srebotnjak \n" "Language-Team: Slovenian \n" "Language: sl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n" -"%100<=4 ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);\n" "X-Generator: Poedit 2.2.1\n" #: src/daemon/pipewire.c:29 @@ -35,8 +33,7 @@ msgstr "" " -h, --help Pokaži to pomoč\n" " -v, --verbose Povečaj opisnost za eno raven\n" " --version Pokaži različico\n" -" -c, --config Naloži prilagoditev config (privzeto " -"%s)\n" +" -c, --config Naloži prilagoditev config (privzeto %s)\n" " -P --properties Določi lastnosti konteksta\n" #: src/daemon/pipewire.desktop.in:3 @@ -62,21 +59,46 @@ msgstr "Lažni izhod" msgid "Tunnel for %s@%s" msgstr "Prehod za %s@%s" -#: src/modules/module-zeroconf-discover.c:320 +#: src/modules/module-zeroconf-discover.c:326 msgid "Unknown device" msgstr "Neznana naprava" -#: src/modules/module-zeroconf-discover.c:332 +#: src/modules/module-zeroconf-discover.c:338 #, c-format msgid "%s on %s@%s" msgstr "%s na %s@%s" -#: src/modules/module-zeroconf-discover.c:336 +#: src/modules/module-zeroconf-discover.c:342 #, c-format msgid "%s on %s" msgstr "%s na %s" -#: src/tools/pw-cat.c:1103 +#: src/tools/pw-cat.c:264 +#, c-format +msgid "Supported formats:\n" +msgstr "Podprti zapisi:\n" + +#: src/tools/pw-cat.c:749 +#, c-format +msgid "Supported channel layouts:\n" +msgstr "Podprte postavitve kanalov:\n" + +#: src/tools/pw-cat.c:759 +#, c-format +msgid "Supported channel layout aliases:\n" +msgstr "Podprti vzdevki postavitev kanalov:\n" + +#: src/tools/pw-cat.c:761 +#, c-format +msgid " %s -> %s\n" +msgstr " %s -> %s\n" + +#: src/tools/pw-cat.c:766 +#, c-format +msgid "Supported channel names:\n" +msgstr "Podprta imena kanalov:\n" + +#: src/tools/pw-cat.c:1177 #, c-format msgid "" "%s [options] [|-]\n" @@ -92,7 +114,7 @@ msgstr "" "\n" "\n" -#: src/tools/pw-cat.c:1110 +#: src/tools/pw-cat.c:1184 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -129,20 +151,23 @@ msgstr "" " -P --properties Nastavi lastnosti vozlišča\n" "\n" -#: src/tools/pw-cat.c:1128 +#: src/tools/pw-cat.c:1202 #, c-format msgid "" -" --rate Sample rate (req. for rec) (default " -"%u)\n" -" --channels Number of channels (req. for rec) " -"(default %u)\n" +" --rate Sample rate (default %u)\n" +" --channels Number of channels (default %u)\n" " --channel-map Channel map\n" -" one of: \"Stereo\", \"5.1\",... " -"or\n" +" a channel layout: \"Stereo\", " +"\"5.1\",... or\n" " comma separated list of channel " "names: eg. \"FL,FR\"\n" -" --format Sample format %s (req. for rec) " -"(default %s)\n" +" --list-layouts List supported channel layouts\n" +" --list-channel-names List supported channel maps\n" +" --format Sample format (default %s)\n" +" --list-formats List supported sample formats\n" +" --container Container format\n" +" --list-containers List supported containers and " +"extensions\n" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" @@ -161,8 +186,13 @@ msgstr "" "\"5.1\",... ali\n" " seznam imen kanalov, ločen z " "vejico: npr. \"FL,FR\"\n" -" --format Vzorčne oblike zapisa %s (zahtevano " -"za rec) (privzeto %s)\n" +" —list-layouts Izpiše podprte postavitve kanalov\n" +" —list-channel-names Izpiše podprte preslikave kanalov\n" +" --format Oblika zapisa vzorcev (privzeto %s)\n" +" —list-formats Izpiše podprte zapise vzorcev\n" +" —container Oblika vsebnika\n" +" —list-containers Seznam podprtih vsebnikov in " +"razširitev\n" " --volume Glasnost toka 0-1.0 (privzeto %.3f)\n" " -q --quality Kakovost prevzorčenja (0 - 15) " "(privzeto %d)\n" @@ -172,7 +202,7 @@ msgstr "" " -n, --sample-count ŠTEVEC Ustavi po ŠTEVEC vzorcih\n" "\n" -#: src/tools/pw-cat.c:1148 +#: src/tools/pw-cat.c:1227 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" @@ -192,6 +222,11 @@ msgstr "" " -c, --midi-clip Način posnetka MIDI\n" "\n" +#: src/tools/pw-cat.c:1827 +#, c-format +msgid "Supported containers and extensions:\n" +msgstr "Podprti vsebniki in razširitve:\n" + #: src/tools/pw-cli.c:2386 #, c-format msgid "" @@ -217,7 +252,7 @@ msgid "Pro Audio" msgstr "Profesionalni zvok" #: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 -#: spa/plugins/bluez5/bluez5-device.c:1976 +#: spa/plugins/bluez5/bluez5-device.c:2021 msgid "Off" msgstr "Izklopljeno" @@ -249,7 +284,7 @@ msgstr "Linijski vhod" #: spa/plugins/alsa/acp/alsa-mixer.c:2726 #: spa/plugins/alsa/acp/alsa-mixer.c:2810 -#: spa/plugins/bluez5/bluez5-device.c:2374 +#: spa/plugins/bluez5/bluez5-device.c:2422 msgid "Microphone" msgstr "Mikrofon" @@ -315,15 +350,15 @@ msgid "No Bass Boost" msgstr "Brez ojačitve nizkih tonov" #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2380 +#: spa/plugins/bluez5/bluez5-device.c:2428 msgid "Speaker" msgstr "Zvočnik" #. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2742 #: spa/plugins/alsa/acp/alsa-mixer.c:2820 -#: spa/plugins/bluez5/bluez5-device.c:2386 -#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2434 +#: spa/plugins/bluez5/bluez5-device.c:2501 msgid "Headphones" msgstr "Slušalke" @@ -433,7 +468,7 @@ msgstr "Stereo" #: spa/plugins/alsa/acp/alsa-mixer.c:4535 #: spa/plugins/alsa/acp/alsa-mixer.c:4693 -#: spa/plugins/bluez5/bluez5-device.c:2362 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Headset" msgstr "Slušalka" @@ -680,100 +715,100 @@ msgstr "Vgrajen zvok" msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1987 +#: spa/plugins/bluez5/bluez5-device.c:2032 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Zvožni prehod (vir A2DP in HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:2016 +#: spa/plugins/bluez5/bluez5-device.c:2061 msgid "Audio Streaming for Hearing Aids (ASHA Sink)" msgstr "Pretakanje zvoka za slušne aparate (ponor ASHA)" -#: spa/plugins/bluez5/bluez5-device.c:2059 +#: spa/plugins/bluez5/bluez5-device.c:2104 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Predvajanje visoke ločljivosti (ponor A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2062 +#: spa/plugins/bluez5/bluez5-device.c:2107 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2070 +#: spa/plugins/bluez5/bluez5-device.c:2115 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Predvajanje visoke ločljivosti (ponor A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:2072 +#: spa/plugins/bluez5/bluez5-device.c:2117 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:2146 +#: spa/plugins/bluez5/bluez5-device.c:2194 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Predvajanje visoke ločljivosti (ponor BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2151 +#: spa/plugins/bluez5/bluez5-device.c:2199 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Vhod visoke ločljivosti (vir BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2155 +#: spa/plugins/bluez5/bluez5-device.c:2203 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Dupleks visoke ločljivosti (vir/ponor BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2164 +#: spa/plugins/bluez5/bluez5-device.c:2212 msgid "High Fidelity Playback (BAP Sink)" msgstr "Predvajanje visoke ločljivosti (ponor BAP)" -#: spa/plugins/bluez5/bluez5-device.c:2168 +#: spa/plugins/bluez5/bluez5-device.c:2216 msgid "High Fidelity Input (BAP Source)" msgstr "Vhod visoke ločljivosti (vir BAP)" -#: spa/plugins/bluez5/bluez5-device.c:2171 +#: spa/plugins/bluez5/bluez5-device.c:2219 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Dupleks visoke ločljivosti (vir/ponor BAP)" -#: spa/plugins/bluez5/bluez5-device.c:2211 +#: spa/plugins/bluez5/bluez5-device.c:2259 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Naglavna enota slušalk (HSP/HFP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2363 -#: spa/plugins/bluez5/bluez5-device.c:2368 -#: spa/plugins/bluez5/bluez5-device.c:2375 -#: spa/plugins/bluez5/bluez5-device.c:2381 -#: spa/plugins/bluez5/bluez5-device.c:2387 -#: spa/plugins/bluez5/bluez5-device.c:2393 -#: spa/plugins/bluez5/bluez5-device.c:2399 -#: spa/plugins/bluez5/bluez5-device.c:2405 #: spa/plugins/bluez5/bluez5-device.c:2411 +#: spa/plugins/bluez5/bluez5-device.c:2416 +#: spa/plugins/bluez5/bluez5-device.c:2423 +#: spa/plugins/bluez5/bluez5-device.c:2429 +#: spa/plugins/bluez5/bluez5-device.c:2435 +#: spa/plugins/bluez5/bluez5-device.c:2441 +#: spa/plugins/bluez5/bluez5-device.c:2447 +#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2459 msgid "Handsfree" msgstr "Prostoročno telefoniranje" -#: spa/plugins/bluez5/bluez5-device.c:2369 +#: spa/plugins/bluez5/bluez5-device.c:2417 msgid "Handsfree (HFP)" msgstr "Prostoročno telefoniranje (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2392 +#: spa/plugins/bluez5/bluez5-device.c:2440 msgid "Portable" msgstr "Prenosna naprava" -#: spa/plugins/bluez5/bluez5-device.c:2398 +#: spa/plugins/bluez5/bluez5-device.c:2446 msgid "Car" msgstr "Avtomobil" -#: spa/plugins/bluez5/bluez5-device.c:2404 +#: spa/plugins/bluez5/bluez5-device.c:2452 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:2410 +#: spa/plugins/bluez5/bluez5-device.c:2458 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2417 +#: spa/plugins/bluez5/bluez5-device.c:2465 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2418 +#: spa/plugins/bluez5/bluez5-device.c:2466 msgid "Bluetooth Handsfree" msgstr "Bluetooth - prostoročno" From ab70dae0a81261141df37eecb6d5b978b4d2e004 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 16 Feb 2026 10:36:29 +0100 Subject: [PATCH 64/93] modules: add PRIORITY_SESSION For driver nodes, priority.session is needed to be able to change the default device. Fixes #5125 --- src/modules/module-jack-tunnel.c | 2 ++ src/modules/module-netjack2-driver.c | 2 ++ src/modules/module-protocol-pulse/modules/module-pipe-source.c | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 760da1669..0c0cee034 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -1157,11 +1157,13 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_properties_set(impl->sink.props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_DRIVER, "30001"); + pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_SESSION, "2001"); pw_properties_set(impl->sink.props, PW_KEY_NODE_NAME, "jack_sink"); pw_properties_set(impl->sink.props, PW_KEY_NODE_DESCRIPTION, "JACK Sink"); pw_properties_set(impl->source.props, PW_KEY_MEDIA_CLASS, "Audio/Source"); pw_properties_set(impl->source.props, PW_KEY_PRIORITY_DRIVER, "30000"); + pw_properties_set(impl->source.props, PW_KEY_PRIORITY_SESSION, "2000"); pw_properties_set(impl->source.props, PW_KEY_NODE_NAME, "jack_source"); pw_properties_set(impl->source.props, PW_KEY_NODE_DESCRIPTION, "JACK Source"); diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 07a756266..ca4e70195 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -1319,9 +1319,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_DRIVER, "40000"); + pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_SESSION, "2000"); pw_properties_set(impl->sink.props, PW_KEY_NODE_NAME, "netjack2_driver_send"); pw_properties_set(impl->source.props, PW_KEY_PRIORITY_DRIVER, "40001"); + pw_properties_set(impl->source.props, PW_KEY_PRIORITY_SESSION, "2001"); pw_properties_set(impl->source.props, PW_KEY_NODE_NAME, "netjack2_driver_receive"); if ((str = pw_properties_get(props, "sink.props")) != NULL) diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-source.c b/src/modules/module-protocol-pulse/modules/module-pipe-source.c index c9c31f106..ff44f3c38 100644 --- a/src/modules/module-protocol-pulse/modules/module-pipe-source.c +++ b/src/modules/module-protocol-pulse/modules/module-pipe-source.c @@ -172,6 +172,8 @@ static int module_pipe_source_prepare(struct module * const module) pw_properties_set(stream_props, PW_KEY_NODE_DRIVER, "true"); if ((str = pw_properties_get(stream_props, PW_KEY_PRIORITY_DRIVER)) == NULL) pw_properties_set(stream_props, PW_KEY_PRIORITY_DRIVER, "50000"); + if ((str = pw_properties_get(stream_props, PW_KEY_PRIORITY_SESSION)) == NULL) + pw_properties_set(stream_props, PW_KEY_PRIORITY_SESSION, "2000"); d->module = module; d->stream_props = stream_props; From 0e80287625661f09f8e99da0171efa65a0293e18 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 17 Feb 2026 13:02:03 +0100 Subject: [PATCH 65/93] RAOP: also support link-local addresses for IPv4 Patch by Lairton Lelis da Fonseca Junior (@lairton) Remove the hard skip for IPv4 link-local addresses and add an interface binding (matching the existing IPv6 link-local behavior). The host needs a link-local address on the interface (ip addr add 169.254.x.x/16 dev wlan0 or via NetworkManager +ipv4.addresses). Fixes #4830 --- src/modules/module-raop-discover.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c index 53032d0be..436972ac0 100644 --- a/src/modules/module-raop-discover.c +++ b/src/modules/module-raop-discover.c @@ -384,10 +384,8 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr } avahi_address_snprint(at, sizeof(at), a); - if (spa_strstartswith(at, link_local_range)) { - pw_log_info("found link-local ip address %s - skipping tunnel creation", at); - goto done; - } + if (spa_strstartswith(at, link_local_range)) + pw_log_info("found link-local ip address %s for '%s'", at, name); tinfo = TUNNEL_INFO(.name = name); @@ -414,6 +412,11 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr (a->data.ipv6.address[1] & 0xc0) == 0x80) snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); + /* For IPv4 link-local, bind to the discovery interface */ + if (a->proto == AVAHI_PROTO_INET && + spa_strstartswith(at, link_local_range)) + snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); + pw_properties_setf(props, "raop.ip", "%s%s", at, if_suffix); pw_properties_setf(props, "raop.ifindex", "%d", interface); pw_properties_setf(props, "raop.port", "%u", port); From 7956d7ceaf9026f304a1c10a3c0c2b49f29a7674 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 17 Feb 2026 13:08:56 +0100 Subject: [PATCH 66/93] snapcast: support IPv4 link-local addresses --- src/modules/module-snapcast-discover.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/module-snapcast-discover.c b/src/modules/module-snapcast-discover.c index 3568d82d4..e929be9c0 100644 --- a/src/modules/module-snapcast-discover.c +++ b/src/modules/module-snapcast-discover.c @@ -640,10 +640,9 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr } avahi_address_snprint(at, sizeof(at), a); - if (spa_strstartswith(at, link_local_range)) { - pw_log_info("found link-local ip address %s - skipping tunnel creation", at); - goto done; - } + if (spa_strstartswith(at, link_local_range)) + pw_log_info("found link-local ip address %s for '%s'", at, name); + pw_log_info("%s %s", name, at); tinfo = TUNNEL_INFO(.name = name, .port = port); @@ -671,6 +670,11 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr (a->data.ipv6.address[1] & 0xc0) == 0x80) snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); + /* For IPv4 link-local, bind to the discovery interface */ + if (a->proto == AVAHI_PROTO_INET && + spa_strstartswith(at, link_local_range)) + snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); + pw_properties_setf(props, "snapcast.ip", "%s%s", at, if_suffix); pw_properties_setf(props, "snapcast.ifindex", "%d", interface); pw_properties_setf(props, "snapcast.port", "%u", port); From 88cbe2420169185b0489cc38b43116e107395822 Mon Sep 17 00:00:00 2001 From: Damien Espitallier Date: Tue, 17 Feb 2026 19:19:52 +0100 Subject: [PATCH 67/93] alsa-udev: Allow ACTION_REMOVE on ignored cards Move the card->ignored check to only apply to ACTION_CHANGE, not ACTION_REMOVE. This ensures that ignored cards can still be properly removed when they are unplugged. --- spa/plugins/alsa/alsa-udev.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c index 7d1ae9a3a..8537c9760 100644 --- a/spa/plugins/alsa/alsa-udev.c +++ b/spa/plugins/alsa/alsa-udev.c @@ -725,11 +725,11 @@ static bool check_access(struct impl *this, struct card *card) static void process_card(struct impl *this, enum action action, struct card *card) { - if (card->ignored) - return; - switch (action) { case ACTION_CHANGE: { + if (card->ignored) + return; + check_access(this, card); if (card->accessible && !card->emitted) { int res = emit_added_object_info(this, card); From b9922d8ed59897f482527f1c1847ce8411c49fad Mon Sep 17 00:00:00 2001 From: Misha Baranov Date: Mon, 16 Feb 2026 22:35:14 +0100 Subject: [PATCH 68/93] module-roc: forward roc-toolkit logs to pipewire logs Roc-toolkit log records are captured via a callback and written to PipeWire log with corresponding verbosity level. The log.level config parameter limits record verbosity at the roc-toolkit level. --- src/modules/meson.build | 4 +-- src/modules/module-roc-sink.c | 23 ++++++++++-- src/modules/module-roc-source.c | 22 ++++++++++-- src/modules/module-roc/common.c | 21 +++++++++++ src/modules/module-roc/common.h | 63 +++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/modules/module-roc/common.c diff --git a/src/modules/meson.build b/src/modules/meson.build index 6b215f3a0..59f46ae13 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -693,7 +693,7 @@ pipewire_module_vban_recv = shared_library('pipewire-module-vban-recv', build_module_roc = roc_dep.found() if build_module_roc pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink', - [ 'module-roc-sink.c' ], + [ 'module-roc-sink.c', 'module-roc/common.c'], include_directories : [configinc], install : true, install_dir : modules_install_dir, @@ -702,7 +702,7 @@ if build_module_roc ) pipewire_module_roc_source = shared_library('pipewire-module-roc-source', - [ 'module-roc-source.c' ], + [ 'module-roc-source.c', 'module-roc/common.c' ], include_directories : [configinc], install : true, install_dir : modules_install_dir, diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c index 1cca69592..39ca2bce1 100644 --- a/src/modules/module-roc-sink.c +++ b/src/modules/module-roc-sink.c @@ -46,6 +46,9 @@ * - `remote.repair.port = `: remote receiver TCP/UDP port for receiver packets * - `remote.control.port = `: remote receiver TCP/UDP port for control packets * - `fec.code = `: Possible values: `disable`, `rs8m`, `ldpc` + * - `log.level = `: log level for roc-toolkit. Possible values: `DEFAULT`, + * `NONE`, `ERROR`, `INFO`, `DEBUG`, `TRACE`; `DEFAULT` follows the log + * level of the PipeWire context. * * ## General options * @@ -75,6 +78,7 @@ * node.name = "roc-sink" * } * audio.position = [ FL FR ] + * log.level = DEFAULT * } * } *] @@ -84,8 +88,9 @@ #define NAME "roc-sink" -PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +PW_LOG_TOPIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +PW_LOG_TOPIC_EXTERN(roc_log_topic); struct module_roc_sink_data { struct pw_impl_module *module; @@ -115,6 +120,8 @@ struct module_roc_sink_data { roc_endpoint *remote_control_addr; int remote_control_port; + + roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -300,6 +307,8 @@ static int roc_sink_setup(struct module_roc_sink_data *data) pw_properties_setf(data->capture_props, PW_KEY_NODE_RATE, "1/%d", info.rate); + pw_roc_log_init(); + res = roc_sender_open(data->context, &sender_config, &data->sender); if (res) { pw_log_error("failed to create roc sender: %d", res); @@ -382,7 +391,8 @@ static const struct spa_dict_item module_roc_sink_info[] = { "( remote.repair.port= ) " "( remote.control.port= ) " "( audio.position= ) " - "( sink.props= { key=val ... } ) " }, + "( sink.props= { key=val ... } ) " + "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -396,6 +406,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) int res = 0; PW_LOG_TOPIC_INIT(mod_topic); + PW_LOG_TOPIC_INIT(roc_log_topic); data = calloc(1, sizeof(struct module_roc_sink_data)); if (data == NULL) @@ -502,6 +513,14 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_log_error("can't connect: %m"); goto out; } + if ((str = pw_properties_get(props, "log.level")) != NULL) { + const struct spa_log *log_conf = pw_log_get(); + const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); + if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { + pw_log_error("Invalid log level %s, using default", str); + data->loglevel = default_level; + } + } pw_proxy_add_listener((struct pw_proxy*)data->core, &data->core_proxy_listener, diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c index 6c67a037c..2173c6af1 100644 --- a/src/modules/module-roc-source.c +++ b/src/modules/module-roc-source.c @@ -56,6 +56,9 @@ * - `fec.code = `: Possible values: `default`, `disable`, `rs8m`, `ldpc` * * - `resampler.profile = `: Deprecated, use roc.resampler.profile + * - `log.level = `: log level for roc-toolkit. Possible values: `DEFAULT`, + * `NONE`, `ERROR`, `INFO`, `DEBUG`, `TRACE`; `DEFAULT` follows the log + * level of the PipeWire context. * * ## General options * @@ -89,6 +92,7 @@ * node.name = "roc-source" * } * audio.position = [ FL FR ] + * log.level = DEFAULT * } * } *] @@ -98,8 +102,9 @@ #define NAME "roc-source" -PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +PW_LOG_TOPIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +PW_LOG_TOPIC_EXTERN(roc_log_topic); struct module_roc_source_data { struct pw_impl_module *module; @@ -135,6 +140,8 @@ struct module_roc_source_data { roc_endpoint *local_control_addr; int local_control_port; + + roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -333,6 +340,8 @@ static int roc_source_setup(struct module_roc_source_data *data) */ receiver_config.target_latency = (unsigned long long)data->sess_latency_msec * SPA_NSEC_PER_MSEC; + pw_roc_log_init(); + res = roc_receiver_open(data->context, &receiver_config, &data->receiver); if (res) { pw_log_error("failed to create roc receiver: %d", res); @@ -421,7 +430,8 @@ static const struct spa_dict_item module_roc_source_info[] = { "( local.repair.port= ) " "( local.control.port= ) " "( audio.position= ) " - "( source.props= { key=value ... } ) " }, + "( source.props= { key=value ... } ) " + "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -557,6 +567,14 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } else { data->fec_code = ROC_FEC_ENCODING_DEFAULT; } + if ((str = pw_properties_get(props, "log.level")) != NULL) { + const struct spa_log *log_conf = pw_log_get(); + const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); + if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { + pw_log_error("Invalid log level %s, using default", str); + data->loglevel = default_level; + } + } data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core); if (data->core == NULL) { diff --git a/src/modules/module-roc/common.c b/src/modules/module-roc/common.c new file mode 100644 index 000000000..244c203dd --- /dev/null +++ b/src/modules/module-roc/common.c @@ -0,0 +1,21 @@ +#include +#include + +#include "common.h" + +PW_LOG_TOPIC(roc_log_topic, "mod.roc.lib"); + +void pw_roc_log_init(void) +{ + roc_log_set_handler(pw_roc_log_handler, NULL); + roc_log_set_level(pw_roc_log_level_pw_2_roc(roc_log_topic->has_custom_level ? roc_log_topic->level : pw_log_level)); +} + +void pw_roc_log_handler(const roc_log_message *message, void *argument) +{ + const enum spa_log_level log_level = pw_roc_log_level_roc_2_pw(message->level); + if (SPA_UNLIKELY(pw_log_topic_enabled(log_level, roc_log_topic))) { + pw_log_logt(log_level, roc_log_topic, message->file, message->line, message->module, message->text, ""); + } +} + diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h index 69153659e..c94ac69a8 100644 --- a/src/modules/module-roc/common.h +++ b/src/modules/module-roc/common.h @@ -3,8 +3,10 @@ #include #include +#include #include +#include #define PW_ROC_DEFAULT_IP "0.0.0.0" #define PW_ROC_DEFAULT_SOURCE_PORT 10001 @@ -18,6 +20,9 @@ #define PW_ROC_MULTITRACK_ENCODING_ID 100 #define PW_ROC_STEREO_POSITIONS "[ FL FR ]" +void pw_roc_log_init(void); +void pw_roc_log_handler(const roc_log_message *message, void *argument); + static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str) { if (!str || !*str || spa_streq(str, "default")) @@ -132,4 +137,62 @@ static inline void pw_roc_fec_encoding_to_proto(roc_fec_encoding fec_code, roc_p } } +static inline roc_log_level pw_roc_log_level_pw_2_roc(const enum spa_log_level pw_log_level) +{ + switch (pw_log_level) { + case SPA_LOG_LEVEL_NONE: + return ROC_LOG_NONE; + case SPA_LOG_LEVEL_ERROR: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_WARN: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_INFO: + return ROC_LOG_INFO; + case SPA_LOG_LEVEL_DEBUG: + return ROC_LOG_DEBUG; + case SPA_LOG_LEVEL_TRACE: + return ROC_LOG_TRACE; + default: + return ROC_LOG_NONE; + } +} + +static inline enum spa_log_level pw_roc_log_level_roc_2_pw(const roc_log_level roc_log_level) +{ + switch (roc_log_level) { + case ROC_LOG_NONE: + return SPA_LOG_LEVEL_NONE; + case ROC_LOG_ERROR: + return SPA_LOG_LEVEL_ERROR; + case ROC_LOG_INFO: + return SPA_LOG_LEVEL_INFO; + case ROC_LOG_DEBUG: + return SPA_LOG_LEVEL_DEBUG; + case ROC_LOG_TRACE: + return SPA_LOG_LEVEL_TRACE; + default: + return SPA_LOG_LEVEL_NONE; + } +} + +static inline int pw_roc_parse_log_level(roc_log_level *loglevel, const char *str, + roc_log_level default_level) +{ + if (spa_streq(str, "DEFAULT")) + *loglevel = default_level; + else if (spa_streq(str, "NONE")) + *loglevel = ROC_LOG_NONE; + else if (spa_streq(str, "ERROR")) + *loglevel = ROC_LOG_ERROR; + else if (spa_streq(str, "INFO")) + *loglevel = ROC_LOG_INFO; + else if (spa_streq(str, "DEBUG")) + *loglevel = ROC_LOG_DEBUG; + else if (spa_streq(str, "TRACE")) + *loglevel = ROC_LOG_TRACE; + else + return -EINVAL; + return 0; +} + #endif /* MODULE_ROC_COMMON_H */ From 700cea78dbe7564131d51b21a7795e2567ee048a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 27 Jan 2026 14:10:44 +0100 Subject: [PATCH 69/93] 1.6.0 --- NEWS | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++-- meson.build | 2 +- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 3acc6e6be..1f0a39a28 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,102 @@ +# PipeWire 1.6.0 (2026-02-19) + +This is the 1.6 release that is API and ABI compatible with previous +1.4.x releases. + +This release contains some of the bigger changes that happened since +the 1.4 release last year, including: + + * An LDAC decoder was added for bluetooth. + * SpanDSP for bluetooth packet loss concealment. + * Safe parsing and building of PODs in shared memory. + * Added support for metadata features. This is used to signal that + the sync_timeline metadata supports the RELEASE operation. + * Node commands and events can contain extra user data. + * Support for more compressed format helper functions to create + and parse formats. + * Support for compile time max channels. The max channels was + increased to 128. + * Support for audio channel layouts was added. This makes it possible + to set "audio.layout" = "5.1" instead of the more verbose + audio.position = [ FL, FR, FC, LFE, SL, SR ] + * Support for Capability Params was added. This can be used to + negotiate capabilities on a link before format and buffer + negotiation takes place. + * More HDR colortypes are added. + * Loops now have locking with priority inversion. Most code was adapted + to use the faster locks instead of epoll/eventfd to update shared state. + * Channel position are parsed from EDID data. + * Channel maps are now set on ALSA. + * The resampler now supports configurable window functions such + as blackman and kaiser windows. The phases are now also calculated + with fixed point math, which makes it more accurate. + * Many bluetooth updates and improvements. + * The filter-graph has an ffmpeg and ONNX plugin. The ffmpeg plugin + can run an audio AVFilterGraph. The ONNX plugin can run some models + such as the silero VAD. + * Many AVB updates. Work is ongoing to merge the Milan protocol. + * Support for v0 clients was removed. + * The jack-tunnel module can now autoconnect ports. + * ROC support multitrack layouts now. + * Many RTP updates. + * rlimits can now be set in the config file. + * Thread reset on fork can now be configured. JACK clients expect this + to be disabled. + * node.exclusive is now enforced. + * node.reliable enables reliable transport. + * pw-cat supports sysex and midiclip as well as some more uncompressed + formats. Options were added to set the container and codec formats + as well as list the supported containers, codecs, layouts and channel + names. + * Documentation updates. + + +## Highlights (since the previous 1.5.85 prerelease) + - Fix a 64 channel limit in the channel mixer. + - Fix an fd leak in pulse-server in some error cases. + - Some small fixes and improvements. + + +## PipeWire + - Fix Capability leaks. + - Return an error in pw-stream get-time when not STREAMING. + - Set the current time in the driver position before starting. + Some followers might look at it. + +## Modules + - Improve default channel handling in module-filter-chain. + - Support source and sink only module-filter-chain. + - Tweak the filter-chain spatializer example gains. + - Handle new snapcast service type. (#5104) + - Implement socket activation without depending on libsystemd. + - Support ipv4 link-local addresses in RAOP and snapcast. (#4830) + - Forward ROC-toolkit logs to pipewire. + +## SPA + - Improve default channel handling in filter-graph. (#5084) + - Clamp control values to min/max. (#5088) + - Support mode JBL gaming headsets. + - Handle some SOFA errors and add gain option. + - Really handle more than 64 channels in the channelmixer. (#5118) + - Allow removal in ALSA-udev of ignored cards. + +# pulse-server + - Fix mono mixdown query. + - Expose headset autoswitch message. + - Handle EPROTO errors by disconnecting. + - Handle timeouts in play-sample streams. (#5099) + +## GStreamer + - Fix crop metadata. + - Fix a race in the buffer release function. + +## Tools + - Improve format support and detection in pw-cat. + - Add some more options to pw-cat to list supported containers + and formats. (#5117) + +Older versions: + # PipeWire 1.5.85 (2026-01-19) This is the fifth and hopefully last 1.6 release candidate that @@ -57,9 +156,6 @@ releases. ## Docs - Document the resampler properties better. - -Older versions: - # PipeWire 1.5.84 (2025-11-27) This is the fourth 1.6 release candidate that is API and ABI diff --git a/meson.build b/meson.build index 2e308cfa5..c954a644c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.5.85', + version : '1.6.0', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', From b8b2c58cdab4fdfa9e61d5b5693a17c90b3fec75 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 19 Feb 2026 11:09:50 +0100 Subject: [PATCH 70/93] Development continues as 1.7.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index c954a644c..5f9ffa39d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.6.0', + version : '1.7.0', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', From 6eb44830697dc90dacfedc7b30ed026d2d600889 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 28 Jan 2026 13:20:38 +0100 Subject: [PATCH 71/93] pulse-server: add client props to sink_input/source_output Instead of adding the client props to the stream props when we create it, add them when we enumerate the streams. This makes it possible to also have the client props in the stream props for streams that are not created with the pulse API. Fixes #5090 --- .../module-protocol-pulse/pulse-server.c | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 59610ef57..58dbb221a 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -1621,7 +1621,7 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui struct pw_manager_object *o; bool is_monitor; - props = pw_properties_copy(client->props); + props = pw_properties_new(NULL, NULL); if (props == NULL) goto error_errno; @@ -1907,7 +1907,7 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint struct pw_manager_object *o; bool is_monitor = false; - props = pw_properties_copy(client->props); + props = pw_properties_new(NULL, NULL); if (props == NULL) goto error_errno; @@ -2298,7 +2298,7 @@ static int do_create_upload_stream(struct client *client, uint32_t command, uint struct message *reply; int res; - if ((props = pw_properties_copy(client->props)) == NULL) + if ((props = pw_properties_new(NULL, NULL)) == NULL) goto error_errno; if ((res = message_get(m, @@ -4068,6 +4068,25 @@ static const char *get_media_name(struct pw_node_info *info) return media_name; } +static int fill_node_info_proplist(struct message *m, const struct spa_dict *node_props, + const struct pw_manager_object *client) +{ + struct pw_client_info *client_info = client ? client->info : NULL; + spa_autoptr(pw_properties) props = NULL; + + if (client_info && client_info->props) { + props = pw_properties_new_dict(node_props); + if (props == NULL) + return -ENOMEM; + + pw_properties_add(props, client_info->props); + + node_props = &props->dict; + } + message_put(m, TAG_PROPLIST, node_props, TAG_INVALID); + return 0; +} + static int fill_sink_input_info(struct client *client, struct message *m, struct pw_manager_object *o) { @@ -4128,10 +4147,16 @@ static int fill_sink_input_info(struct client *client, struct message *m, message_put(m, TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */ TAG_INVALID); - if (client->version >= 13) - message_put(m, - TAG_PROPLIST, info->props, - TAG_INVALID); + if (client->version >= 13) { + int res; + struct pw_manager_object *c = NULL; + if (client_id != SPA_ID_INVALID) { + struct selector sel = { .id = client_id, .type = pw_manager_object_is_client, }; + c = select_object(manager, &sel); + } + if ((res = fill_node_info_proplist(m, info->props, c)) < 0) + return res; + } if (client->version >= 19) message_put(m, TAG_BOOLEAN, corked, /* corked */ @@ -4207,10 +4232,16 @@ static int fill_source_output_info(struct client *client, struct message *m, TAG_STRING, "PipeWire", /* resample method */ TAG_STRING, "PipeWire", /* driver */ TAG_INVALID); - if (client->version >= 13) - message_put(m, - TAG_PROPLIST, info->props, - TAG_INVALID); + if (client->version >= 13) { + int res; + struct pw_manager_object *c = NULL; + if (client_id != SPA_ID_INVALID) { + struct selector sel = { .id = client_id, .type = pw_manager_object_is_client, }; + c = select_object(manager, &sel); + } + if ((res = fill_node_info_proplist(m, info->props, c)) < 0) + return res; + } if (client->version >= 19) message_put(m, TAG_BOOLEAN, corked, /* corked */ From be0e037809de61cd9e9707310aafebcf44b6be10 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 19 Feb 2026 13:03:50 +0100 Subject: [PATCH 72/93] pulse-server: avoid doing allocations and string copies We can just concatenate the stream and client dict on the stack, reusing all the strings without having to do allocations or copies. Also filter out the object.id and object.serial from the client, we want to keep the ones from the streams. --- .../module-protocol-pulse/pulse-server.c | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 58dbb221a..24251942a 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -4072,18 +4072,38 @@ static int fill_node_info_proplist(struct message *m, const struct spa_dict *nod const struct pw_manager_object *client) { struct pw_client_info *client_info = client ? client->info : NULL; - spa_autoptr(pw_properties) props = NULL; + uint32_t n_items, n; + struct spa_dict dict, *client_props = NULL; + const struct spa_dict_item *it; + struct spa_dict_item *items, *it2; + n_items = node_props->n_items; if (client_info && client_info->props) { - props = pw_properties_new_dict(node_props); - if (props == NULL) - return -ENOMEM; - - pw_properties_add(props, client_info->props); - - node_props = &props->dict; + client_props = client_info->props; + n_items += client_props->n_items; } - message_put(m, TAG_PROPLIST, node_props, TAG_INVALID); + + dict.n_items = n = 0; + dict.items = items = alloca(n_items * sizeof(struct spa_dict_item)); + + spa_dict_for_each(it, node_props) + items[n++] = *it; + dict.n_items = n; + + if (client_props) { + spa_dict_for_each(it, client_props) { + if (spa_streq(it->key, PW_KEY_OBJECT_ID) || + spa_streq(it->key, PW_KEY_OBJECT_SERIAL)) + continue; + + if ((it2 = (struct spa_dict_item*)spa_dict_lookup_item(&dict, it->key))) + it2->value = it->value; + else + items[n++] = *it; + } + dict.n_items = n; + } + message_put(m, TAG_PROPLIST, &dict, TAG_INVALID); return 0; } From e7ca02c4d820eedb50a78b4b73af1b807b21d012 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 10 Dec 2025 11:35:27 +0100 Subject: [PATCH 73/93] filter-graph: sync control updates with data thread. don't read the control ports from the processing thread and check for updates. Use the control_changed signal to check and update the parameters of the biquad atimically. See #5019 --- spa/plugins/filter-graph/plugin_builtin.c | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index 3bcde30c9..c8597cf10 100644 --- a/spa/plugins/filter-graph/plugin_builtin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,7 @@ struct plugin { struct spa_fga_dsp *dsp; struct spa_log *log; + struct spa_loop *data_loop; }; struct builtin { @@ -542,7 +544,14 @@ static void bq_run(void *Instance, unsigned long samples) struct biquad *bq = &impl->bq; float *out = impl->port[0]; float *in = impl->port[1]; + spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); +} +static int +do_bq_control_changed(struct spa_loop *loop, bool async, uint32_t seq, const void *data, + size_t size, void *user_data) +{ + struct builtin *impl = user_data; if (impl->type == BQ_NONE) { float b0, b1, b2, a0, a1, a2; b0 = impl->port[5][0]; @@ -562,7 +571,13 @@ static void bq_run(void *Instance, unsigned long samples) if (impl->freq != freq || impl->Q != Q || impl->gain != gain) bq_freq_update(impl, impl->type, freq, Q, gain); } - spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); + return 0; +} + +static void bq_control_changed(void * Instance) +{ + struct builtin *impl = Instance; + spa_loop_locked(impl->plugin->data_loop, do_bq_control_changed, 1, NULL, 0, impl); } /** bq_lowpass */ @@ -574,6 +589,7 @@ static const struct spa_fga_descriptor bq_lowpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -588,6 +604,7 @@ static const struct spa_fga_descriptor bq_highpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -602,6 +619,7 @@ static const struct spa_fga_descriptor bq_bandpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -616,6 +634,7 @@ static const struct spa_fga_descriptor bq_lowshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -630,6 +649,7 @@ static const struct spa_fga_descriptor bq_highshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -644,6 +664,7 @@ static const struct spa_fga_descriptor bq_peaking_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -658,6 +679,7 @@ static const struct spa_fga_descriptor bq_notch_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -673,6 +695,7 @@ static const struct spa_fga_descriptor bq_allpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -687,6 +710,7 @@ static const struct spa_fga_descriptor bq_raw_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_changed = bq_control_changed, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -3387,6 +3411,7 @@ impl_init(const struct spa_handle_factory *factory, impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP); + impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); for (uint32_t i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; From 7887c365d1aac086ebfb15112e9d437f44d498cc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 10 Dec 2025 12:15:38 +0100 Subject: [PATCH 74/93] filter-graph: Make a new control_sync function This function is run for all the nodes with the data loop locked. It can be used to atomically update multiple node controls. We can't use the control_changed function because this one runs without the lock and might do slow things, like what the sofa plugin currently does. See #5019 --- spa/plugins/filter-graph/audio-plugin.h | 1 + spa/plugins/filter-graph/filter-graph.c | 56 ++++++++++++++++------- spa/plugins/filter-graph/plugin_builtin.c | 33 +++++-------- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/spa/plugins/filter-graph/audio-plugin.h b/spa/plugins/filter-graph/audio-plugin.h index 9f9d1bce6..fffdc0de5 100644 --- a/spa/plugins/filter-graph/audio-plugin.h +++ b/spa/plugins/filter-graph/audio-plugin.h @@ -69,6 +69,7 @@ struct spa_fga_descriptor { void (*connect_port) (void *instance, unsigned long port, void *data); void (*control_changed) (void *instance); + void (*control_sync) (void *instance); void (*activate) (void *instance); void (*deactivate) (void *instance); diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index 52609f2c6..0e4ca98ce 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -221,6 +222,7 @@ struct impl { struct spa_cpu *cpu; struct spa_fga_dsp *dsp; struct spa_plugin_loader *loader; + struct spa_loop *data_loop; uint64_t info_all; struct spa_filter_graph_info info; @@ -698,21 +700,46 @@ static int impl_reset(void *object) return 0; } -static void node_control_changed(struct node *node) +static int +do_emit_node_control_sync(struct spa_loop *loop, bool async, uint32_t seq, const void *data, + size_t size, void *user_data) { - const struct spa_fga_descriptor *d = node->desc->desc; + struct impl *impl = user_data; + struct graph *graph = &impl->graph; + struct node *node; + uint32_t i; + spa_list_for_each(node, &graph->node_list, link) { + const struct spa_fga_descriptor *d = node->desc->desc; + if (!node->control_changed || d->control_sync == NULL) + continue; + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] != NULL) + d->control_sync(node->hndl[i]); + } + } + return 0; +} + +static void emit_node_control_changed(struct impl *impl) +{ + struct graph *graph = &impl->graph; + struct node *node; uint32_t i; - if (!node->control_changed) - return; + spa_loop_locked(impl->data_loop, do_emit_node_control_sync, 1, NULL, 0, impl); - for (i = 0; i < node->n_hndl; i++) { - if (node->hndl[i] == NULL) + spa_list_for_each(node, &graph->node_list, link) { + const struct spa_fga_descriptor *d = node->desc->desc; + if (!node->control_changed) continue; - if (d->control_changed) - d->control_changed(node->hndl[i]); + if (d->control_changed != NULL) { + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] != NULL) + d->control_changed(node->hndl[i]); + } + } + node->control_changed = false; } - node->control_changed = false; } static int sync_volume(struct graph *graph, struct volume *vol) @@ -826,11 +853,7 @@ static int impl_set_props(void *object, enum spa_direction direction, const stru spa_pod_dynamic_builder_clean(&b); if (changed > 0) { - struct node *node; - - spa_list_for_each(node, &graph->node_list, link) - node_control_changed(node); - + emit_node_control_changed(impl); spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT); } return 0; @@ -1695,10 +1718,10 @@ static int impl_activate(void *object, const struct spa_dict *props) for (i = 0; i < node->n_hndl; i++) { if (d->activate) d->activate(node->hndl[i]); - if (node->control_changed && d->control_changed) - d->control_changed(node->hndl[i]); } } + emit_node_control_changed(impl); + /* calculate latency */ sort_reset(graph); while ((node = sort_next_node(graph)) != NULL) { @@ -2346,6 +2369,7 @@ impl_init(const struct spa_handle_factory *factory, spa_log_topic_init(impl->log, &log_topic); impl->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); impl->max_align = spa_cpu_get_max_align(impl->cpu); impl->dsp = spa_fga_dsp_new(impl->cpu ? spa_cpu_get_flags(impl->cpu) : 0); diff --git a/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index c8597cf10..a16e4a7fc 100644 --- a/spa/plugins/filter-graph/plugin_builtin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -39,7 +39,6 @@ struct plugin { struct spa_fga_dsp *dsp; struct spa_log *log; - struct spa_loop *data_loop; }; struct builtin { @@ -547,11 +546,9 @@ static void bq_run(void *Instance, unsigned long samples) spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); } -static int -do_bq_control_changed(struct spa_loop *loop, bool async, uint32_t seq, const void *data, - size_t size, void *user_data) +static void bq_control_sync(void * Instance) { - struct builtin *impl = user_data; + struct builtin *impl = Instance; if (impl->type == BQ_NONE) { float b0, b1, b2, a0, a1, a2; b0 = impl->port[5][0]; @@ -571,13 +568,6 @@ do_bq_control_changed(struct spa_loop *loop, bool async, uint32_t seq, const voi if (impl->freq != freq || impl->Q != Q || impl->gain != gain) bq_freq_update(impl, impl->type, freq, Q, gain); } - return 0; -} - -static void bq_control_changed(void * Instance) -{ - struct builtin *impl = Instance; - spa_loop_locked(impl->plugin->data_loop, do_bq_control_changed, 1, NULL, 0, impl); } /** bq_lowpass */ @@ -589,7 +579,7 @@ static const struct spa_fga_descriptor bq_lowpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -604,7 +594,7 @@ static const struct spa_fga_descriptor bq_highpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -619,7 +609,7 @@ static const struct spa_fga_descriptor bq_bandpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -634,7 +624,7 @@ static const struct spa_fga_descriptor bq_lowshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -649,7 +639,7 @@ static const struct spa_fga_descriptor bq_highshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -664,7 +654,7 @@ static const struct spa_fga_descriptor bq_peaking_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -679,7 +669,7 @@ static const struct spa_fga_descriptor bq_notch_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -695,7 +685,7 @@ static const struct spa_fga_descriptor bq_allpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -710,7 +700,7 @@ static const struct spa_fga_descriptor bq_raw_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, - .control_changed = bq_control_changed, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -3411,7 +3401,6 @@ impl_init(const struct spa_handle_factory *factory, impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP); - impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); for (uint32_t i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; From 2fb38af3e0464fcb2656ec40c16935f541570afc Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Thu, 19 Feb 2026 14:25:03 +0100 Subject: [PATCH 75/93] modules: move the scheduler to a module Mostly because we can but also because there are more ways of doing the scheduling and this opens the door for some experimentation. --- src/daemon/minimal.conf.in | 4 + src/daemon/pipewire.conf.in | 4 + src/modules/meson.build | 10 + src/modules/module-scheduler-v1.c | 1009 +++++++++++++++++++++++++++++ src/pipewire/context.c | 847 +----------------------- src/pipewire/context.h | 5 +- src/pipewire/impl-link.c | 1 + src/pipewire/private.h | 3 + 8 files changed, 1041 insertions(+), 842 deletions(-) create mode 100644 src/modules/module-scheduler-v1.c diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in index 82647e9ca..6def01bcf 100644 --- a/src/daemon/minimal.conf.in +++ b/src/daemon/minimal.conf.in @@ -100,6 +100,10 @@ context.modules = [ } flags = [ ifexists nofail ] } + # the graph scheduler + { name = libpipewire-module-scheduler-v1 + condition = [ { module.scheduler-v1 = !false } ] + } # The native communication protocol. { name = libpipewire-module-protocol-native } diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in index c3eb7120f..a9142cede 100644 --- a/src/daemon/pipewire.conf.in +++ b/src/daemon/pipewire.conf.in @@ -121,6 +121,10 @@ context.modules = [ flags = [ ifexists nofail ] condition = [ { module.rt = !false } ] } + # the graph scheduler + { name = libpipewire-module-scheduler-v1 + condition = [ { module.scheduler-v1 = !false } ] + } # The native communication protocol. { name = libpipewire-module-protocol-native diff --git a/src/modules/meson.build b/src/modules/meson.build index 59f46ae13..8636286e6 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -46,6 +46,7 @@ module_sources = [ 'module-vban-recv.c', 'module-vban-send.c', 'module-session-manager.c', + 'module-scheduler-v1.c', 'module-zeroconf-discover.c', 'module-roc-source.c', 'module-roc-sink.c', @@ -532,6 +533,15 @@ pipewire_module_adapter = shared_library('pipewire-module-adapter', dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], ) +pipewire_module_scheduler_v1 = shared_library('pipewire-module-scheduler-v1', + [ 'module-scheduler-v1.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], +) + pipewire_module_session_manager = shared_library('pipewire-module-session-manager', [ 'module-session-manager.c', 'module-session-manager/client-endpoint/client-endpoint.c', diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c new file mode 100644 index 000000000..22965b257 --- /dev/null +++ b/src/modules/module-scheduler-v1.c @@ -0,0 +1,1009 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_SYS_VFS_H +#include +#endif +#ifdef HAVE_SYS_MOUNT_H +#include +#endif + +#include +#include +#include +#include + +#include +#include + +/** \page page_module_scheduler_v1 SchedulerV1 + * + * + * ## Module Name + * + * `libpipewire-module-scheduler-v1` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * ## General options + * + * Options with well-known behavior: + * + * ## Config override + * + * A `module.scheduler-v1.args` config section can be added + * to override the module arguments. + * + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-scheduler-v1-args.conf + * + * module.scheduler-v1.args = { + * } + *\endcode + * + * ## Example configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-scheduler-v1 + * args = { + * } + * } + *] + *\endcode + * + * Since: 1.7.0 + */ + +#define NAME "scheduler-v1" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define MODULE_USAGE "" + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Implement the Scheduler V1" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +#define MAX_HOPS 64 +#define MAX_SYNC 4u + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct spa_hook context_listener; + struct spa_hook module_listener; +}; + +static int ensure_state(struct pw_impl_node *node, bool running) +{ + enum pw_node_state state = node->info.state; + if (node->active && node->runnable && + !SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_NEED_CONFIGURE) && running) + state = PW_NODE_STATE_RUNNING; + else if (state > PW_NODE_STATE_IDLE) + state = PW_NODE_STATE_IDLE; + return pw_impl_node_set_state(node, state); +} + +/* From a node (that is runnable) follow all prepared links in the given direction + * and groups to active nodes and make them recursively runnable as well. + */ +static inline int run_nodes(struct pw_context *context, struct pw_impl_node *node, + struct spa_list *nodes, enum pw_direction direction, int hop) +{ + struct pw_impl_node *t; + struct pw_impl_port *p; + struct pw_impl_link *l; + + if (hop == MAX_HOPS) { + pw_log_warn("exceeded hops (%d)", hop); + return -EIO; + } + + pw_log_debug("node %p: '%s' direction:%s", node, node->name, + pw_direction_as_string(direction)); + + SPA_FLAG_SET(node->checked, 1u<input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + t = l->output->node; + + if (!t->active || !l->prepared || + (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) + continue; + + pw_log_debug(" peer %p: '%s'", t, t->name); + t->runnable = true; + run_nodes(context, t, nodes, direction, hop + 1); + } + } + } else { + spa_list_for_each(p, &node->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + t = l->input->node; + + if (!t->active || !l->prepared || + (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) + continue; + + pw_log_debug(" peer %p: '%s'", t, t->name); + t->runnable = true; + run_nodes(context, t, nodes, direction, hop + 1); + } + } + } + /* now go through all the nodes that have the same link group and + * that are not yet visited. Note how nodes with the same group + * don't get included here. They were added to the same driver but + * need to otherwise stay idle unless some non-passive link activates + * them. */ + if (node->link_groups != NULL) { + spa_list_for_each(t, nodes, sort_link) { + if (t->exported || !t->active || + SPA_FLAG_IS_SET(t->checked, 1u<link_groups, node->link_groups) < 0) + continue; + + pw_log_debug(" group %p: '%s'", t, t->name); + t->runnable = true; + if (!t->driving) + run_nodes(context, t, nodes, direction, hop + 1); + } + } + return 0; +} + +/* Follow all prepared links and groups from node, activate the links. + * If a non-passive link is found, we set the peer runnable flag. + * + * After this is done, we end up with a list of nodes in collect that are all + * linked to node. + * Some of the nodes have the runnable flag set. We then start from those nodes + * and make all linked nodes and groups runnable as well. (see run_nodes). + * + * This ensures that we only activate the paths from the runnable nodes to the + * driver nodes and leave the other nodes idle. + */ +static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect) +{ + struct spa_list queue; + struct pw_impl_node *n, *t; + struct pw_impl_port *p; + struct pw_impl_link *l; + uint32_t n_sync; + char *sync[MAX_SYNC+1]; + + pw_log_debug("node %p: '%s'", node, node->name); + + /* start with node in the queue */ + spa_list_init(&queue); + spa_list_append(&queue, &node->sort_link); + node->visited = true; + + n_sync = 0; + sync[0] = NULL; + + /* now follow all the links from the nodes in the queue + * and add the peers to the queue. */ + spa_list_consume(n, &queue, sort_link) { + spa_list_remove(&n->sort_link); + spa_list_append(collect, &n->sort_link); + + pw_log_debug(" next node %p: '%s' runnable:%u active:%d", + n, n->name, n->runnable, n->active); + + if (!n->active) + continue; + + if (n->sync) { + for (uint32_t i = 0; n->sync_groups[i]; i++) { + if (n_sync >= MAX_SYNC) + break; + if (pw_strv_find(sync, n->sync_groups[i]) >= 0) + continue; + sync[n_sync++] = n->sync_groups[i]; + sync[n_sync] = NULL; + } + } + + spa_list_for_each(p, &n->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + t = l->output->node; + + if (!t->active) + continue; + + pw_impl_link_prepare(l); + + if (!l->prepared) + continue; + + if (!l->passive) + t->runnable = true; + + if (!t->visited) { + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + } + spa_list_for_each(p, &n->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + t = l->input->node; + + if (!t->active) + continue; + + pw_impl_link_prepare(l); + + if (!l->prepared) + continue; + + if (!l->passive) + t->runnable = true; + + if (!t->visited) { + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + } + /* now go through all the nodes that have the same group and + * that are not yet visited */ + if (n->groups != NULL || n->link_groups != NULL || sync[0] != NULL) { + spa_list_for_each(t, &context->node_list, link) { + if (t->exported || !t->active || t->visited) + continue; + /* the other node will be scheduled with this one if it's in + * the same group or link group */ + if (pw_strv_find_common(t->groups, n->groups) < 0 && + pw_strv_find_common(t->link_groups, n->link_groups) < 0 && + pw_strv_find_common(t->sync_groups, sync) < 0) + continue; + + pw_log_debug("%p: %s join group of %s", + t, t->name, n->name); + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, + n->groups, n->link_groups, sync); + } + /* All non-driver runnable nodes (ie. reachable with a non-passive link) now make + * all linked nodes up and downstream runnable as well */ + spa_list_for_each(n, collect, sort_link) { + if (!n->driver && n->runnable) { + run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); + run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); + } + } + /* now we might have made a driver runnable, if the node is not runnable at this point + * it means it was linked to the driver with passives links and some other node + * made the driver active. If the node is a leaf it can not be activated in any other + * way and we will also make it, and all its peers, runnable */ + spa_list_for_each(n, collect, sort_link) { + if (!n->driver && n->driver_node->runnable && !n->runnable && n->leaf && n->active) { + n->runnable = true; + run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); + run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); + } + } + + return 0; +} + +static void move_to_driver(struct pw_context *context, struct spa_list *nodes, + struct pw_impl_node *driver) +{ + struct pw_impl_node *n; + pw_log_debug("driver: %p %s runnable:%u", driver, driver->name, driver->runnable); + spa_list_consume(n, nodes, sort_link) { + spa_list_remove(&n->sort_link); + + driver->runnable |= n->runnable; + + pw_log_debug(" follower: %p %s runnable:%u driver-runnable:%u", n, n->name, + n->runnable, driver->runnable); + pw_impl_node_set_driver(n, driver); + } +} +static void remove_from_driver(struct pw_context *context, struct spa_list *nodes) +{ + struct pw_impl_node *n; + spa_list_consume(n, nodes, sort_link) { + spa_list_remove(&n->sort_link); + pw_impl_node_set_driver(n, NULL); + ensure_state(n, false); + } +} + +static inline void get_quantums(struct pw_context *context, uint32_t *def, + uint32_t *min, uint32_t *max, uint32_t *rate, uint32_t *floor, uint32_t *ceil) +{ + struct settings *s = &context->settings; + if (s->clock_force_quantum != 0) { + *def = *min = *max = s->clock_force_quantum; + *rate = 0; + } else { + *def = s->clock_quantum; + *min = s->clock_min_quantum; + *max = s->clock_max_quantum; + *rate = s->clock_rate; + } + *floor = s->clock_quantum_floor; + *ceil = s->clock_quantum_limit; +} + +static inline const uint32_t *get_rates(struct pw_context *context, uint32_t *def, uint32_t *n_rates, + bool *force) +{ + struct settings *s = &context->settings; + if (s->clock_force_rate != 0) { + *force = true; + *n_rates = 1; + *def = s->clock_force_rate; + return &s->clock_force_rate; + } else { + *force = false; + *n_rates = s->n_clock_rates; + *def = s->clock_rate; + return s->clock_rates; + } +} +static void reconfigure_driver(struct pw_context *context, struct pw_impl_node *n) +{ + struct pw_impl_node *s; + + spa_list_for_each(s, &n->follower_list, follower_link) { + if (s == n) + continue; + pw_log_debug("%p: follower %p: '%s' suspend", + context, s, s->name); + pw_impl_node_set_state(s, PW_NODE_STATE_SUSPENDED); + } + pw_log_debug("%p: driver %p: '%s' suspend", + context, n, n->name); + + if (n->info.state >= PW_NODE_STATE_IDLE) + n->need_resume = !n->pause_on_idle; + pw_impl_node_set_state(n, PW_NODE_STATE_SUSPENDED); +} + +/* find smaller power of 2 */ +static uint32_t flp2(uint32_t x) +{ + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); + return x - (x >> 1); +} + +/* cmp fractions, avoiding overflows */ +static int fraction_compare(const struct spa_fraction *a, const struct spa_fraction *b) +{ + uint64_t fa = (uint64_t)a->num * (uint64_t)b->denom; + uint64_t fb = (uint64_t)b->num * (uint64_t)a->denom; + return fa < fb ? -1 : (fa > fb ? 1 : 0); +} + +static inline uint32_t calc_gcd(uint32_t a, uint32_t b) +{ + while (b != 0) { + uint32_t temp = a; + a = b; + b = temp % b; + } + return a; +} + +struct rate_info { + uint32_t rate; + uint32_t gcd; + uint32_t diff; +}; + +static inline void update_highest_rate(struct rate_info *best, struct rate_info *current) +{ + /* find highest rate */ + if (best->rate == 0 || best->rate < current->rate) + *best = *current; +} + +static inline void update_nearest_gcd(struct rate_info *best, struct rate_info *current) +{ + /* find nearest GCD */ + if (best->rate == 0 || + (best->gcd < current->gcd) || + (best->gcd == current->gcd && best->diff > current->diff)) + *best = *current; +} +static inline void update_nearest_rate(struct rate_info *best, struct rate_info *current) +{ + /* find nearest rate */ + if (best->rate == 0 || best->diff > current->diff) + *best = *current; +} + +static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t def) +{ + uint32_t i, limit; + struct rate_info best; + struct rate_info info[n_rates]; + + for (i = 0; i < n_rates; i++) { + info[i].rate = rates[i]; + info[i].gcd = calc_gcd(rate, rates[i]); + info[i].diff = SPA_ABS((int32_t)rate - (int32_t)rates[i]); + } + + /* first find higher nearest GCD. This tries to find next bigest rate that + * requires the least amount of resample filter banks. Usually these are + * rates that are multiples of each other or multiples of a common rate. + * + * 44100 and [ 32000 56000 88200 96000 ] -> 88200 + * 48000 and [ 32000 56000 88200 96000 ] -> 96000 + * 88200 and [ 44100 48000 96000 192000 ] -> 96000 + * 32000 and [ 44100 192000 ] -> 44100 + * 8000 and [ 44100 48000 ] -> 48000 + * 8000 and [ 44100 192000 ] -> 44100 + * 11025 and [ 44100 48000 ] -> 44100 + * 44100 and [ 48000 176400 ] -> 48000 + * 144 and [ 44100 48000 88200 96000] -> 48000 + */ + spa_zero(best); + /* Don't try to do excessive upsampling by limiting the max rate + * for desired < default to default*2. For other rates allow + * a x3 upsample rate max. For values lower than half of the default, + * limit to the default. */ + limit = rate < def/2 ? def : rate < def ? def*2 : rate*3; + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= rate && info[i].rate <= limit) + update_nearest_gcd(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* we would need excessive upsampling, pick a nearest higher rate */ + spa_zero(best); + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= rate) + update_nearest_rate(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* There is nothing above the rate, we need to downsample. Try to downsample + * but only to something that is from a common rate family. Also don't + * try to downsample to something that will sound worse (< 44100). + * + * 88200 and [ 22050 44100 48000 ] -> 44100 + * 88200 and [ 22050 48000 ] -> 48000 + */ + spa_zero(best); + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= 44100) + update_nearest_gcd(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* There is nothing to downsample above our threshold. Downsample to whatever + * is the highest rate then. */ + spa_zero(best); + for (i = 0; i < n_rates; i++) + update_highest_rate(&best, &info[i]); + if (best.rate != 0) + return best.rate; + + return def; +} + +/* here we evaluate the complete state of the graph. + * + * It roughly operates in 3 stages: + * + * 1. go over all drivers and collect the nodes that need to be scheduled with the + * driver. This include all nodes that have an active link with the driver or + * with a node already scheduled with the driver. + * + * 2. go over all nodes that are not assigned to a driver. The ones that require + * a driver are moved to some random active driver found in step 1. + * + * 3. go over all drivers again, collect the quantum/rate of all followers, select + * the desired final value and activate the followers and then the driver. + * + * A complete graph evaluation is performed for each change that is made to the + * graph, such as making/destroying links, adding/removing nodes, property changes such + * as quantum/rate changes or metadata changes. + */ +static void context_recalc_graph(void *data) +{ + struct impl *impl = data; + struct pw_context *context = impl->context; + struct settings *settings = &context->settings; + struct pw_impl_node *n, *s, *target, *fallback; + const uint32_t *rates; + uint32_t max_quantum, min_quantum, def_quantum, rate_quantum, floor_quantum, ceil_quantum; + uint32_t n_rates, def_rate, transport; + bool freewheel, global_force_rate, global_force_quantum; + struct spa_list collect; + +again: + freewheel = false; + + /* clean up the flags first */ + spa_list_for_each(n, &context->node_list, link) { + n->visited = false; + n->checked = 0; + n->runnable = n->always_process && n->active; + } + + get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &rate_quantum, + &floor_quantum, &ceil_quantum); + rates = get_rates(context, &def_rate, &n_rates, &global_force_rate); + + global_force_quantum = rate_quantum == 0; + + /* start from all drivers and group all nodes that are linked + * to it. Some nodes are not (yet) linked to anything and they + * will end up 'unassigned' to a driver. Other nodes are drivers + * and if they have active followers, we can use them to schedule + * the unassigned nodes. */ + target = fallback = NULL; + spa_list_for_each(n, &context->driver_list, driver_link) { + if (n->exported) + continue; + + if (!n->visited) { + spa_list_init(&collect); + collect_nodes(context, n, &collect); + move_to_driver(context, &collect, n); + } + /* from now on we are only interested in active driving nodes + * with a driver_priority. We're going to see if there are + * active followers. */ + if (!n->driving || !n->active || n->priority_driver <= 0) + continue; + + /* first active driving node is fallback */ + if (fallback == NULL) + fallback = n; + + if (!n->runnable) + continue; + + spa_list_for_each(s, &n->follower_list, follower_link) { + pw_log_debug("%p: driver %p: follower %p %s: active:%d", + context, n, s, s->name, s->active); + if (s != n && s->active) { + /* if the driving node has active followers, it + * is a target for our unassigned nodes */ + if (target == NULL) + target = n; + if (n->freewheel) + freewheel = true; + break; + } + } + } + /* no active node, use fallback driving node */ + if (target == NULL) + target = fallback; + + /* update the freewheel status */ + pw_context_set_freewheel(context, freewheel); + + /* now go through all available nodes. The ones we didn't visit + * in collect_nodes() are not linked to any driver. We assign them + * to either an active driver or the first driver if they are in a + * group that needs a driver. Else we remove them from a driver + * and stop them. */ + spa_list_for_each(n, &context->node_list, link) { + struct pw_impl_node *t, *driver; + + if (n->exported || n->visited) + continue; + + pw_log_debug("%p: unassigned node %p: '%s' active:%d want_driver:%d target:%p", + context, n, n->name, n->active, n->want_driver, target); + + /* collect all nodes in this group */ + spa_list_init(&collect); + collect_nodes(context, n, &collect); + + driver = NULL; + spa_list_for_each(t, &collect, sort_link) { + /* is any active and want a driver */ + if ((t->want_driver && t->active && t->runnable) || + t->always_process) { + driver = target; + break; + } + } + if (driver != NULL) { + driver->runnable = true; + /* driver needed for this group */ + move_to_driver(context, &collect, driver); + } else { + /* no driver, make sure the nodes stop */ + remove_from_driver(context, &collect); + } + } + + /* assign final quantum and set state for followers and drivers */ + spa_list_for_each(n, &context->driver_list, driver_link) { + bool running = false, lock_quantum = false, lock_rate = false; + struct spa_fraction latency = SPA_FRACTION(0, 0); + struct spa_fraction max_latency = SPA_FRACTION(0, 0); + struct spa_fraction rate = SPA_FRACTION(0, 0); + uint32_t target_quantum, target_rate, current_rate, current_quantum; + uint64_t quantum_stamp = 0, rate_stamp = 0; + bool force_rate, force_quantum, restore_rate = false, restore_quantum = false; + bool do_reconfigure = false, need_resume, was_target_pending; + bool have_request = false; + const uint32_t *node_rates; + uint32_t node_n_rates, node_def_rate; + uint32_t node_max_quantum, node_min_quantum, node_def_quantum, node_rate_quantum; + + if (!n->driving || n->exported) + continue; + + node_def_quantum = def_quantum; + node_min_quantum = min_quantum; + node_max_quantum = max_quantum; + node_rate_quantum = rate_quantum; + force_quantum = global_force_quantum; + + node_def_rate = def_rate; + node_n_rates = n_rates; + node_rates = rates; + force_rate = global_force_rate; + + /* collect quantum and rate */ + spa_list_for_each(s, &n->follower_list, follower_link) { + + if (!s->moved) { + /* We only try to enforce the lock flags for nodes that + * are not recently moved between drivers. The nodes that + * are moved should try to enforce their quantum on the + * new driver. */ + lock_quantum |= s->lock_quantum; + lock_rate |= s->lock_rate; + } + if (!global_force_quantum && s->force_quantum > 0 && + s->stamp > quantum_stamp) { + node_def_quantum = node_min_quantum = node_max_quantum = s->force_quantum; + node_rate_quantum = 0; + quantum_stamp = s->stamp; + force_quantum = true; + } + if (!global_force_rate && s->force_rate > 0 && + s->stamp > rate_stamp) { + node_def_rate = s->force_rate; + node_n_rates = 1; + node_rates = &s->force_rate; + force_rate = true; + rate_stamp = s->stamp; + } + + /* smallest latencies */ + if (latency.denom == 0 || + (s->latency.denom > 0 && + fraction_compare(&s->latency, &latency) < 0)) + latency = s->latency; + if (max_latency.denom == 0 || + (s->max_latency.denom > 0 && + fraction_compare(&s->max_latency, &max_latency) < 0)) + max_latency = s->max_latency; + + /* largest rate, which is in fact the smallest fraction */ + if (rate.denom == 0 || + (s->rate.denom > 0 && + fraction_compare(&s->rate, &rate) < 0)) + rate = s->rate; + + if (s->active) + running = n->runnable; + + pw_log_debug("%p: follower %p running:%d runnable:%d rate:%u/%u latency %u/%u '%s'", + context, s, running, s->runnable, rate.num, rate.denom, + latency.num, latency.denom, s->name); + + if (running && s != n && s->supports_request > 0) + have_request = true; + + s->moved = false; + } + + if (n->forced_rate && !force_rate && n->runnable) { + /* A node that was forced to a rate but is no longer being + * forced can restore its rate */ + pw_log_info("(%s-%u) restore rate", n->name, n->info.id); + restore_rate = true; + } + if (n->forced_quantum && !force_quantum && n->runnable) { + /* A node that was forced to a quantum but is no longer being + * forced can restore its quantum */ + pw_log_info("(%s-%u) restore quantum", n->name, n->info.id); + restore_quantum = true; + } + + if (force_quantum) + lock_quantum = false; + if (force_rate) + lock_rate = false; + + need_resume = n->need_resume; + if (need_resume) { + running = true; + n->need_resume = false; + } + + current_rate = n->target_rate.denom; + if (!restore_rate && + (lock_rate || need_resume || !running || + (!force_rate && (n->info.state > PW_NODE_STATE_IDLE)))) { + pw_log_debug("%p: keep rate:1/%u restore:%u lock:%u resume:%u " + "running:%u force:%u state:%s", context, + current_rate, restore_rate, lock_rate, need_resume, + running, force_rate, + pw_node_state_as_string(n->info.state)); + + /* when we don't need to restore or rate and + * when someone wants us to lock the rate of this driver or + * when we are in the process of reconfiguring the driver or + * when we are not running any followers or + * when the driver is busy and we don't need to force a rate, + * keep the current rate */ + target_rate = current_rate; + } + else { + /* Here we are allowed to change the rate of the driver. + * Start with the default rate. If the desired rate is + * allowed, switch to it */ + if (rate.denom != 0 && rate.num == 1) + target_rate = rate.denom; + else + target_rate = node_def_rate; + + target_rate = find_best_rate(node_rates, node_n_rates, + target_rate, node_def_rate); + + pw_log_debug("%p: def_rate:%d target_rate:%d rate:%d/%d", context, + node_def_rate, target_rate, rate.num, rate.denom); + } + + was_target_pending = n->target_pending; + + if (target_rate != current_rate) { + /* we doing a rate switch */ + pw_log_info("(%s-%u) state:%s new rate:%u/(%u)->%u", + n->name, n->info.id, + pw_node_state_as_string(n->info.state), + n->target_rate.denom, current_rate, + target_rate); + + if (force_rate) { + if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD) + do_reconfigure |= !was_target_pending; + } else { + if (n->info.state >= PW_NODE_STATE_SUSPENDED) + do_reconfigure |= !was_target_pending; + } + /* we're setting the pending rate. This will become the new + * current rate in the next iteration of the graph. */ + n->target_rate = SPA_FRACTION(1, target_rate); + n->forced_rate = force_rate; + n->target_pending = true; + current_rate = target_rate; + } + + if (node_rate_quantum != 0 && current_rate != node_rate_quantum) { + /* the quantum values are scaled with the current rate */ + node_def_quantum = SPA_SCALE32(node_def_quantum, current_rate, node_rate_quantum); + node_min_quantum = SPA_SCALE32(node_min_quantum, current_rate, node_rate_quantum); + node_max_quantum = SPA_SCALE32(node_max_quantum, current_rate, node_rate_quantum); + } + + /* calculate desired quantum. Don't limit to the max_latency when we are + * going to force a quantum or rate and reconfigure the nodes. */ + if (max_latency.denom != 0 && !force_quantum && !force_rate) { + uint32_t tmp = SPA_SCALE32(max_latency.num, current_rate, max_latency.denom); + if (tmp < node_max_quantum) + node_max_quantum = tmp; + } + + current_quantum = n->target_quantum; + if (!restore_quantum && (lock_quantum || need_resume || !running)) { + pw_log_debug("%p: keep quantum:%u restore:%u lock:%u resume:%u " + "running:%u force:%u state:%s", context, + current_quantum, restore_quantum, lock_quantum, need_resume, + running, force_quantum, + pw_node_state_as_string(n->info.state)); + target_quantum = current_quantum; + } + else { + target_quantum = node_def_quantum; + if (latency.denom != 0) + target_quantum = SPA_SCALE32(latency.num, current_rate, latency.denom); + target_quantum = SPA_CLAMP(target_quantum, node_min_quantum, node_max_quantum); + target_quantum = SPA_CLAMP(target_quantum, floor_quantum, ceil_quantum); + + if (settings->clock_power_of_two_quantum && !force_quantum) + target_quantum = flp2(target_quantum); + } + + if (target_quantum != current_quantum) { + pw_log_info("(%s-%u) new quantum:%"PRIu64"->%u", + n->name, n->info.id, + n->target_quantum, + target_quantum); + /* this is the new pending quantum */ + n->target_quantum = target_quantum; + n->forced_quantum = force_quantum; + n->target_pending = true; + + if (force_quantum) + do_reconfigure |= !was_target_pending; + } + + if (n->target_pending) { + if (do_reconfigure) { + reconfigure_driver(context, n); + /* we might be suspended now and the links need to be prepared again */ + goto again; + } + /* we have a pending change. We place the new values in the + * pending fields so that they are picked up by the driver in + * the next cycle */ + pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context, + n->target_quantum, n->target_rate.num, + n->target_rate.denom); + SPA_SEQ_WRITE(n->rt.position->clock.target_seq); + n->rt.position->clock.target_duration = n->target_quantum; + n->rt.position->clock.target_rate = n->target_rate; + SPA_SEQ_WRITE(n->rt.position->clock.target_seq); + + if (n->info.state < PW_NODE_STATE_RUNNING) { + n->rt.position->clock.duration = n->target_quantum; + n->rt.position->clock.rate = n->target_rate; + } + n->target_pending = false; + } else { + n->target_quantum = n->rt.position->clock.target_duration; + n->target_rate = n->rt.position->clock.target_rate; + } + + if (n->info.state < PW_NODE_STATE_RUNNING) + n->rt.position->clock.nsec = get_time_ns(n->rt.target.system); + + SPA_FLAG_UPDATE(n->rt.position->clock.flags, + SPA_IO_CLOCK_FLAG_LAZY, have_request && n->supports_lazy > 0); + + pw_log_debug("%p: driver %p running:%d runnable:%d quantum:%u rate:%u (%"PRIu64"/%u)'%s'", + context, n, running, n->runnable, target_quantum, target_rate, + n->rt.position->clock.target_duration, + n->rt.position->clock.target_rate.denom, n->name); + + transport = PW_NODE_ACTIVATION_COMMAND_NONE; + + /* first change the node states of the followers to the new target */ + spa_list_for_each(s, &n->follower_list, follower_link) { + if (s->transport != PW_NODE_ACTIVATION_COMMAND_NONE) { + transport = s->transport; + s->transport = PW_NODE_ACTIVATION_COMMAND_NONE; + } + if (s == n) + continue; + pw_log_debug("%p: follower %p: active:%d '%s'", + context, s, s->active, s->name); + ensure_state(s, running); + } + + if (transport != PW_NODE_ACTIVATION_COMMAND_NONE) { + pw_log_info("%s: transport %d", n->name, transport); + SPA_ATOMIC_STORE(n->rt.target.activation->command, transport); + } + + /* now that all the followers are ready, start the driver */ + ensure_state(n, running); + } +} + +static const struct pw_context_events context_events = { + PW_VERSION_CONTEXT_EVENTS, + .recalc_graph = context_recalc_graph, +}; + +static void module_destroy(void *data) +{ + struct impl *impl = data; + + if (impl->context) { + spa_hook_remove(&impl->context_listener); + spa_hook_remove(&impl->module_listener); + } + + pw_properties_free(impl->props); + + free(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args_str) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *args; + struct impl *impl; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args_str); + + if (args_str) + args = pw_properties_new_string(args_str); + else + args = pw_properties_new(NULL, NULL); + + if (!args) { + res = -errno; + goto error; + } + + pw_context_conf_update_props(context, "module."NAME".args", args); + + impl->props = args; + impl->context = context; + + pw_context_add_listener(context, &impl->context_listener, &context_events, impl); + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + return 0; + +error: + module_destroy(impl); + return res; +} diff --git a/src/pipewire/context.c b/src/pipewire/context.c index 1c30be70a..40ed031bb 100644 --- a/src/pipewire/context.c +++ b/src/pipewire/context.c @@ -36,8 +36,6 @@ PW_LOG_TOPIC_EXTERN(log_context); #define PW_LOG_TOPIC_DEFAULT log_context -#define MAX_HOPS 64 -#define MAX_SYNC 4u #define MAX_LOOPS 64u #define DEFAULT_DATA_LOOPS 1 @@ -112,13 +110,17 @@ static void fill_core_properties(struct pw_context *context) pw_properties_set(properties, PW_KEY_CORE_NAME, context->core->info.name); } -static int context_set_freewheel(struct pw_context *context, bool freewheel) +SPA_EXPORT +int pw_context_set_freewheel(struct pw_context *context, bool freewheel) { struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this); struct spa_thread *thr; uint32_t i; int res = 0; + if (context->freewheeling == freewheel) + return 0; + for (i = 0; i < impl->n_data_loops; i++) { if (impl->data_loops[i].impl == NULL || (thr = pw_data_loop_get_thread(impl->data_loops[i].impl)) == NULL) @@ -982,468 +984,9 @@ SPA_PRINTF_FUNC(7, 8) int pw_context_debug_port_params(struct pw_context *this, return 0; } -static int ensure_state(struct pw_impl_node *node, bool running) -{ - enum pw_node_state state = node->info.state; - if (node->active && node->runnable && - !SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_NEED_CONFIGURE) && running) - state = PW_NODE_STATE_RUNNING; - else if (state > PW_NODE_STATE_IDLE) - state = PW_NODE_STATE_IDLE; - return pw_impl_node_set_state(node, state); -} - -/* From a node (that is runnable) follow all prepared links in the given direction - * and groups to active nodes and make them recursively runnable as well. - */ -static inline int run_nodes(struct pw_context *context, struct pw_impl_node *node, - struct spa_list *nodes, enum pw_direction direction, int hop) -{ - struct pw_impl_node *t; - struct pw_impl_port *p; - struct pw_impl_link *l; - - if (hop == MAX_HOPS) { - pw_log_warn("exceeded hops (%d)", hop); - return -EIO; - } - - pw_log_debug("node %p: '%s' direction:%s", node, node->name, - pw_direction_as_string(direction)); - - SPA_FLAG_SET(node->checked, 1u<input_ports, link) { - spa_list_for_each(l, &p->links, input_link) { - t = l->output->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } else { - spa_list_for_each(p, &node->output_ports, link) { - spa_list_for_each(l, &p->links, output_link) { - t = l->input->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } - /* now go through all the nodes that have the same link group and - * that are not yet visited. Note how nodes with the same group - * don't get included here. They were added to the same driver but - * need to otherwise stay idle unless some non-passive link activates - * them. */ - if (node->link_groups != NULL) { - spa_list_for_each(t, nodes, sort_link) { - if (t->exported || !t->active || - SPA_FLAG_IS_SET(t->checked, 1u<link_groups, node->link_groups) < 0) - continue; - - pw_log_debug(" group %p: '%s'", t, t->name); - t->runnable = true; - if (!t->driving) - run_nodes(context, t, nodes, direction, hop + 1); - } - } - return 0; -} - -/* Follow all prepared links and groups from node, activate the links. - * If a non-passive link is found, we set the peer runnable flag. - * - * After this is done, we end up with a list of nodes in collect that are all - * linked to node. - * Some of the nodes have the runnable flag set. We then start from those nodes - * and make all linked nodes and groups runnable as well. (see run_nodes). - * - * This ensures that we only activate the paths from the runnable nodes to the - * driver nodes and leave the other nodes idle. - */ -static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect) -{ - struct spa_list queue; - struct pw_impl_node *n, *t; - struct pw_impl_port *p; - struct pw_impl_link *l; - uint32_t n_sync; - char *sync[MAX_SYNC+1]; - - pw_log_debug("node %p: '%s'", node, node->name); - - /* start with node in the queue */ - spa_list_init(&queue); - spa_list_append(&queue, &node->sort_link); - node->visited = true; - - n_sync = 0; - sync[0] = NULL; - - /* now follow all the links from the nodes in the queue - * and add the peers to the queue. */ - spa_list_consume(n, &queue, sort_link) { - spa_list_remove(&n->sort_link); - spa_list_append(collect, &n->sort_link); - - pw_log_debug(" next node %p: '%s' runnable:%u active:%d", - n, n->name, n->runnable, n->active); - - if (!n->active) - continue; - - if (n->sync) { - for (uint32_t i = 0; n->sync_groups[i]; i++) { - if (n_sync >= MAX_SYNC) - break; - if (pw_strv_find(sync, n->sync_groups[i]) >= 0) - continue; - sync[n_sync++] = n->sync_groups[i]; - sync[n_sync] = NULL; - } - } - - spa_list_for_each(p, &n->input_ports, link) { - spa_list_for_each(l, &p->links, input_link) { - t = l->output->node; - - if (!t->active) - continue; - - pw_impl_link_prepare(l); - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - - if (!t->visited) { - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - } - spa_list_for_each(p, &n->output_ports, link) { - spa_list_for_each(l, &p->links, output_link) { - t = l->input->node; - - if (!t->active) - continue; - - pw_impl_link_prepare(l); - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - - if (!t->visited) { - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - } - /* now go through all the nodes that have the same group and - * that are not yet visited */ - if (n->groups != NULL || n->link_groups != NULL || sync[0] != NULL) { - spa_list_for_each(t, &context->node_list, link) { - if (t->exported || !t->active || t->visited) - continue; - /* the other node will be scheduled with this one if it's in - * the same group or link group */ - if (pw_strv_find_common(t->groups, n->groups) < 0 && - pw_strv_find_common(t->link_groups, n->link_groups) < 0 && - pw_strv_find_common(t->sync_groups, sync) < 0) - continue; - - pw_log_debug("%p: %s join group of %s", - t, t->name, n->name); - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, - n->groups, n->link_groups, sync); - } - /* All non-driver runnable nodes (ie. reachable with a non-passive link) now make - * all linked nodes up and downstream runnable as well */ - spa_list_for_each(n, collect, sort_link) { - if (!n->driver && n->runnable) { - run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); - run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); - } - } - /* now we might have made a driver runnable, if the node is not runnable at this point - * it means it was linked to the driver with passives links and some other node - * made the driver active. If the node is a leaf it can not be activated in any other - * way and we will also make it, and all its peers, runnable */ - spa_list_for_each(n, collect, sort_link) { - if (!n->driver && n->driver_node->runnable && !n->runnable && n->leaf && n->active) { - n->runnable = true; - run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); - run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); - } - } - - return 0; -} - -static void move_to_driver(struct pw_context *context, struct spa_list *nodes, - struct pw_impl_node *driver) -{ - struct pw_impl_node *n; - pw_log_debug("driver: %p %s runnable:%u", driver, driver->name, driver->runnable); - spa_list_consume(n, nodes, sort_link) { - spa_list_remove(&n->sort_link); - - driver->runnable |= n->runnable; - - pw_log_debug(" follower: %p %s runnable:%u driver-runnable:%u", n, n->name, - n->runnable, driver->runnable); - pw_impl_node_set_driver(n, driver); - } -} -static void remove_from_driver(struct pw_context *context, struct spa_list *nodes) -{ - struct pw_impl_node *n; - spa_list_consume(n, nodes, sort_link) { - spa_list_remove(&n->sort_link); - pw_impl_node_set_driver(n, NULL); - ensure_state(n, false); - } -} - -static inline void get_quantums(struct pw_context *context, uint32_t *def, - uint32_t *min, uint32_t *max, uint32_t *rate, uint32_t *floor, uint32_t *ceil) -{ - struct settings *s = &context->settings; - if (s->clock_force_quantum != 0) { - *def = *min = *max = s->clock_force_quantum; - *rate = 0; - } else { - *def = s->clock_quantum; - *min = s->clock_min_quantum; - *max = s->clock_max_quantum; - *rate = s->clock_rate; - } - *floor = s->clock_quantum_floor; - *ceil = s->clock_quantum_limit; -} - -static inline const uint32_t *get_rates(struct pw_context *context, uint32_t *def, uint32_t *n_rates, - bool *force) -{ - struct settings *s = &context->settings; - if (s->clock_force_rate != 0) { - *force = true; - *n_rates = 1; - *def = s->clock_force_rate; - return &s->clock_force_rate; - } else { - *force = false; - *n_rates = s->n_clock_rates; - *def = s->clock_rate; - return s->clock_rates; - } -} -static void reconfigure_driver(struct pw_context *context, struct pw_impl_node *n) -{ - struct pw_impl_node *s; - - spa_list_for_each(s, &n->follower_list, follower_link) { - if (s == n) - continue; - pw_log_debug("%p: follower %p: '%s' suspend", - context, s, s->name); - pw_impl_node_set_state(s, PW_NODE_STATE_SUSPENDED); - } - pw_log_debug("%p: driver %p: '%s' suspend", - context, n, n->name); - - if (n->info.state >= PW_NODE_STATE_IDLE) - n->need_resume = !n->pause_on_idle; - pw_impl_node_set_state(n, PW_NODE_STATE_SUSPENDED); -} - -/* find smaller power of 2 */ -static uint32_t flp2(uint32_t x) -{ - x = x | (x >> 1); - x = x | (x >> 2); - x = x | (x >> 4); - x = x | (x >> 8); - x = x | (x >> 16); - return x - (x >> 1); -} - -/* cmp fractions, avoiding overflows */ -static int fraction_compare(const struct spa_fraction *a, const struct spa_fraction *b) -{ - uint64_t fa = (uint64_t)a->num * (uint64_t)b->denom; - uint64_t fb = (uint64_t)b->num * (uint64_t)a->denom; - return fa < fb ? -1 : (fa > fb ? 1 : 0); -} - -static inline uint32_t calc_gcd(uint32_t a, uint32_t b) -{ - while (b != 0) { - uint32_t temp = a; - a = b; - b = temp % b; - } - return a; -} - -struct rate_info { - uint32_t rate; - uint32_t gcd; - uint32_t diff; -}; - -static inline void update_highest_rate(struct rate_info *best, struct rate_info *current) -{ - /* find highest rate */ - if (best->rate == 0 || best->rate < current->rate) - *best = *current; -} - -static inline void update_nearest_gcd(struct rate_info *best, struct rate_info *current) -{ - /* find nearest GCD */ - if (best->rate == 0 || - (best->gcd < current->gcd) || - (best->gcd == current->gcd && best->diff > current->diff)) - *best = *current; -} -static inline void update_nearest_rate(struct rate_info *best, struct rate_info *current) -{ - /* find nearest rate */ - if (best->rate == 0 || best->diff > current->diff) - *best = *current; -} - -static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t def) -{ - uint32_t i, limit; - struct rate_info best; - struct rate_info info[n_rates]; - - for (i = 0; i < n_rates; i++) { - info[i].rate = rates[i]; - info[i].gcd = calc_gcd(rate, rates[i]); - info[i].diff = SPA_ABS((int32_t)rate - (int32_t)rates[i]); - } - - /* first find higher nearest GCD. This tries to find next bigest rate that - * requires the least amount of resample filter banks. Usually these are - * rates that are multiples of each other or multiples of a common rate. - * - * 44100 and [ 32000 56000 88200 96000 ] -> 88200 - * 48000 and [ 32000 56000 88200 96000 ] -> 96000 - * 88200 and [ 44100 48000 96000 192000 ] -> 96000 - * 32000 and [ 44100 192000 ] -> 44100 - * 8000 and [ 44100 48000 ] -> 48000 - * 8000 and [ 44100 192000 ] -> 44100 - * 11025 and [ 44100 48000 ] -> 44100 - * 44100 and [ 48000 176400 ] -> 48000 - * 144 and [ 44100 48000 88200 96000] -> 48000 - */ - spa_zero(best); - /* Don't try to do excessive upsampling by limiting the max rate - * for desired < default to default*2. For other rates allow - * a x3 upsample rate max. For values lower than half of the default, - * limit to the default. */ - limit = rate < def/2 ? def : rate < def ? def*2 : rate*3; - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= rate && info[i].rate <= limit) - update_nearest_gcd(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* we would need excessive upsampling, pick a nearest higher rate */ - spa_zero(best); - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= rate) - update_nearest_rate(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* There is nothing above the rate, we need to downsample. Try to downsample - * but only to something that is from a common rate family. Also don't - * try to downsample to something that will sound worse (< 44100). - * - * 88200 and [ 22050 44100 48000 ] -> 44100 - * 88200 and [ 22050 48000 ] -> 48000 - */ - spa_zero(best); - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= 44100) - update_nearest_gcd(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* There is nothing to downsample above our threshold. Downsample to whatever - * is the highest rate then. */ - spa_zero(best); - for (i = 0; i < n_rates; i++) - update_highest_rate(&best, &info[i]); - if (best.rate != 0) - return best.rate; - - return def; -} - -/* here we evaluate the complete state of the graph. - * - * It roughly operates in 3 stages: - * - * 1. go over all drivers and collect the nodes that need to be scheduled with the - * driver. This include all nodes that have an active link with the driver or - * with a node already scheduled with the driver. - * - * 2. go over all nodes that are not assigned to a driver. The ones that require - * a driver are moved to some random active driver found in step 1. - * - * 3. go over all drivers again, collect the quantum/rate of all followers, select - * the desired final value and activate the followers and then the driver. - * - * A complete graph evaluation is performed for each change that is made to the - * graph, such as making/destroying links, adding/removing nodes, property changes such - * as quantum/rate changes or metadata changes. - */ int pw_context_recalc_graph(struct pw_context *context, const char *reason) { struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this); - struct settings *settings = &context->settings; - struct pw_impl_node *n, *s, *target, *fallback; - const uint32_t *rates; - uint32_t max_quantum, min_quantum, def_quantum, rate_quantum, floor_quantum, ceil_quantum; - uint32_t n_rates, def_rate, transport; - bool freewheel, global_force_rate, global_force_quantum; - struct spa_list collect; pw_log_info("%p: busy:%d reason:%s", context, impl->recalc, reason); @@ -1454,392 +997,14 @@ int pw_context_recalc_graph(struct pw_context *context, const char *reason) again: impl->recalc = true; - freewheel = false; - /* clean up the flags first */ - spa_list_for_each(n, &context->node_list, link) { - n->visited = false; - n->checked = 0; - n->runnable = n->always_process && n->active; - } + pw_context_emit_recalc_graph(context); - get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &rate_quantum, - &floor_quantum, &ceil_quantum); - rates = get_rates(context, &def_rate, &n_rates, &global_force_rate); - - global_force_quantum = rate_quantum == 0; - - /* start from all drivers and group all nodes that are linked - * to it. Some nodes are not (yet) linked to anything and they - * will end up 'unassigned' to a driver. Other nodes are drivers - * and if they have active followers, we can use them to schedule - * the unassigned nodes. */ - target = fallback = NULL; - spa_list_for_each(n, &context->driver_list, driver_link) { - if (n->exported) - continue; - - if (!n->visited) { - spa_list_init(&collect); - collect_nodes(context, n, &collect); - move_to_driver(context, &collect, n); - } - /* from now on we are only interested in active driving nodes - * with a driver_priority. We're going to see if there are - * active followers. */ - if (!n->driving || !n->active || n->priority_driver <= 0) - continue; - - /* first active driving node is fallback */ - if (fallback == NULL) - fallback = n; - - if (!n->runnable) - continue; - - spa_list_for_each(s, &n->follower_list, follower_link) { - pw_log_debug("%p: driver %p: follower %p %s: active:%d", - context, n, s, s->name, s->active); - if (s != n && s->active) { - /* if the driving node has active followers, it - * is a target for our unassigned nodes */ - if (target == NULL) - target = n; - if (n->freewheel) - freewheel = true; - break; - } - } - } - /* no active node, use fallback driving node */ - if (target == NULL) - target = fallback; - - /* update the freewheel status */ - if (context->freewheeling != freewheel) - context_set_freewheel(context, freewheel); - - /* now go through all available nodes. The ones we didn't visit - * in collect_nodes() are not linked to any driver. We assign them - * to either an active driver or the first driver if they are in a - * group that needs a driver. Else we remove them from a driver - * and stop them. */ - spa_list_for_each(n, &context->node_list, link) { - struct pw_impl_node *t, *driver; - - if (n->exported || n->visited) - continue; - - pw_log_debug("%p: unassigned node %p: '%s' active:%d want_driver:%d target:%p", - context, n, n->name, n->active, n->want_driver, target); - - /* collect all nodes in this group */ - spa_list_init(&collect); - collect_nodes(context, n, &collect); - - driver = NULL; - spa_list_for_each(t, &collect, sort_link) { - /* is any active and want a driver */ - if ((t->want_driver && t->active && t->runnable) || - t->always_process) { - driver = target; - break; - } - } - if (driver != NULL) { - driver->runnable = true; - /* driver needed for this group */ - move_to_driver(context, &collect, driver); - } else { - /* no driver, make sure the nodes stop */ - remove_from_driver(context, &collect); - } - } - - /* assign final quantum and set state for followers and drivers */ - spa_list_for_each(n, &context->driver_list, driver_link) { - bool running = false, lock_quantum = false, lock_rate = false; - struct spa_fraction latency = SPA_FRACTION(0, 0); - struct spa_fraction max_latency = SPA_FRACTION(0, 0); - struct spa_fraction rate = SPA_FRACTION(0, 0); - uint32_t target_quantum, target_rate, current_rate, current_quantum; - uint64_t quantum_stamp = 0, rate_stamp = 0; - bool force_rate, force_quantum, restore_rate = false, restore_quantum = false; - bool do_reconfigure = false, need_resume, was_target_pending; - bool have_request = false; - const uint32_t *node_rates; - uint32_t node_n_rates, node_def_rate; - uint32_t node_max_quantum, node_min_quantum, node_def_quantum, node_rate_quantum; - - if (!n->driving || n->exported) - continue; - - node_def_quantum = def_quantum; - node_min_quantum = min_quantum; - node_max_quantum = max_quantum; - node_rate_quantum = rate_quantum; - force_quantum = global_force_quantum; - - node_def_rate = def_rate; - node_n_rates = n_rates; - node_rates = rates; - force_rate = global_force_rate; - - /* collect quantum and rate */ - spa_list_for_each(s, &n->follower_list, follower_link) { - - if (!s->moved) { - /* We only try to enforce the lock flags for nodes that - * are not recently moved between drivers. The nodes that - * are moved should try to enforce their quantum on the - * new driver. */ - lock_quantum |= s->lock_quantum; - lock_rate |= s->lock_rate; - } - if (!global_force_quantum && s->force_quantum > 0 && - s->stamp > quantum_stamp) { - node_def_quantum = node_min_quantum = node_max_quantum = s->force_quantum; - node_rate_quantum = 0; - quantum_stamp = s->stamp; - force_quantum = true; - } - if (!global_force_rate && s->force_rate > 0 && - s->stamp > rate_stamp) { - node_def_rate = s->force_rate; - node_n_rates = 1; - node_rates = &s->force_rate; - force_rate = true; - rate_stamp = s->stamp; - } - - /* smallest latencies */ - if (latency.denom == 0 || - (s->latency.denom > 0 && - fraction_compare(&s->latency, &latency) < 0)) - latency = s->latency; - if (max_latency.denom == 0 || - (s->max_latency.denom > 0 && - fraction_compare(&s->max_latency, &max_latency) < 0)) - max_latency = s->max_latency; - - /* largest rate, which is in fact the smallest fraction */ - if (rate.denom == 0 || - (s->rate.denom > 0 && - fraction_compare(&s->rate, &rate) < 0)) - rate = s->rate; - - if (s->active) - running = n->runnable; - - pw_log_debug("%p: follower %p running:%d runnable:%d rate:%u/%u latency %u/%u '%s'", - context, s, running, s->runnable, rate.num, rate.denom, - latency.num, latency.denom, s->name); - - if (running && s != n && s->supports_request > 0) - have_request = true; - - s->moved = false; - } - - if (n->forced_rate && !force_rate && n->runnable) { - /* A node that was forced to a rate but is no longer being - * forced can restore its rate */ - pw_log_info("(%s-%u) restore rate", n->name, n->info.id); - restore_rate = true; - } - if (n->forced_quantum && !force_quantum && n->runnable) { - /* A node that was forced to a quantum but is no longer being - * forced can restore its quantum */ - pw_log_info("(%s-%u) restore quantum", n->name, n->info.id); - restore_quantum = true; - } - - if (force_quantum) - lock_quantum = false; - if (force_rate) - lock_rate = false; - - need_resume = n->need_resume; - if (need_resume) { - running = true; - n->need_resume = false; - } - - current_rate = n->target_rate.denom; - if (!restore_rate && - (lock_rate || need_resume || !running || - (!force_rate && (n->info.state > PW_NODE_STATE_IDLE)))) { - pw_log_debug("%p: keep rate:1/%u restore:%u lock:%u resume:%u " - "running:%u force:%u state:%s", context, - current_rate, restore_rate, lock_rate, need_resume, - running, force_rate, - pw_node_state_as_string(n->info.state)); - - /* when we don't need to restore or rate and - * when someone wants us to lock the rate of this driver or - * when we are in the process of reconfiguring the driver or - * when we are not running any followers or - * when the driver is busy and we don't need to force a rate, - * keep the current rate */ - target_rate = current_rate; - } - else { - /* Here we are allowed to change the rate of the driver. - * Start with the default rate. If the desired rate is - * allowed, switch to it */ - if (rate.denom != 0 && rate.num == 1) - target_rate = rate.denom; - else - target_rate = node_def_rate; - - target_rate = find_best_rate(node_rates, node_n_rates, - target_rate, node_def_rate); - - pw_log_debug("%p: def_rate:%d target_rate:%d rate:%d/%d", context, - node_def_rate, target_rate, rate.num, rate.denom); - } - - was_target_pending = n->target_pending; - - if (target_rate != current_rate) { - /* we doing a rate switch */ - pw_log_info("(%s-%u) state:%s new rate:%u/(%u)->%u", - n->name, n->info.id, - pw_node_state_as_string(n->info.state), - n->target_rate.denom, current_rate, - target_rate); - - if (force_rate) { - if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD) - do_reconfigure |= !was_target_pending; - } else { - if (n->info.state >= PW_NODE_STATE_SUSPENDED) - do_reconfigure |= !was_target_pending; - } - /* we're setting the pending rate. This will become the new - * current rate in the next iteration of the graph. */ - n->target_rate = SPA_FRACTION(1, target_rate); - n->forced_rate = force_rate; - n->target_pending = true; - current_rate = target_rate; - } - - if (node_rate_quantum != 0 && current_rate != node_rate_quantum) { - /* the quantum values are scaled with the current rate */ - node_def_quantum = SPA_SCALE32(node_def_quantum, current_rate, node_rate_quantum); - node_min_quantum = SPA_SCALE32(node_min_quantum, current_rate, node_rate_quantum); - node_max_quantum = SPA_SCALE32(node_max_quantum, current_rate, node_rate_quantum); - } - - /* calculate desired quantum. Don't limit to the max_latency when we are - * going to force a quantum or rate and reconfigure the nodes. */ - if (max_latency.denom != 0 && !force_quantum && !force_rate) { - uint32_t tmp = SPA_SCALE32(max_latency.num, current_rate, max_latency.denom); - if (tmp < node_max_quantum) - node_max_quantum = tmp; - } - - current_quantum = n->target_quantum; - if (!restore_quantum && (lock_quantum || need_resume || !running)) { - pw_log_debug("%p: keep quantum:%u restore:%u lock:%u resume:%u " - "running:%u force:%u state:%s", context, - current_quantum, restore_quantum, lock_quantum, need_resume, - running, force_quantum, - pw_node_state_as_string(n->info.state)); - target_quantum = current_quantum; - } - else { - target_quantum = node_def_quantum; - if (latency.denom != 0) - target_quantum = SPA_SCALE32(latency.num, current_rate, latency.denom); - target_quantum = SPA_CLAMP(target_quantum, node_min_quantum, node_max_quantum); - target_quantum = SPA_CLAMP(target_quantum, floor_quantum, ceil_quantum); - - if (settings->clock_power_of_two_quantum && !force_quantum) - target_quantum = flp2(target_quantum); - } - - if (target_quantum != current_quantum) { - pw_log_info("(%s-%u) new quantum:%"PRIu64"->%u", - n->name, n->info.id, - n->target_quantum, - target_quantum); - /* this is the new pending quantum */ - n->target_quantum = target_quantum; - n->forced_quantum = force_quantum; - n->target_pending = true; - - if (force_quantum) - do_reconfigure |= !was_target_pending; - } - - if (n->target_pending) { - if (do_reconfigure) { - reconfigure_driver(context, n); - /* we might be suspended now and the links need to be prepared again */ - goto again; - } - /* we have a pending change. We place the new values in the - * pending fields so that they are picked up by the driver in - * the next cycle */ - pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context, - n->target_quantum, n->target_rate.num, - n->target_rate.denom); - SPA_SEQ_WRITE(n->rt.position->clock.target_seq); - n->rt.position->clock.target_duration = n->target_quantum; - n->rt.position->clock.target_rate = n->target_rate; - SPA_SEQ_WRITE(n->rt.position->clock.target_seq); - - if (n->info.state < PW_NODE_STATE_RUNNING) { - n->rt.position->clock.duration = n->target_quantum; - n->rt.position->clock.rate = n->target_rate; - } - n->target_pending = false; - } else { - n->target_quantum = n->rt.position->clock.target_duration; - n->target_rate = n->rt.position->clock.target_rate; - } - - if (n->info.state < PW_NODE_STATE_RUNNING) - n->rt.position->clock.nsec = get_time_ns(n->rt.target.system); - - SPA_FLAG_UPDATE(n->rt.position->clock.flags, - SPA_IO_CLOCK_FLAG_LAZY, have_request && n->supports_lazy > 0); - - pw_log_debug("%p: driver %p running:%d runnable:%d quantum:%u rate:%u (%"PRIu64"/%u)'%s'", - context, n, running, n->runnable, target_quantum, target_rate, - n->rt.position->clock.target_duration, - n->rt.position->clock.target_rate.denom, n->name); - - transport = PW_NODE_ACTIVATION_COMMAND_NONE; - - /* first change the node states of the followers to the new target */ - spa_list_for_each(s, &n->follower_list, follower_link) { - if (s->transport != PW_NODE_ACTIVATION_COMMAND_NONE) { - transport = s->transport; - s->transport = PW_NODE_ACTIVATION_COMMAND_NONE; - } - if (s == n) - continue; - pw_log_debug("%p: follower %p: active:%d '%s'", - context, s, s->active, s->name); - ensure_state(s, running); - } - - if (transport != PW_NODE_ACTIVATION_COMMAND_NONE) { - pw_log_info("%s: transport %d", n->name, transport); - SPA_ATOMIC_STORE(n->rt.target.activation->command, transport); - } - - /* now that all the followers are ready, start the driver */ - ensure_state(n, running); - } impl->recalc = false; if (impl->recalc_pending) { impl->recalc_pending = false; goto again; } - return 0; } diff --git a/src/pipewire/context.h b/src/pipewire/context.h index 61c6662c4..5eaa8de30 100644 --- a/src/pipewire/context.h +++ b/src/pipewire/context.h @@ -51,7 +51,7 @@ struct pw_impl_node; /** context events emitted by the context object added with \ref pw_context_add_listener */ struct pw_context_events { -#define PW_VERSION_CONTEXT_EVENTS 1 +#define PW_VERSION_CONTEXT_EVENTS 2 uint32_t version; /** The context is being destroyed */ @@ -69,6 +69,9 @@ struct pw_context_events { void (*driver_added) (void *data, struct pw_impl_node *node); /** a driver was removed, since 0.3.75 version:1 */ void (*driver_removed) (void *data, struct pw_impl_node *node); + + /** recalculate the graph state, since 1.7.0 version:2 */ + void (*recalc_graph) (void *data); }; /** Make a new context object for a given main_loop. Ownership of the properties is taken, even diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index a77dcf35f..610a6d620 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -969,6 +969,7 @@ static void output_remove(struct pw_impl_link *this) this->output = NULL; } +SPA_EXPORT int pw_impl_link_prepare(struct pw_impl_link *this) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); diff --git a/src/pipewire/private.h b/src/pipewire/private.h index 5582709ad..95a1a4646 100644 --- a/src/pipewire/private.h +++ b/src/pipewire/private.h @@ -364,6 +364,7 @@ pw_core_resource_errorf(struct pw_resource *resource, uint32_t id, int seq, #define pw_context_emit_global_removed(c,g) pw_context_emit(c, global_removed, 0, g) #define pw_context_emit_driver_added(c,n) pw_context_emit(c, driver_added, 1, n) #define pw_context_emit_driver_removed(c,n) pw_context_emit(c, driver_removed, 1, n) +#define pw_context_emit_recalc_graph(c) pw_context_emit(c, recalc_graph, 2) struct pw_context { struct pw_impl_core *core; /**< core object */ @@ -1269,6 +1270,8 @@ int pw_context_debug_port_params(struct pw_context *context, struct spa_node *node, enum spa_direction direction, uint32_t port_id, uint32_t id, int err, const char *debug, ...); +int pw_context_set_freewheel(struct pw_context *context, bool freewheel); + int pw_proxy_init(struct pw_proxy *proxy, struct pw_core *core, const char *type, uint32_t version); void pw_proxy_remove(struct pw_proxy *proxy); From 987579b7b7df4a6ea4aa278d3aab9f91a94fbd64 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Thu, 19 Feb 2026 10:43:45 -0800 Subject: [PATCH 76/93] tests: Update context events test New field and version, update the test. --- test/test-context.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test-context.c b/test/test-context.c index 093e7b388..e35de484c 100644 --- a/test/test-context.c +++ b/test/test-context.c @@ -29,6 +29,7 @@ PWTEST(context_abi) void (*global_removed) (void *data, struct pw_global *global); void (*driver_added) (void *data, struct pw_impl_node *node); void (*driver_removed) (void *data, struct pw_impl_node *node); + void (*recalc_graph) (void *data); } test = { PW_VERSION_CONTEXT_EVENTS, NULL }; pw_init(0, NULL); @@ -41,7 +42,7 @@ PWTEST(context_abi) TEST_FUNC(ev, test, driver_added); TEST_FUNC(ev, test, driver_removed); - pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 1); + pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 2); pwtest_int_eq(sizeof(ev), sizeof(test)); pw_deinit(); From 569c2dce5577a9414166253a0c27794e5a7a53d1 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Thu, 19 Feb 2026 10:57:24 -0800 Subject: [PATCH 77/93] doc: Add module scheduler subpage --- doc/dox/modules.dox | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/dox/modules.dox b/doc/dox/modules.dox index 4e9358197..7ad81dabc 100644 --- a/doc/dox/modules.dox +++ b/doc/dox/modules.dox @@ -81,6 +81,7 @@ List of known modules: - \subpage page_module_raop_discover - \subpage page_module_roc_sink - \subpage page_module_roc_source +- \subpage page_module_scheduler_v1 - \subpage page_module_rtp_sap - \subpage page_module_rtp_sink - \subpage page_module_rtp_source From f5107f3e836f907cba39bf1de86c85f645eee8da Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Thu, 19 Feb 2026 10:58:03 -0800 Subject: [PATCH 78/93] ci: Fix doccheck error message --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 448bfa06e..a75b2a140 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -658,7 +658,7 @@ doccheck: - cat pipewire_module_pages - | for page in $(cat pipewire_module_pages); do - git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/pipewire-modules.dox" && false) + git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/dox/modules.dox" && false) done check_missing_headers: From c244fbf945a4e240f40bdf0fe6baf0dfc4ddde13 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 17 Feb 2026 14:17:30 -0800 Subject: [PATCH 79/93] spa: json: Add a helper method to shrink an object string --- spa/include/spa/utils/json.h | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h index 212637dab..5021290bf 100644 --- a/spa/include/spa/utils/json.h +++ b/spa/include/spa/utils/json.h @@ -212,6 +212,111 @@ SPA_API_JSON_UTILS int spa_json_str_array_uint32(const char *arr, size_t arr_len spa_json_make_str_array_unpack(32,uint32_t, atoi); } +/* convenience */ + +#define _SPA_STR_APPEND(str, len, idx, value ) \ +{ \ + if ((idx) >= (len)) \ + return -1; \ + (str)[(idx)++] = (value); \ +} + +static int _spa_json_str_object_reduce(struct spa_json *json, char *out, size_t out_size, const char *value, size_t len) +{ + struct spa_json sub; + size_t idx = 0; + int count = 0, res; + + if (spa_json_is_object(value, len)) { + char key[1024]; + + _SPA_STR_APPEND(out, out_size, idx, '{'); + + spa_json_enter(json, &sub); + while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { + _SPA_STR_APPEND(out, out_size, idx, '"'); + if (idx + strlen(key) >= out_size) + return -1; + strcpy(&out[idx], key); + idx += strlen(key); + _SPA_STR_APPEND(out, out_size, idx, '"'); + _SPA_STR_APPEND(out, out_size, idx, ':'); + + res = _spa_json_str_object_reduce(&sub, &out[idx], out_size - idx, value, len); + if (res < 0) + return res; + + idx += res; + _SPA_STR_APPEND(out, out_size, idx, ','); + count++; + } + + /* Remove trailing comma */ + if (count) + idx--; + _SPA_STR_APPEND(out, out_size, idx, '}'); + } else if (spa_json_is_array(value, len)) { + _SPA_STR_APPEND(out, out_size, idx, '['); + + spa_json_enter(json, &sub); + while ((len = spa_json_next(&sub, &value)) > 0) { + res = _spa_json_str_object_reduce(&sub, &out[idx], out_size - idx, value, len); + if (res < 0) + return res; + + idx += res; + _SPA_STR_APPEND(out, out_size, idx, ','); + count++; + } + + /* Remove trailing comma */ + if (count) + idx--; + _SPA_STR_APPEND(out, out_size, idx, ']'); + } else if (spa_json_is_string(value, len) || + spa_json_is_null(value, len) || + spa_json_is_bool(value, len) || + spa_json_is_int(value, len) || + spa_json_is_float(value, len)) { + /* Object type we understand */ + if (len >= out_size) + return -1; + strcpy(out, value); + idx += len; + } else { + /* Naked value, treat as string */ + _SPA_STR_APPEND(out, out_size, idx, '"'); + if (idx + len >= out_size) + return -1; + strncpy(&out[idx], value, len); + idx += len; + _SPA_STR_APPEND(out, out_size, idx, '"'); + } + + return idx; +} + +/* Parse a JSON object string and strip all whitespaces */ +SPA_API_JSON_UTILS int spa_json_str_object_reduce_inplace(char *str) +{ + struct spa_json json; + size_t size = strlen(str) + 1, len; + char temp[size]; + const char *value; + int res; + + len = spa_json_begin(&json, str, size, &value); + + res = _spa_json_str_object_reduce(&json, temp, size, value, len); + if (res < 0) + return res; + temp[res] = '\0'; + + strncpy(str, temp, size); + + return res; +} + /** * \} */ From d4329600d1ae774c0a5d23523472806341c4441b Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 17 Feb 2026 15:29:23 -0800 Subject: [PATCH 80/93] audioconvert: Report loaded filter graphs in props Makes it easier to know what filters have been loaded. --- spa/plugins/audioconvert/audioconvert.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index e9355ca41..5a4153770 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -268,6 +268,7 @@ struct impl { struct spa_list active_graphs; struct filter_graph graphs[MAX_GRAPH]; struct spa_process_latency_info latency; + char *graph_descs[MAX_GRAPH]; int in_filter_props; int filter_props_count; @@ -846,6 +847,7 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index, { struct props *p = &this->props; struct spa_pod_frame f[2]; + struct filter_graph *g; switch (index) { case 0: @@ -918,8 +920,12 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index, spa_pod_builder_bool(b, p->lock_volumes); spa_pod_builder_string(b, "audioconvert.filter-graph.disable"); spa_pod_builder_bool(b, p->filter_graph_disabled); - spa_pod_builder_string(b, "audioconvert.filter-graph"); - spa_pod_builder_string(b, ""); + spa_list_for_each(g, &this->active_graphs, link) { + char key[64]; + snprintf(key, sizeof(key), "audioconvert.filter-graph.%d", g->order); + spa_pod_builder_string(b, key); + spa_pod_builder_string(b, this->graph_descs[g->order]); + } spa_pod_builder_pop(b, &f[1]); *param = spa_pod_builder_pop(b, &f[0]); break; @@ -953,7 +959,7 @@ static int impl_node_enum_params(void *object, int seq, struct impl *this = object; struct spa_pod *param; struct spa_pod_builder b = { 0 }; - uint8_t buffer[4096]; + uint8_t buffer[16384]; struct spa_result_node_params result; uint32_t count = 0; int res = 0; @@ -1409,6 +1415,7 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) g->removing = true; spa_log_info(impl->log, "removing filter-graph order:%d", order); } + free(impl->graph_descs[order]); } if (graph != NULL && graph[0] != '\0') { @@ -1434,6 +1441,9 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) spa_list_remove(&pending->link); insert_graph(&impl->active_graphs, pending); + impl->graph_descs[order] = strdup(graph); + spa_json_str_object_reduce_inplace(impl->graph_descs[order]); + spa_log_info(impl->log, "loading filter-graph order:%d", order); } if (impl->setup) @@ -4234,6 +4244,7 @@ static void free_dir(struct dir *dir) static int impl_clear(struct spa_handle *handle) { struct impl *this; + int i; spa_return_val_if_fail(handle != NULL, -EINVAL); @@ -4245,6 +4256,10 @@ static int impl_clear(struct spa_handle *handle) free_tmp(this); clean_filter_handles(this, true); + for (i = 0; i < MAX_GRAPH; i++) { + if (this->graph_descs[i]) + free(this->graph_descs[i]); + } if (this->resample.free) resample_free(&this->resample); @@ -4299,6 +4314,7 @@ impl_init(const struct spa_handle_factory *factory, struct filter_graph *g = &this->graphs[i]; g->impl = this; spa_list_append(&this->free_graphs, &g->link); + this->graph_descs[i] = NULL; } this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; From d8b06f94ee0a5bc9f70371fd57937643cc7c8775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20P=C5=91cze?= Date: Thu, 19 Feb 2026 20:11:08 +0100 Subject: [PATCH 81/93] pipewire: module-roc-{sink,source}: remove logging related unused code !2699 has been merged a bit prematurely and it contained things that are not used. So remove the unused member variables, functions, fix module usage strings, and move some functions from headers. --- src/modules/module-roc-sink.c | 13 +------ src/modules/module-roc-source.c | 13 +------ src/modules/module-roc/common.c | 45 ++++++++++++++++++++++--- src/modules/module-roc/common.h | 60 --------------------------------- 4 files changed, 43 insertions(+), 88 deletions(-) diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c index 39ca2bce1..66a2c716b 100644 --- a/src/modules/module-roc-sink.c +++ b/src/modules/module-roc-sink.c @@ -120,8 +120,6 @@ struct module_roc_sink_data { roc_endpoint *remote_control_addr; int remote_control_port; - - roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -391,8 +389,7 @@ static const struct spa_dict_item module_roc_sink_info[] = { "( remote.repair.port= ) " "( remote.control.port= ) " "( audio.position= ) " - "( sink.props= { key=val ... } ) " - "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, + "( sink.props= { key=val ... } ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -513,14 +510,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_log_error("can't connect: %m"); goto out; } - if ((str = pw_properties_get(props, "log.level")) != NULL) { - const struct spa_log *log_conf = pw_log_get(); - const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); - if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { - pw_log_error("Invalid log level %s, using default", str); - data->loglevel = default_level; - } - } pw_proxy_add_listener((struct pw_proxy*)data->core, &data->core_proxy_listener, diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c index 2173c6af1..a46189e5d 100644 --- a/src/modules/module-roc-source.c +++ b/src/modules/module-roc-source.c @@ -140,8 +140,6 @@ struct module_roc_source_data { roc_endpoint *local_control_addr; int local_control_port; - - roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -430,8 +428,7 @@ static const struct spa_dict_item module_roc_source_info[] = { "( local.repair.port= ) " "( local.control.port= ) " "( audio.position= ) " - "( source.props= { key=value ... } ) " - "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, + "( source.props= { key=value ... } ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -567,14 +564,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } else { data->fec_code = ROC_FEC_ENCODING_DEFAULT; } - if ((str = pw_properties_get(props, "log.level")) != NULL) { - const struct spa_log *log_conf = pw_log_get(); - const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); - if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { - pw_log_error("Invalid log level %s, using default", str); - data->loglevel = default_level; - } - } data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core); if (data->core == NULL) { diff --git a/src/modules/module-roc/common.c b/src/modules/module-roc/common.c index 244c203dd..1cbd786a8 100644 --- a/src/modules/module-roc/common.c +++ b/src/modules/module-roc/common.c @@ -5,13 +5,45 @@ PW_LOG_TOPIC(roc_log_topic, "mod.roc.lib"); -void pw_roc_log_init(void) +static inline roc_log_level pw_roc_log_level_pw_2_roc(const enum spa_log_level pw_log_level) { - roc_log_set_handler(pw_roc_log_handler, NULL); - roc_log_set_level(pw_roc_log_level_pw_2_roc(roc_log_topic->has_custom_level ? roc_log_topic->level : pw_log_level)); + switch (pw_log_level) { + case SPA_LOG_LEVEL_NONE: + return ROC_LOG_NONE; + case SPA_LOG_LEVEL_ERROR: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_WARN: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_INFO: + return ROC_LOG_INFO; + case SPA_LOG_LEVEL_DEBUG: + return ROC_LOG_DEBUG; + case SPA_LOG_LEVEL_TRACE: + return ROC_LOG_TRACE; + default: + return ROC_LOG_NONE; + } } -void pw_roc_log_handler(const roc_log_message *message, void *argument) +static inline enum spa_log_level pw_roc_log_level_roc_2_pw(const roc_log_level roc_log_level) +{ + switch (roc_log_level) { + case ROC_LOG_NONE: + return SPA_LOG_LEVEL_NONE; + case ROC_LOG_ERROR: + return SPA_LOG_LEVEL_ERROR; + case ROC_LOG_INFO: + return SPA_LOG_LEVEL_INFO; + case ROC_LOG_DEBUG: + return SPA_LOG_LEVEL_DEBUG; + case ROC_LOG_TRACE: + return SPA_LOG_LEVEL_TRACE; + default: + return SPA_LOG_LEVEL_NONE; + } +} + +static void pw_roc_log_handler(const roc_log_message *message, void *argument) { const enum spa_log_level log_level = pw_roc_log_level_roc_2_pw(message->level); if (SPA_UNLIKELY(pw_log_topic_enabled(log_level, roc_log_topic))) { @@ -19,3 +51,8 @@ void pw_roc_log_handler(const roc_log_message *message, void *argument) } } +void pw_roc_log_init(void) +{ + roc_log_set_handler(pw_roc_log_handler, NULL); + roc_log_set_level(pw_roc_log_level_pw_2_roc(roc_log_topic->has_custom_level ? roc_log_topic->level : pw_log_level)); +} diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h index c94ac69a8..d49e392fe 100644 --- a/src/modules/module-roc/common.h +++ b/src/modules/module-roc/common.h @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -21,7 +20,6 @@ #define PW_ROC_STEREO_POSITIONS "[ FL FR ]" void pw_roc_log_init(void); -void pw_roc_log_handler(const roc_log_message *message, void *argument); static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str) { @@ -137,62 +135,4 @@ static inline void pw_roc_fec_encoding_to_proto(roc_fec_encoding fec_code, roc_p } } -static inline roc_log_level pw_roc_log_level_pw_2_roc(const enum spa_log_level pw_log_level) -{ - switch (pw_log_level) { - case SPA_LOG_LEVEL_NONE: - return ROC_LOG_NONE; - case SPA_LOG_LEVEL_ERROR: - return ROC_LOG_ERROR; - case SPA_LOG_LEVEL_WARN: - return ROC_LOG_ERROR; - case SPA_LOG_LEVEL_INFO: - return ROC_LOG_INFO; - case SPA_LOG_LEVEL_DEBUG: - return ROC_LOG_DEBUG; - case SPA_LOG_LEVEL_TRACE: - return ROC_LOG_TRACE; - default: - return ROC_LOG_NONE; - } -} - -static inline enum spa_log_level pw_roc_log_level_roc_2_pw(const roc_log_level roc_log_level) -{ - switch (roc_log_level) { - case ROC_LOG_NONE: - return SPA_LOG_LEVEL_NONE; - case ROC_LOG_ERROR: - return SPA_LOG_LEVEL_ERROR; - case ROC_LOG_INFO: - return SPA_LOG_LEVEL_INFO; - case ROC_LOG_DEBUG: - return SPA_LOG_LEVEL_DEBUG; - case ROC_LOG_TRACE: - return SPA_LOG_LEVEL_TRACE; - default: - return SPA_LOG_LEVEL_NONE; - } -} - -static inline int pw_roc_parse_log_level(roc_log_level *loglevel, const char *str, - roc_log_level default_level) -{ - if (spa_streq(str, "DEFAULT")) - *loglevel = default_level; - else if (spa_streq(str, "NONE")) - *loglevel = ROC_LOG_NONE; - else if (spa_streq(str, "ERROR")) - *loglevel = ROC_LOG_ERROR; - else if (spa_streq(str, "INFO")) - *loglevel = ROC_LOG_INFO; - else if (spa_streq(str, "DEBUG")) - *loglevel = ROC_LOG_DEBUG; - else if (spa_streq(str, "TRACE")) - *loglevel = ROC_LOG_TRACE; - else - return -EINVAL; - return 0; -} - #endif /* MODULE_ROC_COMMON_H */ From d7c3e8c2bc7f8fcef9e42d84b66f57795443a43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20P=C5=91cze?= Date: Thu, 19 Feb 2026 20:24:00 +0100 Subject: [PATCH 82/93] pipewire: module-roc-{sink,source}: fix log format string issues Passing an unknown string as the format string is unsafe, so don't do it. Fixes: b9922d8ed59897 ("module-roc: forward roc-toolkit logs to pipewire logs") --- src/modules/module-roc/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-roc/common.c b/src/modules/module-roc/common.c index 1cbd786a8..475c5f40f 100644 --- a/src/modules/module-roc/common.c +++ b/src/modules/module-roc/common.c @@ -47,7 +47,7 @@ static void pw_roc_log_handler(const roc_log_message *message, void *argument) { const enum spa_log_level log_level = pw_roc_log_level_roc_2_pw(message->level); if (SPA_UNLIKELY(pw_log_topic_enabled(log_level, roc_log_topic))) { - pw_log_logt(log_level, roc_log_topic, message->file, message->line, message->module, message->text, ""); + pw_log_logt(log_level, roc_log_topic, message->file, message->line, message->module, "%s", message->text); } } From e46bfe67b60458e444ee2495209144b49e97d2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20P=C5=91cze?= Date: Thu, 19 Feb 2026 20:56:36 +0100 Subject: [PATCH 83/93] treewide: fix some `-Wdiscarded-qualifiers` Newer glibc versions have made certain `str*()` functions into macros that ensure that the const-ness of the argument is propagated to the return type. --- pipewire-jack/src/pipewire-jack.c | 2 +- spa/plugins/bluez5/bluez5-dbus.c | 7 ++----- spa/plugins/support/logger.c | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index 1bef74283..73627a67e 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -5318,7 +5318,7 @@ int jack_set_freewheel(jack_client_t* client, int onoff) pw_thread_loop_lock(c->context.loop); str = pw_properties_get(c->props, PW_KEY_NODE_GROUP); if (str != NULL) { - char *p = strstr(str, ",pipewire.freewheel"); + const char *p = strstr(str, ",pipewire.freewheel"); if (p == NULL) p = strstr(str, "pipewire.freewheel"); if (p == NULL && onoff) diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 21a5e53de..7dfe45911 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -720,14 +720,12 @@ static const char *bap_features_get_uuid(struct bap_features *feat, size_t i) /** Get feature name at \a i, or NULL if uuid doesn't match */ static const char *bap_features_get_name(struct bap_features *feat, size_t i, const char *uuid) { - char *pos; - if (i >= feat->dict.n_items) return NULL; if (!spa_streq(feat->dict.items[i].value, uuid)) return NULL; - pos = strchr(feat->dict.items[i].key, ':'); + const char *pos = strchr(feat->dict.items[i].key, ':'); if (!pos) return NULL; return pos + 1; @@ -1336,7 +1334,6 @@ static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vendor, uint16_t *product, uint16_t *version) { - char *pos; unsigned int src, i, j, k; if (spa_strstartswith(modalias, "bluetooth:")) @@ -1346,7 +1343,7 @@ static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vend else return -EINVAL; - pos = strchr(modalias, ':'); + const char *pos = strchr(modalias, ':'); if (pos == NULL) return -EINVAL; diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c index c6e6ca4b8..6ea5f31b5 100644 --- a/spa/plugins/support/logger.c +++ b/spa/plugins/support/logger.c @@ -73,7 +73,7 @@ impl_log_logtv(void *object, char timestamp[18] = {0}; char topicstr[32] = {0}; char filename[64] = {0}; - char location[1000 + RESERVED_LENGTH], *p, *s; + char location[1000 + RESERVED_LENGTH], *p; static const char * const levels[] = { "-", "E", "W", "I", "D", "T", "*T*" }; const char *prefix = "", *suffix = ""; int size, len; @@ -118,7 +118,7 @@ impl_log_logtv(void *object, if (impl->line && line != 0) { - s = strrchr(file, '/'); + const char *s = strrchr(file, '/'); spa_scnprintf(filename, sizeof(filename), "[%16.16s:%5i %s()]", s ? s + 1 : file, line, func); } From 5bd93b97adfb5a8352daf8fed1c7eb26dc8d1f6f Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Thu, 19 Feb 2026 11:50:42 -0800 Subject: [PATCH 84/93] Revert "spa: json: Add a helper method to shrink an object string" Drop this until we have better API for building/visiting JSON. This reverts commit c244fbf945a4e240f40bdf0fe6baf0dfc4ddde13. --- spa/include/spa/utils/json.h | 105 ------------------------ spa/plugins/audioconvert/audioconvert.c | 1 - 2 files changed, 106 deletions(-) diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h index 5021290bf..212637dab 100644 --- a/spa/include/spa/utils/json.h +++ b/spa/include/spa/utils/json.h @@ -212,111 +212,6 @@ SPA_API_JSON_UTILS int spa_json_str_array_uint32(const char *arr, size_t arr_len spa_json_make_str_array_unpack(32,uint32_t, atoi); } -/* convenience */ - -#define _SPA_STR_APPEND(str, len, idx, value ) \ -{ \ - if ((idx) >= (len)) \ - return -1; \ - (str)[(idx)++] = (value); \ -} - -static int _spa_json_str_object_reduce(struct spa_json *json, char *out, size_t out_size, const char *value, size_t len) -{ - struct spa_json sub; - size_t idx = 0; - int count = 0, res; - - if (spa_json_is_object(value, len)) { - char key[1024]; - - _SPA_STR_APPEND(out, out_size, idx, '{'); - - spa_json_enter(json, &sub); - while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { - _SPA_STR_APPEND(out, out_size, idx, '"'); - if (idx + strlen(key) >= out_size) - return -1; - strcpy(&out[idx], key); - idx += strlen(key); - _SPA_STR_APPEND(out, out_size, idx, '"'); - _SPA_STR_APPEND(out, out_size, idx, ':'); - - res = _spa_json_str_object_reduce(&sub, &out[idx], out_size - idx, value, len); - if (res < 0) - return res; - - idx += res; - _SPA_STR_APPEND(out, out_size, idx, ','); - count++; - } - - /* Remove trailing comma */ - if (count) - idx--; - _SPA_STR_APPEND(out, out_size, idx, '}'); - } else if (spa_json_is_array(value, len)) { - _SPA_STR_APPEND(out, out_size, idx, '['); - - spa_json_enter(json, &sub); - while ((len = spa_json_next(&sub, &value)) > 0) { - res = _spa_json_str_object_reduce(&sub, &out[idx], out_size - idx, value, len); - if (res < 0) - return res; - - idx += res; - _SPA_STR_APPEND(out, out_size, idx, ','); - count++; - } - - /* Remove trailing comma */ - if (count) - idx--; - _SPA_STR_APPEND(out, out_size, idx, ']'); - } else if (spa_json_is_string(value, len) || - spa_json_is_null(value, len) || - spa_json_is_bool(value, len) || - spa_json_is_int(value, len) || - spa_json_is_float(value, len)) { - /* Object type we understand */ - if (len >= out_size) - return -1; - strcpy(out, value); - idx += len; - } else { - /* Naked value, treat as string */ - _SPA_STR_APPEND(out, out_size, idx, '"'); - if (idx + len >= out_size) - return -1; - strncpy(&out[idx], value, len); - idx += len; - _SPA_STR_APPEND(out, out_size, idx, '"'); - } - - return idx; -} - -/* Parse a JSON object string and strip all whitespaces */ -SPA_API_JSON_UTILS int spa_json_str_object_reduce_inplace(char *str) -{ - struct spa_json json; - size_t size = strlen(str) + 1, len; - char temp[size]; - const char *value; - int res; - - len = spa_json_begin(&json, str, size, &value); - - res = _spa_json_str_object_reduce(&json, temp, size, value, len); - if (res < 0) - return res; - temp[res] = '\0'; - - strncpy(str, temp, size); - - return res; -} - /** * \} */ diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index 5a4153770..da5396f61 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -1442,7 +1442,6 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) insert_graph(&impl->active_graphs, pending); impl->graph_descs[order] = strdup(graph); - spa_json_str_object_reduce_inplace(impl->graph_descs[order]); spa_log_info(impl->log, "loading filter-graph order:%d", order); } From 8f3d8d77abcb3fb81573bf902deee75bc8a7efb4 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 20 Feb 2026 10:12:27 +0100 Subject: [PATCH 85/93] impl-link: prepare a link right after creating it There is no reason to delay preparing the link (by the scheduler) when both nodes are active, we can do that right from the start. This makes things a bit more symetrical because deactivating a node does not unprepare a link. This however changes things a bit because you can no longer delay link prepare until you activate the node. I don't know if this is actually in use and it would probably be to delay format negotiation. The right way do delay format negotiation is to wait until an EnumFormat is set but that is something to improve later. --- src/modules/module-scheduler-v1.c | 4 ---- src/pipewire/impl-link.c | 18 +++--------------- src/pipewire/private.h | 3 --- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c index 22965b257..0be38f572 100644 --- a/src/modules/module-scheduler-v1.c +++ b/src/modules/module-scheduler-v1.c @@ -241,8 +241,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, if (!t->active) continue; - pw_impl_link_prepare(l); - if (!l->prepared) continue; @@ -262,8 +260,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, if (!t->active) continue; - pw_impl_link_prepare(l); - if (!l->prepared) continue; diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index 610a6d620..1c90cedb2 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -969,8 +969,7 @@ static void output_remove(struct pw_impl_link *this) this->output = NULL; } -SPA_EXPORT -int pw_impl_link_prepare(struct pw_impl_link *this) +static int link_prepare(struct pw_impl_link *this) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); @@ -978,9 +977,6 @@ int pw_impl_link_prepare(struct pw_impl_link *this) this, this->prepared, this->preparing, impl->input.node->active, impl->output.node->active, this->passive); - if (!impl->input.node->active || !impl->output.node->active) - return 0; - if (this->destroyed || this->preparing || this->prepared) return 0; @@ -1092,7 +1088,7 @@ static void port_param_changed(struct pw_impl_link *this, uint32_t id, pw_log_info("%p: format changed", this); this->preparing = this->prepared = false; link_update_state(this, PW_LINK_STATE_INIT, 0, NULL); - pw_impl_link_prepare(this); + link_prepare(this); } static void input_port_param_changed(void *data, uint32_t id) @@ -1220,12 +1216,6 @@ static void output_node_result(void *data, int seq, int res, uint32_t type, cons node_result(impl, &impl->output, seq, res, type, result); } -static void node_active_changed(void *data, bool active) -{ - struct impl *impl = data; - pw_impl_link_prepare(&impl->this); -} - static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_impl_node *driver) { struct impl *impl = data; @@ -1240,14 +1230,12 @@ static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_ static const struct pw_impl_node_events input_node_events = { PW_VERSION_IMPL_NODE_EVENTS, .result = input_node_result, - .active_changed = node_active_changed, .driver_changed = node_driver_changed, }; static const struct pw_impl_node_events output_node_events = { PW_VERSION_IMPL_NODE_EVENTS, .result = output_node_result, - .active_changed = node_active_changed, .driver_changed = node_driver_changed, }; @@ -1720,7 +1708,7 @@ int pw_impl_link_register(struct pw_impl_link *link, pw_global_add_listener(link->global, &link->global_listener, &global_events, link); pw_global_register(link->global); - pw_impl_link_prepare(link); + link_prepare(link); return 0; diff --git a/src/pipewire/private.h b/src/pipewire/private.h index 95a1a4646..3aceb3f02 100644 --- a/src/pipewire/private.h +++ b/src/pipewire/private.h @@ -1358,9 +1358,6 @@ int pw_impl_node_set_io(struct pw_impl_node *node, uint32_t id, void *data, size int pw_impl_node_add_target(struct pw_impl_node *node, struct pw_node_target *t); int pw_impl_node_remove_target(struct pw_impl_node *node, struct pw_node_target *t); -/** Prepare a link - * Starts the negotiation of formats and buffers on \a link */ -int pw_impl_link_prepare(struct pw_impl_link *link); /** starts streaming on a link */ int pw_impl_link_activate(struct pw_impl_link *link); From ce18660127798c624f576cd0750dfaeec0ae148b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Fri, 20 Feb 2026 10:34:58 +0100 Subject: [PATCH 86/93] tests: fix test --- test/test-context.c | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-context.c b/test/test-context.c index e35de484c..967c631fb 100644 --- a/test/test-context.c +++ b/test/test-context.c @@ -41,6 +41,7 @@ PWTEST(context_abi) TEST_FUNC(ev, test, global_removed); TEST_FUNC(ev, test, driver_added); TEST_FUNC(ev, test, driver_removed); + TEST_FUNC(ev, test, recalc_graph); pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 2); pwtest_int_eq(sizeof(ev), sizeof(test)); From 9e82e49446d4bf27779c8266356e182a74c3f14b Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Sat, 21 Feb 2026 16:12:42 +0100 Subject: [PATCH 87/93] scheduler: rework the runnable state calculation Move the runnable state calculation out of the collect_nodes function. They are really two different steps that doin't overlap much. The runnable state of a node is very easy to calculate. A node is runnable if it is linked to another node without a passive port. When we find two runnable nodes, make them runnable, which makes all nodes linked to them runnable, stopping at passive ports. We don't have to check the active state of the nodes or links to group them together. This ensures we don't swap nodes around too much when the node or link state changes. --- src/modules/module-scheduler-v1.c | 241 ++++++++++++++++-------------- 1 file changed, 128 insertions(+), 113 deletions(-) diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c index 0be38f572..d1797388c 100644 --- a/src/modules/module-scheduler-v1.c +++ b/src/modules/module-scheduler-v1.c @@ -106,91 +106,134 @@ static int ensure_state(struct pw_impl_node *node, bool running) return pw_impl_node_set_state(node, state); } -/* From a node (that is runnable) follow all prepared links in the given direction - * and groups to active nodes and make them recursively runnable as well. +/* make a node runnable. This will automatically also make all non-passive peer nodes + * runnable and the nodes that belong to the same groups, link_groups or sync groups + * + * We have 4 cases for the links: + * (p) marks a passive port. we don't follow the peer from this port. + * + * A -> B ==> B can also be runnable + * A p-> B ==> B can also be runnable + * A ->p B ==> B can not be runnable + * A p->p B ==> B can not be runnable */ -static inline int run_nodes(struct pw_context *context, struct pw_impl_node *node, - struct spa_list *nodes, enum pw_direction direction, int hop) +static void make_runnable(struct pw_context *context, struct pw_impl_node *node) { - struct pw_impl_node *t; struct pw_impl_port *p; struct pw_impl_link *l; + struct pw_impl_node *n; + uint32_t n_sync = 0; + char *sync[MAX_SYNC+1] = { NULL }; - if (hop == MAX_HOPS) { - pw_log_warn("exceeded hops (%d)", hop); - return -EIO; + if (!node->runnable) { + pw_log_warn("%s is runnable", node->name); + node->runnable = true; } - pw_log_debug("node %p: '%s' direction:%s", node, node->name, - pw_direction_as_string(direction)); - - SPA_FLAG_SET(node->checked, 1u<input_ports, link) { - spa_list_for_each(l, &p->links, input_link) { - t = l->output->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } else { - spa_list_for_each(p, &node->output_ports, link) { - spa_list_for_each(l, &p->links, output_link) { - t = l->input->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } - /* now go through all the nodes that have the same link group and - * that are not yet visited. Note how nodes with the same group - * don't get included here. They were added to the same driver but - * need to otherwise stay idle unless some non-passive link activates - * them. */ - if (node->link_groups != NULL) { - spa_list_for_each(t, nodes, sort_link) { - if (t->exported || !t->active || - SPA_FLAG_IS_SET(t->checked, 1u<sync) { + for (uint32_t i = 0; node->sync_groups[i]; i++) { + if (n_sync >= MAX_SYNC) + break; + if (pw_strv_find(sync, node->sync_groups[i]) >= 0) continue; - if (pw_strv_find_common(t->link_groups, node->link_groups) < 0) + sync[n_sync++] = node->sync_groups[i]; + sync[n_sync] = NULL; + } + } + spa_list_for_each(p, &node->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + n = l->input->node; + if (!l->prepared || !n->active || l->input->passive) + continue; + if (!n->runnable) + make_runnable(context, n); + } + } + spa_list_for_each(p, &node->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + n = l->output->node; + if (!l->prepared || !n->active || l->output->passive) + continue; + if (!n->runnable) + make_runnable(context, n); + } + } + /* now go through all the nodes that share groups, link_groups or + * sync groups that are not yet runnable */ + if (node->groups != NULL || node->link_groups != NULL || sync[0] != NULL) { + spa_list_for_each(n, &context->node_list, link) { + if (n->exported || !n->active || n->runnable) + continue; + /* the other node will be scheduled with this one if it's in + * the same group or link group */ + if (pw_strv_find_common(n->groups, node->groups) < 0 && + pw_strv_find_common(n->link_groups, node->link_groups) < 0 && + pw_strv_find_common(n->sync_groups, sync) < 0) continue; - pw_log_debug(" group %p: '%s'", t, t->name); - t->runnable = true; - if (!t->driving) - run_nodes(context, t, nodes, direction, hop + 1); + make_runnable(context, n); } } - return 0; } -/* Follow all prepared links and groups from node, activate the links. - * If a non-passive link is found, we set the peer runnable flag. +/* check if a node and its peer can run. They can both run if there is a non-passive + * link between them. + * + * There are 4 cases: + * + * (p) marks a passive port. we don't follow the peer from this port. + * A can not be a driver + * + * A -> B ==> both nodes can run + * A ->p B ==> both nodes can run + * A p-> B ==> nodes don't run, port A is passive and doesn't activate B + * A p->p B ==> nodes don't run + * + * Once we decide the two nodes should be made runnable we cann make_runnable() + * + * */ +static void check_runnable(struct pw_context *context, struct pw_impl_node *node) +{ + struct pw_impl_port *p; + struct pw_impl_link *l; + struct pw_impl_node *n; + + spa_list_for_each(p, &node->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + n = l->input->node; + /* we can only check the peer when the link is ready and + * the peer is active */ + if (!l->prepared || !n->active) + continue; + + if (!p->passive) { + make_runnable(context, node); + make_runnable(context, n); + } + } + } + spa_list_for_each(p, &node->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + n = l->output->node; + if (!l->prepared || !n->active) + continue; + + if (!p->passive) { + make_runnable(context, node); + make_runnable(context, n); + } + } + } +} + +/* Follow all links and groups from node. * * After this is done, we end up with a list of nodes in collect that are all * linked to node. - * Some of the nodes have the runnable flag set. We then start from those nodes - * and make all linked nodes and groups runnable as well. (see run_nodes). * - * This ensures that we only activate the paths from the runnable nodes to the - * driver nodes and leave the other nodes idle. + * We don't need to care about active nodes or links, we just follow and group everything. + * The inactive nodes or links will simply not be runnable but will already be grouped + * correctly when they do become active and prepared. */ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect) { @@ -237,16 +280,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, spa_list_for_each(p, &n->input_ports, link) { spa_list_for_each(l, &p->links, input_link) { t = l->output->node; - - if (!t->active) - continue; - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - if (!t->visited) { t->visited = true; spa_list_append(&queue, &t->sort_link); @@ -256,16 +289,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, spa_list_for_each(p, &n->output_ports, link) { spa_list_for_each(l, &p->links, output_link) { t = l->input->node; - - if (!t->active) - continue; - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - if (!t->visited) { t->visited = true; spa_list_append(&queue, &t->sort_link); @@ -276,7 +299,7 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, * that are not yet visited */ if (n->groups != NULL || n->link_groups != NULL || sync[0] != NULL) { spa_list_for_each(t, &context->node_list, link) { - if (t->exported || !t->active || t->visited) + if (t->exported || t->visited) continue; /* the other node will be scheduled with this one if it's in * the same group or link group */ @@ -294,26 +317,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, n->groups, n->link_groups, sync); } - /* All non-driver runnable nodes (ie. reachable with a non-passive link) now make - * all linked nodes up and downstream runnable as well */ - spa_list_for_each(n, collect, sort_link) { - if (!n->driver && n->runnable) { - run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); - run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); - } - } - /* now we might have made a driver runnable, if the node is not runnable at this point - * it means it was linked to the driver with passives links and some other node - * made the driver active. If the node is a leaf it can not be activated in any other - * way and we will also make it, and all its peers, runnable */ - spa_list_for_each(n, collect, sort_link) { - if (!n->driver && n->driver_node->runnable && !n->runnable && n->leaf && n->active) { - n->runnable = true; - run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); - run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); - } - } - return 0; } @@ -527,16 +530,18 @@ static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t /* here we evaluate the complete state of the graph. * - * It roughly operates in 3 stages: + * It roughly operates in 4 stages: * - * 1. go over all drivers and collect the nodes that need to be scheduled with the + * 1. go over all nodes and check if they should be scheduled (runnable) or not. + * + * 2. go over all drivers and collect the nodes that need to be scheduled with the * driver. This include all nodes that have an active link with the driver or * with a node already scheduled with the driver. * - * 2. go over all nodes that are not assigned to a driver. The ones that require - * a driver are moved to some random active driver found in step 1. + * 3. go over all nodes that are not assigned to a driver. The ones that require + * a driver are moved to some random active driver found in step 2. * - * 3. go over all drivers again, collect the quantum/rate of all followers, select + * 4. go over all drivers again, collect the quantum/rate of all followers, select * the desired final value and activate the followers and then the driver. * * A complete graph evaluation is performed for each change that is made to the @@ -571,6 +576,16 @@ again: global_force_quantum = rate_quantum == 0; + /* first look at all nodes and decide which one should be runnable */ + spa_list_for_each(n, &context->node_list, link) { + /* we don't check drivers, they need to be made runnable + * from other nodes */ + if (n->exported || !n->active || n->driver) + continue; + + check_runnable(context, n); + } + /* start from all drivers and group all nodes that are linked * to it. Some nodes are not (yet) linked to anything and they * will end up 'unassigned' to a driver. Other nodes are drivers From 973f48dde74f1be4b84bec2f48a4ad33e7543762 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Sat, 21 Feb 2026 16:32:30 +0100 Subject: [PATCH 88/93] scheduler: make always_process nodes runnable Don't just set the flag but call make_runnable for always_process node so that the peers also get activated. --- src/modules/module-scheduler-v1.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c index d1797388c..8bfbab21c 100644 --- a/src/modules/module-scheduler-v1.c +++ b/src/modules/module-scheduler-v1.c @@ -126,7 +126,7 @@ static void make_runnable(struct pw_context *context, struct pw_impl_node *node) char *sync[MAX_SYNC+1] = { NULL }; if (!node->runnable) { - pw_log_warn("%s is runnable", node->name); + pw_log_debug("%s is runnable", node->name); node->runnable = true; } @@ -198,6 +198,9 @@ static void check_runnable(struct pw_context *context, struct pw_impl_node *node struct pw_impl_link *l; struct pw_impl_node *n; + if (node->always_process && !node->runnable) + make_runnable(context, node); + spa_list_for_each(p, &node->output_ports, link) { spa_list_for_each(l, &p->links, output_link) { n = l->input->node; @@ -567,7 +570,7 @@ again: spa_list_for_each(n, &context->node_list, link) { n->visited = false; n->checked = 0; - n->runnable = n->always_process && n->active; + n->runnable = false; } get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &rate_quantum, From 476220c18b93912b5cbddee3f841df7dd865de8a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Sat, 21 Feb 2026 16:33:21 +0100 Subject: [PATCH 89/93] scheduler: don't take active state into account for grouping The grouping of the node does not depend on the active state. --- src/modules/module-scheduler-v1.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c index 8bfbab21c..7f9501073 100644 --- a/src/modules/module-scheduler-v1.c +++ b/src/modules/module-scheduler-v1.c @@ -266,9 +266,6 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, pw_log_debug(" next node %p: '%s' runnable:%u active:%d", n, n->name, n->runnable, n->active); - if (!n->active) - continue; - if (n->sync) { for (uint32_t i = 0; n->sync_groups[i]; i++) { if (n_sync >= MAX_SYNC) @@ -585,7 +582,6 @@ again: * from other nodes */ if (n->exported || !n->active || n->driver) continue; - check_runnable(context, n); } From 846096d435c1199415b8f54508953edb19dcc9ae Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Sat, 21 Feb 2026 17:25:54 +0100 Subject: [PATCH 90/93] scheduler: prepare link before usage A link might become unprepared because a node suspended. When we want to make the nodes runnable, make sure the link is prepared again. --- src/modules/module-scheduler-v1.c | 29 +++++++++++++++-------------- src/pipewire/impl-link.c | 7 ++++--- src/pipewire/private.h | 3 +++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c index 7f9501073..215ef0fef 100644 --- a/src/modules/module-scheduler-v1.c +++ b/src/modules/module-scheduler-v1.c @@ -204,27 +204,28 @@ static void check_runnable(struct pw_context *context, struct pw_impl_node *node spa_list_for_each(p, &node->output_ports, link) { spa_list_for_each(l, &p->links, output_link) { n = l->input->node; - /* we can only check the peer when the link is ready and - * the peer is active */ - if (!l->prepared || !n->active) + /* the peer needs to be active and we are linked to it + * with a non-passive link */ + if (!n->active || p->passive) continue; - - if (!p->passive) { - make_runnable(context, node); - make_runnable(context, n); - } + /* explicitly prepare the link in case it was suspended */ + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + make_runnable(context, node); + make_runnable(context, n); } } spa_list_for_each(p, &node->input_ports, link) { spa_list_for_each(l, &p->links, input_link) { n = l->output->node; - if (!l->prepared || !n->active) + if (!n->active || p->passive) continue; - - if (!p->passive) { - make_runnable(context, node); - make_runnable(context, n); - } + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + make_runnable(context, node); + make_runnable(context, n); } } } diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index 1c90cedb2..ea979d8ba 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -969,7 +969,8 @@ static void output_remove(struct pw_impl_link *this) this->output = NULL; } -static int link_prepare(struct pw_impl_link *this) +SPA_EXPORT +int pw_impl_link_prepare(struct pw_impl_link *this) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); @@ -1088,7 +1089,7 @@ static void port_param_changed(struct pw_impl_link *this, uint32_t id, pw_log_info("%p: format changed", this); this->preparing = this->prepared = false; link_update_state(this, PW_LINK_STATE_INIT, 0, NULL); - link_prepare(this); + pw_impl_link_prepare(this); } static void input_port_param_changed(void *data, uint32_t id) @@ -1708,7 +1709,7 @@ int pw_impl_link_register(struct pw_impl_link *link, pw_global_add_listener(link->global, &link->global_listener, &global_events, link); pw_global_register(link->global); - link_prepare(link); + pw_impl_link_prepare(link); return 0; diff --git a/src/pipewire/private.h b/src/pipewire/private.h index 3aceb3f02..95a1a4646 100644 --- a/src/pipewire/private.h +++ b/src/pipewire/private.h @@ -1358,6 +1358,9 @@ int pw_impl_node_set_io(struct pw_impl_node *node, uint32_t id, void *data, size int pw_impl_node_add_target(struct pw_impl_node *node, struct pw_node_target *t); int pw_impl_node_remove_target(struct pw_impl_node *node, struct pw_node_target *t); +/** Prepare a link + * Starts the negotiation of formats and buffers on \a link */ +int pw_impl_link_prepare(struct pw_impl_link *link); /** starts streaming on a link */ int pw_impl_link_activate(struct pw_impl_link *link); From 367ce4626c98d0262c10af05336e1b75d00143d5 Mon Sep 17 00:00:00 2001 From: Ripley Tom Date: Sat, 21 Feb 2026 20:55:17 +0100 Subject: [PATCH 91/93] src/modules/module-rtp-source.c: Fix alignment requirement for 32 bit build --- src/modules/module-rtp-source.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 8f911c62a..2110385b8 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -231,7 +231,7 @@ struct impl { /* Monotonic timestamp of the last time a packet was * received. This is accessed with atomic accessors * to avoid race conditions. */ - uint64_t last_packet_time; + SPA_ALIGNED(8) uint64_t last_packet_time; struct pw_timer standby_timer; /* This timer is used when the first stream_start() call fails because From c847b8162959c29b783585e0dcadbfb096e7cb73 Mon Sep 17 00:00:00 2001 From: Ripley Tom Date: Sat, 21 Feb 2026 19:33:11 +0100 Subject: [PATCH 92/93] spa/plugins/alsa/acp/compat.h: Fix missed -Wdiscarded-qualifiers warning --- spa/plugins/alsa/acp/compat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h index f7592e1a6..0f7b959df 100644 --- a/spa/plugins/alsa/acp/compat.h +++ b/spa/plugins/alsa/acp/compat.h @@ -429,9 +429,9 @@ static PA_PRINTF_FUNC(1,0) inline char *pa_vsprintf_malloc(const char *fmt, va_l #define pa_fopen_cloexec(f,m) fopen(f,m"e") -static inline char *pa_path_get_filename(const char *p) +static inline const char *pa_path_get_filename(const char *p) { - char *fn; + const char *fn; if (!p) return NULL; if ((fn = strrchr(p, PA_PATH_SEP_CHAR))) From ff04b47942809e910d07858d5bd9c937e5c48bba Mon Sep 17 00:00:00 2001 From: Ripley Tom Date: Sat, 21 Feb 2026 20:19:04 +0100 Subject: [PATCH 93/93] meson.build: Add -Werror=discarded-qualifiers --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index 5f9ffa39d..bca2c7a27 100644 --- a/meson.build +++ b/meson.build @@ -116,6 +116,7 @@ cc_flags = common_flags + [ '-Werror=old-style-definition', '-Werror=missing-parameter-type', '-Werror=strict-prototypes', + '-Werror=discarded-qualifiers', ] add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') add_project_arguments(cc_native.get_supported_arguments(cc_flags),