Compare commits

..

44 commits

Author SHA1 Message Date
Pauli Virtanen
84bfbd92a1 bluez5: bap: prefer high-reliability qos for 44.1/48 kHz
On some device combinations (MT7925 / Sony LinkBuds S) the low-latency
48 kHz QoS crackle.

It's probably better to default to high-reliability for those, until we
have proper quality vs. latency configuration in place.
2026-02-03 19:10:25 +02:00
Hugo Osvaldo Barrera
57efceeb02 Implement socket activation without libsystemd
Socket activation uses sd_listen_fds from libsystemd, and can only be
compiled on systems with systemd.

This is an issue for Alpine / postmarketOS, where upstream has no
systemd package, but downstream depends on upstream's pipewire package
and wants to rely on socket activation. This also prevents using
socket-activation on other non-systemd distributions, including
non-Linux.

Implement equivalent functionality without a dependency on libsystemd.
2026-02-03 13:17:59 +01:00
Wim Taymans
2c0988ab4c bluez-dbus: fix adapter memcpy length
sizeof(adapter) is larger than the big_entry->adapter and so the code
would copy too much. Instead only copy the strlen of the parsed
adapter, which we checked above to be smaller than the available size.

This doesn't copy the 0 byte because the memory is assumed to be 0
filled already by the calloc.

If the address is exactly the HCI_DEV_NAME_LEN, it will result in a non-0
terminated string, which may or may not be a problem...
2026-02-03 10:16:22 +01:00
Wim Taymans
ac7728097f snapcast: support newer snapcast service type
Newer snapcast servers publish the service as _snapcast-ctrl._tcp so
listen for that as well.

Fixes #5104
2026-02-03 10:01:38 +01:00
Carlos Rafael Giani
642504f5f9 module-rtp: Compensate for stream resampler effects in RTP sink node 2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
6192c11038 module-rtp: Convert clock pos to RTP time in RTP source and factor in ASRC
This can easily be overlooked if the RTP rate equals the clock rate, which
is fairly common (for example, RTP rate and clock rate both being 48 kHz).

And, if an ASRC is active, and converting the output of the RTP source
node, the resampler's delay need to be taken into the account as well.
2026-02-03 09:07:48 +01:00
Stanislav Ruzani
e874f705a9 module-rtp: clear ringbuffer when stream stops to prevent old packets
Clear the ringbuffer in stream_stop() when processing stops to prevent old invalid packets
from being sent when processing resumes via rtp_audio_flush_packets().

This ensures a clean state when the stream restarts.
2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
a16485f8aa module-rtp: Extract common RTP code into static library for better reuse 2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
5d7f21f130 module-rtp: Improve rtp_audio_flush_packets() logging 2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
9f0dc9c1af module-rtp: Update ringbuffer indices upon resync with proper API calls 2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
543000151f module-rtp: Handle unsigned 32-bit integer overflow in constant delay mode 2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
413f5762c4 module-rtp: Clear ringbuffer in constant delay mode
Clearing the ring buffer is important not only in the direct timestamp
mode, but also in the constant delay mode, since missed packets can lead
to gaps in the ring buffer. These gaps may have stale data inside if the
ringbuffer is not cleared after reading from it.
2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
95970e539e module-rtp: Fix invalid ring buffer read attempts in direct timestamp mode
In corner cases where the read and write pointers are very close, it may
not be possible to read out all the wanted samples. This can for example
happen when there is a jump in the graph driver position. Currently, the
code reads the wanted number of samples out of the ring buffer regardless
of the write and read pointer positions. It does so even when the read
pointer is ahead of the write pointer (that is, an underrun occurs).

Fix this by checking the fill level and reading only the available amount
of samples if that amount is less than the wanted amount. The remaining
space in the target buffer is then filled with nullbytes.
2026-02-03 09:07:48 +01:00
Carlos Rafael Giani
a32e6e108c Revert "module-rtp: Remove device_delay from timestamp math"
This reverts commit dcdc19238b.

Reverting this because it caused big sync errors of ~62 ms in test setups.
Further discussions about this can be found here:

https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2666

Followup commits modify the device delay application (by scaling it),
which is another reason why this needs to be reverted.
2026-02-03 09:07:36 +01:00
Barnabás Pőcze
cad1df748e meson.build: define SPA_AUDIO_MAX_CHANNELS for C++ as well
Previously the override was only present in `cc_flags`, meaning that
C++ source files, like `aec-webrtc.cpp`, would not have it.

