diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index ed82b2f41..9cf3943ed 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@ monitor-prop 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. @@ -1202,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/doc/dox/internals/dma-buf.dox b/doc/dox/internals/dma-buf.dox index 273be930e..5042f285c 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 @@ -364,12 +365,15 @@ 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, "true"), + SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_ID_NEGOTIATION, "1"), SPA_DICT_ITEM(PW_CAPABILITY_DEVICE_IDS, device_ids))); ``` 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), 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 diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 5c4a2b785..4d14183e7 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, @@ -112,6 +115,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 +301,33 @@ static const struct media_codec *codec_list_best(struct impl *backend, struct sp return NULL; } +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)); + codecs = (void *)buffer; + if (msbc) + codecs->codecs[0].id = BT_CODEC_MSBC; + else + codecs->codecs[0].id = BT_CODEC_CVSD; + codecs->num_codecs = 1; + codecs->codecs[0].data_path_id = backend->hfphsp_sco_datapath; + + err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); + if (err < 0) + spa_log_error(backend->log, "ERROR: %s (%d)", strerror(errno), errno); + else + spa_log_info(backend->log, "set offload codec succeeded"); +} + static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) { if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) @@ -2595,6 +2626,8 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte } } + sco_offload_btcodec(backend, sock, transparent); + return spa_steal_fd(sock); } @@ -4101,6 +4134,14 @@ 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) +{ + backend->hfphsp_sco_datapath = HFP_SCO_DEFAULT_DATAPATH; + + spa_atou32(spa_dict_lookup(info, "bluez5.hw-offload-datapath"), + &backend->hfphsp_sco_datapath, 10); +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_native_free, @@ -4163,6 +4204,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/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[] = { diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 926b1b3c4..a17eedc84 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]; }; /* @@ -3102,11 +3104,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); @@ -6242,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); @@ -6987,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]; @@ -7022,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, 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) goto parse_failed; diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 3efec465a..afc56c920 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 */ diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index 9271ace34..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]; @@ -193,6 +195,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 +262,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); @@ -339,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; @@ -433,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) { @@ -447,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; @@ -458,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); @@ -565,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) @@ -706,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; } @@ -925,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); } @@ -936,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; @@ -990,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++) { @@ -1020,17 +1046,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); - 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; @@ -1410,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]; @@ -1784,7 +1798,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; @@ -1796,19 +1810,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 */ @@ -1816,16 +1819,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) @@ -1836,12 +1834,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 != " @@ -2029,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; @@ -2083,6 +2081,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 +2231,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/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index 8a964ccd7..3bcde30c9 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,136 @@ 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, +}; + +/* 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) { @@ -3180,6 +3311,10 @@ static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) return &zeroramp_desc; case 30: return &noisegate_desc; + case 31: + return &busy_desc; + case 32: + return &null_desc; } return NULL; } diff --git a/spa/plugins/filter-graph/plugin_sofa.c b/spa/plugins/filter-graph/plugin_sofa.c index 7ec73ea2b..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"); @@ -168,11 +177,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; @@ -183,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)); @@ -250,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 944ed6205..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 @@ -118,8 +127,32 @@ context.modules = [ } } - { type = builtin label = mixer name = mixL } - { type = builtin label = mixer name = mixR } + { type = builtin label = mixer name = mixL + control = { + # 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 = { + # 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 + } + } ] links = [ # output 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 6fdc561e6..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; } } @@ -438,8 +448,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 +798,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..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 @@ -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; } } @@ -783,23 +784,26 @@ 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 */ 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 */ 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; diff --git a/src/modules/meson.build b/src/modules/meson.build index 0605900e5..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 @@ -573,6 +569,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 +617,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 +631,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 diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index eb6a6c8ef..64a92c5ac 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. @@ -652,6 +656,40 @@ 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 + * + * ### Null + * + * The `null` plugin has one data input "In" and one control input "Control" that + * simply discards the data. * * ## SOFA filters * @@ -679,6 +717,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * blocksize = ... * tailsize = ... * filename = ... + * gain = ... * } * control = { * "Azimuth" = ... @@ -695,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 @@ -1215,92 +1255,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) @@ -1369,6 +1422,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); @@ -1425,10 +1481,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, @@ -1505,8 +1564,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) @@ -1565,7 +1623,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)); } @@ -1594,43 +1652,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) @@ -1653,7 +1707,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; } @@ -1662,32 +1716,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: @@ -1711,20 +1777,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 +1795,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); } } @@ -1760,7 +1833,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); } 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/client.h b/src/modules/module-protocol-pulse/client.h index 2c413e51c..81d790b51 100644 --- a/src/modules/module-protocol-pulse/client.h +++ b/src/modules/module-protocol-pulse/client.h @@ -62,8 +62,12 @@ 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..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@" @@ -324,5 +326,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/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); } diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index 48c7c7be3..b44d4f422 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -110,14 +110,59 @@ 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"); + fprintf(response, "Value must be true, false, or null"); + 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_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 null"); return -EINVAL; } @@ -138,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; @@ -208,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 c22499718..59610ef57 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -973,12 +973,33 @@ 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 (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"); } } 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/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/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; diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 13873f29c..d20e9a37c 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; @@ -61,6 +70,9 @@ static void rtp_audio_process_playback(void *data) * read or write index itself.) */ 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 * here. The code in rtp_audio_receive() writes to the ring buffer at @@ -89,22 +101,32 @@ 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) { - /* 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; + 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 { /* In the unlikely case that no spa_io_position pointer * was passed yet by PipeWire to this node, resort to a @@ -112,26 +134,72 @@ 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); + read_index = timestamp; } - 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. + * 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); - /* 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); + /* 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, + 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 @@ -222,6 +290,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); } @@ -334,17 +421,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); } } @@ -426,20 +539,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); @@ -500,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 */ @@ -517,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; @@ -533,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; @@ -569,7 +707,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; 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"); } 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: 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 */ 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); diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index bdd44a255..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; @@ -1700,17 +1700,42 @@ 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) 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