Fixes: 6d74eee874 ("spa: bump channels to 128 again")
2026-02-02 18:13:14 +01:00
Wim Taymans
3c80f0fb3e filter-graph: add gain option to sofa
So that we can apply the gain to the IR. This is more efficient than
doing a volume after the convolution.

See #5098
2026-02-02 16:28:54 +01:00
Pauli Virtanen
f60e03b4ef pulse-server: use timeout also for creating sample-play streams
Add 35 sec timeout for PLAY_SAMPLE streams to start streaming, similar
to what we do with normal streams, and fail playback if they don't
start.

This avoids pending sample playback using up resources indefinitely if
streams fail to start for some reason, e.g. session manager is not
linking them.
2026-02-02 11:25:01 +00:00
Pauli Virtanen
a50e9a995e pulse-server: disconnect from server on EPROTO
If we get EPROTO, we likely have missed on some messages from the
server, and our state is now out of sync.

It's likely we can't recover (e.g. if error is due to fd limit hit), so
just drop the server connection in this case, similarly as if we got
EPIPE.
2026-02-02 11:16:15 +00:00
Alexander Sarmanow
f34a87fe38 bluez5: bap: add support for per adapter broadcast config
By setting the hci handle (e.g. hci0) of the desired adapter, the BIG
config will only applied on that adapter. In case no "adapter" entry in
the config is given, it will be applied on all adapters.

Signed-off-by: Alexander Sarmanow <a.sarmanow@televic.com>
2026-02-02 11:15:41 +00:00
Frédéric Danis
1a478c7147 bluez5: Fix stack smashing crash in remote_endpoint_update_props()
Commit 2942bae034 introduced parsing of
"SupportedFeatures" which uses a third DBusMessageIter pointer.

*** stack smashing detected ***: terminated
==389050==
==389050== Process terminating with default action of signal 6 (SIGABRT)
==389050==    at 0x4F57B2C: __pthread_kill_implementation (pthread_kill.c:44)
==389050==    by 0x4F57B2C: __pthread_kill_internal (pthread_kill.c:78)
==389050==    by 0x4F57B2C: pthread_kill@@GLIBC_2.34 (pthread_kill.c:89)
==389050==    by 0x4EFE27D: raise (raise.c:26)
==389050==    by 0x4EE18FE: abort (abort.c:79)
==389050==    by 0x4EE27B5: __libc_message_impl.cold (libc_fatal.c:134)
==389050==    by 0x4FEFC48: __fortify_fail (fortify_fail.c:24)
==389050==    by 0x4FF0ED3: __stack_chk_fail (stack_chk_fail.c:24)
==389050==    by 0xBC1D1A1: remote_endpoint_update_props (bluez5-dbus.c:3137)
==389050==    by 0xB53609F: ???
==389050==    by 0x1DF: ???
==389050==    by 0x61C17BF: ??? (in /usr/lib/x86_64-linux-gnu/libdbus-1.so.3.32.4)
==389050==    by 0x1DF: ???
==389050==    by 0xC5ED113: ???
2026-01-30 18:02:38 +01:00
Wim Taymans
69d8822303 filter-chain: tweak spatializer gain
We're adding two signals together so half the gain to keep it at
the same volume.
2026-01-29 16:52:02 +01:00
Wim Taymans
47dd57faa7 filter-graph: handle other SOFA errors as errno 2026-01-29 16:50:52 +01:00
Wim Taymans
2aecb49f50 pulse-server: use null to clear the value
The message makes it seem that you can pass 'clear' to clear the setting
but in fact you should pass 'null'.
2026-01-27 10:17:34 +01:00
Arun Raghavan
ed59342d28 pipewire-pulse: Expose bluetooth headset autoswitch config as a message
Makes it easier for libpulse-based clients to modify this setting if
they want.
2026-01-26 14:44:30 -08:00
Arun Raghavan
703380d62d pulse-server: Fix querying after setting of mono mixdown 2026-01-26 14:37:08 -08:00
Mengshi Wu
bc53b6b343 bluez5: Remove unused bt_features variable in sco_create_socket. 2026-01-25 17:26:15 +00:00
Mengshi Wu
332e35039d doc: Fix bluez5.hw-offload-datapath property type in documentation 2026-01-25 17:26:15 +00:00
Mengshi Wu
254620676f bluez5: Use named constants for Bluetooth codec IDs
Replace magic numbers (0x02, 0x05) with named constants BT_CODEC_CVSD
and BT_CODEC_MSBC for better code readability. Also remove redundant
zero initialization of num_caps field since the buffer is already
memset to zero.
2026-01-25 17:26:15 +00:00
Mengshi Wu
78f16bc04b bluez5: Remove hw-offload feature flag check and associated quirks
The sco_offload_btcodec() function now returns void and only skips
offload setup when using the default datapath, simplifying the logic
and removing the need for explicit feature flag checks.
2026-01-25 17:26:15 +00:00
Mengshi Wu
2b5d21da5b bluez5: simplify SCO datapath parsing with spa_atou32 2026-01-25 17:26:15 +00:00
Mengshi Wu
2d6a7d2186 bluez5: fix format string in sco_offload_btcodec log message 2026-01-25 17:26:15 +00:00
Mengshi Wu
db7c74a042 bluez5/backend-native: Add HFP/HSP hardware offload datapath configuration
Add support for configuring the SCO hardware offload data path for
HFP/HSP profiles using the Bluetooth SIG-specified procedure. This
enables vendor-specific SCO offload integrations.

Changes:
- Add `bluez5.hw-offload-datapath` configuration property (default: 0)
- Implement `sco_offload_btcodec()` to set BT_CODEC socket option
- Add `SPA_BT_FEATURE_HW_OFFLOAD` quirk feature flag
- Apply offload configuration when creating SCO sockets if quirk enabled
- Document new property in pipewire-props.7.md

The datapath ID is configurable via device parameters and only applied
when the hardware offload feature flag is set in quirks, allowing
platform-specific SCO offload implementations.
2026-01-25 17:26:15 +00:00
Wim Taymans
35817c0d85 pw-cat: support some more formats
So that you can give an .oga extension and --format=opus to get an
opus ogg file.
2026-01-22 17:14:41 +01:00
Jonas Ådahl
f2c4452e8d stream: Make capability device IDs an JSON object
Also change the encoding of the byte array to use hexadecimal encoding,
i.e. the byte array [100, 200] in becomes "64c8".
2026-01-22 16:08:19 +00:00
Jonas Ådahl
8600721de0 stream: Change PW_CAPABILITY_DEVICE_ID_NEGOTIATION to a version number
This allows easier evolvement of the negotiation protocol.
2026-01-22 16:08:19 +00:00
Arun Raghavan
04793138b5 alsa-card-profiles: Add config for a couple of JBL gaming headsets
Similar to the existing SteelSeries and Logitech ones, but the order of
the playback endpoints is reversed, and only mono input is supported.
2026-01-22 15:35:05 +00:00
Wim Taymans
f978b702b1 pw-cat: also look at simple formats for extension
The simple formats contain some common mappings for other extensions such
as mp3.

Makes pw-record test.mp3 actually write an mp3 instead of a wav file.
2026-01-22 13:08:06 +01:00
Wim Taymans
f82f4945b9 filter-graph: improve min/max checks for control
Clamp the control values to their min/max.

Remove the default_control array, we can just restore the default with
the port_set_control_value(). Do this when no value has been set on the
port when we set up the graph. The avantage is that we calculate the
default, min and max correctly when they depend on the graph rate.

Fixes #5088
2026-01-21 18:35:36 +01:00
Sven Ulland
c634ef9610 pipewiresrc: Fix video crop metadata not being set
Since commit a1f33a99df changed buffer handling to create new GstBuffers
instead of reusing pool buffers, the video crop metadata was silently
lost. The code used gst_buffer_get_video_crop_meta() which returns NULL
on a fresh buffer, so the crop values from PipeWire were never applied.

Change to gst_buffer_add_video_crop_meta() to actually attach the
metadata to the buffer.

Also remove the now-obsolete call in gst_pipewire_pool_wrap_buffer.

This was discovered when using the XDG Desktop Portal's RemoteDesktop
interface: the full desktop was being delivered instead of just the
selected window, because the crop region metadata was not being
propagated to the GStreamer buffer.

Fixes: a1f33a99df ("gst: dequeue a shared buffer instead of original
pool buffer"), from merge request !1258

CC: @jameshilliard
2026-01-21 15:26:57 +00:00
Wim Taymans
56a4ab5234 filter-chain: support no input or output streams
When the graph has no inputs and the channels is set to 0, don't create
a capture stream. Likewise, don't create a playback stream when there
are no graph outputs and the output channels is 0.

You can use this to make a sine source or a null sink.
2026-01-21 16:26:16 +01:00
Wim Taymans
7f2cce1021 filter-graph: add a null plugin
It discards all input.
2026-01-21 16:26:16 +01:00
Wim Taymans
a97c4d10af filter-graph: allow 0 input and output ports
There is no reason to fail when there is no input or output port.
We can simply run the graph with what there is. Even if there is no
input or output at all, running one instance of the plugins is
possible.

Add a busy builtin plugin that has no ports and keeps the CPU IDLE or
busy for the give percent.
2026-01-21 16:26:16 +01:00
Wim Taymans
ba3e564e34 filter-graph: notify about default numner of in/out
The filter graph has, after parsing, a default number of input and
output ports. This is based on the description or the first/last element
input and output ports. Pass this information in the properties when
we emit the info.

Don't use the number of configured input/output ports as the default
number of channels in filter-chain because this is only determined after
activating the graph. Instead, use the default input/output channels.

The result is that when you load filter-chain without any channel
layout, it will default to the number of input/outputs of the graph
instead of 0. This allows for the node to be visible in the pulseaudio
API.

Fixes #5084
2026-01-21 16:26:16 +01:00
Jonas Holmberg
39dd760c60 impl-port: Free capabilities
Free capabilities when destroying port.
2026-01-20 15:21:56 +01:00
37 changed files with 1256 additions and 443 deletions

View file

@ -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 hardwareoffload data path for HFP/HSP using the Bluetooth
SIGspecified 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:
```

View file

@ -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)));
```

View file

@ -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),

View file

@ -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"

View file

@ -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 <http://www.gnu.org/licenses/>.
; 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

View file

@ -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,

View file

@ -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[] = {

View file

@ -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;

View file

@ -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 */

View file

@ -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;
}

View file

@ -14,6 +14,7 @@
#include <sys/wait.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <time.h>
#include <spa/utils/json.h>
#include <spa/utils/result.h>
@ -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;
}

View file

@ -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,

View file

@ -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

View file

@ -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;
}

60
src/examples/utils.h Normal file
View file

@ -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;
}

View file

@ -29,7 +29,7 @@
#include <pipewire/pipewire.h>
#include <pipewire/capabilities.h>
#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),

View file

@ -30,7 +30,7 @@
#include <pipewire/pipewire.h>
#include <pipewire/capabilities.h>
#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 */

View file

@ -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));

View file

@ -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;

View file

@ -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

View file

@ -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 **)&params[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);
}

View file

@ -34,10 +34,6 @@
#include <spa/utils/json.h>
#include <spa/debug/log.h>
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#endif
#ifdef HAVE_SELINUX
#include <selinux/selinux.h>
#endif
@ -45,6 +41,7 @@
#include <pipewire/impl.h>
#include <pipewire/extensions/protocol-native.h>
#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;

View file

@ -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;

View file

@ -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 */

View file

@ -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);
}

View file

@ -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 <command> [<params>]\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 <params>\n"
" pipewire-pulse:list-modules list all module names\n"
" pipewire-pulse:describe-module describe module info for <params>\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 <params>\n"
" pipewire-pulse:list-modules list all module names\n"
" pipewire-pulse:describe-module describe module info for <params>\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;
}

View file

@ -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");
}
}

View file

@ -17,10 +17,12 @@
#include <pipewire/properties.h>
#include <pipewire/stream.h>
#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);
}

View file

@ -11,6 +11,8 @@
#include <spa/utils/list.h>
#include <spa/utils/hook.h>
#include <pipewire/pipewire.h>
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;
};

View file

@ -21,9 +21,6 @@
#include <netinet/ip.h>
#include <unistd.h>
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#endif
#include <spa/utils/cleanup.h>
#include <spa/utils/defs.h>
@ -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;
}

View file

@ -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;

View file

@ -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, &timestamp);
avail = spa_ringbuffer_get_read_index(&impl->ring, &timestamp);
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;

View file

@ -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");
}

View file

@ -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:

View file

@ -7,6 +7,12 @@
#include <arpa/inet.h>
#include <net/if.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/un.h>
#include <spa/utils/string.h>
#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 */

View file

@ -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);

View file

@ -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