summaryrefslogtreecommitdiffstats
path: root/spa/plugins/bluez5
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--spa/plugins/bluez5/README-MIDI.md24
-rw-r--r--spa/plugins/bluez5/README-OPUS-A2DP.md335
-rw-r--r--spa/plugins/bluez5/README-SBC-XQ.md54
-rw-r--r--spa/plugins/bluez5/a2dp-codec-aac.c661
-rw-r--r--spa/plugins/bluez5/a2dp-codec-aptx.c748
-rw-r--r--spa/plugins/bluez5/a2dp-codec-caps.h460
-rw-r--r--spa/plugins/bluez5/a2dp-codec-faststream.c640
-rw-r--r--spa/plugins/bluez5/a2dp-codec-lc3plus.c790
-rw-r--r--spa/plugins/bluez5/a2dp-codec-ldac.c604
-rw-r--r--spa/plugins/bluez5/a2dp-codec-opus.c1444
-rw-r--r--spa/plugins/bluez5/a2dp-codec-sbc.c689
-rw-r--r--spa/plugins/bluez5/backend-hsphfpd.c1588
-rw-r--r--spa/plugins/bluez5/backend-native.c2838
-rw-r--r--spa/plugins/bluez5/backend-ofono.c947
-rw-r--r--spa/plugins/bluez5/bap-codec-caps.h142
-rw-r--r--spa/plugins/bluez5/bap-codec-lc3.c859
-rw-r--r--spa/plugins/bluez5/bluez-hardware.conf103
-rw-r--r--spa/plugins/bluez5/bluez5-dbus.c5182
-rw-r--r--spa/plugins/bluez5/bluez5-device.c2398
-rw-r--r--spa/plugins/bluez5/codec-loader.c234
-rw-r--r--spa/plugins/bluez5/codec-loader.h39
-rw-r--r--spa/plugins/bluez5/dbus-monitor.c265
-rw-r--r--spa/plugins/bluez5/dbus-monitor.h83
-rw-r--r--spa/plugins/bluez5/decode-buffer.h486
-rw-r--r--spa/plugins/bluez5/defs.h822
-rw-r--r--spa/plugins/bluez5/hci.c93
-rw-r--r--spa/plugins/bluez5/media-codecs.c212
-rw-r--r--spa/plugins/bluez5/media-codecs.h178
-rw-r--r--spa/plugins/bluez5/media-sink.c1868
-rw-r--r--spa/plugins/bluez5/media-source.c1707
-rw-r--r--spa/plugins/bluez5/meson.build206
-rw-r--r--spa/plugins/bluez5/midi-enum.c887
-rw-r--r--spa/plugins/bluez5/midi-node.c2151
-rw-r--r--spa/plugins/bluez5/midi-parser.c295
-rw-r--r--spa/plugins/bluez5/midi-server.c581
-rw-r--r--spa/plugins/bluez5/midi.h142
-rw-r--r--spa/plugins/bluez5/modemmanager.c1249
-rw-r--r--spa/plugins/bluez5/modemmanager.h161
-rw-r--r--spa/plugins/bluez5/org.bluez.xml71
-rw-r--r--spa/plugins/bluez5/player.c428
-rw-r--r--spa/plugins/bluez5/player.h51
-rw-r--r--spa/plugins/bluez5/plugin.c83
-rw-r--r--spa/plugins/bluez5/quirks.c406
-rw-r--r--spa/plugins/bluez5/rtp.h74
-rw-r--r--spa/plugins/bluez5/sco-io.c289
-rw-r--r--spa/plugins/bluez5/sco-sink.c1517
-rw-r--r--spa/plugins/bluez5/sco-source.c1592
-rw-r--r--spa/plugins/bluez5/test-midi.c299
-rw-r--r--spa/plugins/bluez5/upower.c311
-rw-r--r--spa/plugins/bluez5/upower.h36
50 files changed, 37322 insertions, 0 deletions
diff --git a/spa/plugins/bluez5/README-MIDI.md b/spa/plugins/bluez5/README-MIDI.md
new file mode 100644
index 0000000..45d092c
--- /dev/null
+++ b/spa/plugins/bluez5/README-MIDI.md
@@ -0,0 +1,24 @@
+## BLE MIDI & SELinux
+
+The SELinux configuration on Fedora 37 (as of 2022-11-10) does not
+permit access to the bluetoothd APIs needed for BLE MIDI.
+
+As a workaround, hopefully to be not necessary in future, you can
+permit such access by creating a file `blemidi.te` with contents:
+
+ policy_module(blemidi, 1.0);
+
+ require {
+ type system_dbusd_t;
+ type unconfined_t;
+ type bluetooth_t;
+ }
+
+ allow bluetooth_t unconfined_t:unix_stream_socket { read write };
+ allow system_dbusd_t bluetooth_t:unix_stream_socket { read write };
+
+Then having package `selinux-policy-devel` installed, running
+`make -f /usr/share/selinux/devel/Makefile blemidi.pp`, and finally
+to insert the rules via `sudo semodule -i blemidi.pp`.
+
+The policy change can be removed by `sudo semodule -r blemidi`.
diff --git a/spa/plugins/bluez5/README-OPUS-A2DP.md b/spa/plugins/bluez5/README-OPUS-A2DP.md
new file mode 100644
index 0000000..a7aefc1
--- /dev/null
+++ b/spa/plugins/bluez5/README-OPUS-A2DP.md
@@ -0,0 +1,335 @@
+---
+title: OPUS-A2DP-0.5 specification
+author: Pauli Virtanen <pav@iki.fi>
+date: Jun 4, 2022
+---
+
+# OPUS-A2DP-0.5 specification
+
+In this file, a way to use Opus as an A2DP vendor codec is specified.
+
+We will call this "OPUS-A2DP-0.5". There is no previous public
+specification for using Opus as an A2DP vendor codec (to my
+knowledge), which is why we need this one.
+
+[[_TOC_]]
+
+# Media Codec Capabilities
+
+The Media Codec Specific Information Elements ([AVDTP v1.3], §8.21.5)
+capability and configuration structure is as follows:
+
+| Octet | Bits | Meaning |
+|-------|------|-----------------------------------------------|
+| 0-5 | 0-7 | Vendor ID Part |
+| 6-7 | 0-7 | Channel Configuration |
+| 8-11 | 0-7 | Audio Location Configuration |
+| 12-14 | 0-7 | Limits Configuration |
+| 15-16 | 0-7 | Return Direction Channel Configuration |
+| 17-20 | 0-7 | Return Direction Audio Location Configuration |
+| 21-23 | 0-7 | Return Direction Limits Configuration |
+
+All integer fields and multi-byte bitfields are laid out in **little
+endian** order. All integer fields are unsigned.
+
+Each entry may have different meaning when present as a capability.
+Below, we indicate this by abbreviations CAP for capability and SEL
+for the value selected by SRC.
+
+Bits in fields marked RFA (Reserved For Additions) shall be set to
+zero.
+
+> **Note**
+>
+> See `a2dp-codec-caps.h` for definition as C structs.
+
+## Vendor ID Part
+
+The fixed value
+
+| Octet | Bits | Meaning |
+|-------|------|-------------------------------|
+| 0-3 | 0-7 | A2DP Vendor ID (0x05F1) |
+| 4-5 | 0-7 | A2DP Vendor Codec ID (0x1005) |
+
+> **Note**
+>
+> The Vendor ID is that of the Linux Foundation, and we are using it
+> here unofficially.
+
+## Channel Configuration
+
+The channel configuration consists of the channel count, and the count
+of coupled streams. The latter indicates which channels are encoded as
+left/right pairs, as defined in Sec. 5.1.1 of Opus Ogg Encapsulation [RFC7845].
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------------------|
+| 6 | 0-7 | Channel Count. CAP: maximum number supported. SEL: actual. |
+| 7 | 0-7 | Coupled Stream Count. CAP: 0. SEL: actual. |
+
+The Channel Count indicates the number of logical channels encoded in
+the data stream.
+
+The Coupled Stream Count indicates the number of streams that encode a
+coupled (left & right) channel pair. The count shall satisfy
+`(Channel Count) >= 2*(Coupled Stream Count)`.
+The Stream Count is `(Channel Count) - (Coupled Stream Count)`.
+
+The logical Channels are identified by a Channel Index *j* such that `0 <= j
+< (Channel Count)`. The channels `0 <= j < 2*(Coupled Stream Count)`
+are encoded in the *k*-th stream of the payload, where `k = floor(j/2)` and
+`j mod 2` determines which of the two channels of the stream the logical
+channel is. The channels `2*(Coupled Stream Count) <= j < (Channel Count)`
+are encoded in the *k*-th stream of the payload, where `k = j - (Coupled Stream Count)`.
+
+> **Note**
+>
+> The prescription here is identical to [RFC7845] with channel mapping
+> `mapping[j] = j`. We do not want to include the mapping table in the
+> A2DP capabilities, so it is assumed to be trivial.
+
+## Audio Location Configuration
+
+The semantic meaning for each channel is determined by their Audio
+Location bitfield.
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------------|
+| 8-11 | 0-7 | Audio Location bitfield. CAP: available. SEL: actual |
+
+The values specified in CAP are informative, and SEL may contain bits
+that were not set in CAP. SNK shall handle unsupported audio
+locations. It may do this for example by ignoring unsupported channels
+or via suitable up/downmixing. Hence, SRC may transmit channels with
+audio locations that are not marked supported by SNK.
+
+The audio location bit values are:
+
+| Channel Order | Bitmask | Audio Location |
+|---------------|------------|-------------------------|
+| 0 | 0x00000001 | Front Left |
+| 1 | 0x00000002 | Front Right |
+| 2 | 0x00000400 | Side Left |
+| 3 | 0x00000800 | Side Right |
+| 4 | 0x00000010 | Back Left |
+| 5 | 0x00000020 | Back Right |
+| 6 | 0x00000040 | Front Left of Center |
+| 7 | 0x00000080 | Front Right of Center |
+| 8 | 0x00001000 | Top Front Left |
+| 9 | 0x00002000 | Top Front Right |
+| 10 | 0x00040000 | Top Side Left |
+| 11 | 0x00080000 | Top Side Right |
+| 12 | 0x00010000 | Top Back Left |
+| 13 | 0x00020000 | Top Back Right |
+| 14 | 0x00400000 | Bottom Front Left |
+| 15 | 0x00800000 | Bottom Front Right |
+| 16 | 0x01000000 | Front Left Wide |
+| 17 | 0x02000000 | Front Right Wide |
+| 18 | 0x04000000 | Left Surround |
+| 19 | 0x08000000 | Right Surround |
+| 20 | 0x00000004 | Front Center |
+| 21 | 0x00000100 | Back Center |
+| 22 | 0x00004000 | Top Front Center |
+| 23 | 0x00008000 | Top Center |
+| 24 | 0x00100000 | Top Back Center |
+| 25 | 0x00200000 | Bottom Front Center |
+| 26 | 0x00000008 | Low Frequency Effects 1 |
+| 27 | 0x00000200 | Low Frequency Effects 2 |
+| 28 | 0x10000000 | RFA |
+| 29 | 0x20000000 | RFA |
+| 30 | 0x40000000 | RFA |
+| 31 | 0x80000000 | RFA |
+
+Each bit value is associated with a Channel Order. The bits set in
+the bitfield define audio locations for the streams present in the
+payload. The set bit with the smallest Channel Order value defines the
+audio location for the Channel Index *j=0*, the bit with the next
+lowest Channel Order value defines the audio location for the Channel
+Index *j=1*, and so forth.
+
+When the Channel Count is larger than the number of bits set in the
+Audio Location bitfield, the audio locations of the remaining channels
+are unspecified. Implementations may handle them as appropriate for
+their use case, considering them as AUX0–AUXN, or in the case of
+Channel Count = 1, as the single mono audio channel.
+
+When the Channel Count is smaller than the number of bits set in the
+Audio Location bitfield, the audio locations for the channels are
+assigned as above, and remaining excess bits shall be ignored.
+
+> **Note**
+>
+> The channel audio location specification is similar to the location
+> bitfield of the `Audio_Channel_Allocation` LTV structure in Bluetooth
+> SIG [Assigned Numbers, Generic Audio] used in the LE Audio, and the
+> bitmasks defined above are the same.
+>
+> The channel ordering differs from LE Audio, and is defined here to be
+> compatible with the internal stream ordering in the reference Opus
+> Multistream surround encoder Mapping Family 0 and 1 output. This
+> allows making use of its surround masking and LFE handling
+> capabilities. The stream ordering of the reference Opus surround
+> encoder, although being unchanged since its addition in 2013, is an
+> internal detail of the encoder. Implementations using the surround
+> encoder need to check that the mapping table used by the encoder
+> corresponds to the above channel ordering.
+>
+> For reference, we list the Audio Location bitfield values
+> corresponding to the different channel counts in Opus Mapping Family 0
+> and 1 surround encoder output, and the expected mapping table:
+>
+> | Mapping Family | Channel Count | Audio Location Value | Stream Ordering | Mapping Table |
+> |----------------|---------------|----------------------|---------------------------------|--------------------------|
+> | 0 | 1 | 0x00000000 | mono | {0} |
+> | 0 | 2 | 0x00000003 | FL, FR | {0, 1} |
+> | 1 | 1 | 0x00000000 | mono | {0} |
+> | 1 | 2 | 0x00000003 | FL, FR | {0, 1} |
+> | 1 | 3 | 0x00000007 | FL, FR, FC | {0, 2, 1} |
+> | 1 | 4 | 0x00000033 | FL, FR, BL, BR | {0, 1, 2, 3} |
+> | 1 | 5 | 0x00000037 | FL, FR, BL, BR, FC | {0, 4, 1, 2, 3} |
+> | 1 | 6 | 0x0000003f | FL, FR, BL, BR, FC, LFE | {0, 4, 1, 2, 3, 5} |
+> | 1 | 7 | 0x00000d0f | FL, FR, SL, SR, FC, BC, LFE | {0, 4, 1, 2, 3, 5, 6} |
+> | 1 | 8 | 0x00000c3f | FL, FR, SL, SR, BL, BR, FC, LFE | {0, 6, 1, 2, 3, 4, 5, 7} |
+>
+> The Mapping Table in the table indicates the mapping table selected by
+> `opus_multistream_surround_encoder_create` (Opus 1.3.1). If the
+> encoder outputs a different mapping table in a future Opus encoder
+> release, the channel ordering will be incorrect, and the surround
+> encoder can not be used. We expect that the probability of the Opus
+> encoder authors making such changes is negligible.
+
+## Limits Configuration
+
+The limits for allowed frame durations and maximum bitrate can also be
+configured.
+
+| Octet | Bits | Meaning |
+|-------|------|-----------------------------------------------------|
+| 16 | 0 | Frame duration 2.5ms. CAP: supported, SEL: selected |
+| 16 | 1 | Frame duration 5ms. CAP: supported, SEL: selected |
+| 16 | 2 | Frame duration 10ms. CAP: supported, SEL: selected |
+| 16 | 3 | Frame duration 20ms. CAP: supported, SEL: selected |
+| 16 | 4 | Frame duration 40ms. CAP: supported, SEL: selected |
+| 16 | 5-7 | RFA |
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------|
+| 17-18 | 0-7 | Maximum bitrate. CAP: supported, SEL: selected |
+
+The maximum bitrate is given in units of 1024 bits per second.
+
+The maximum bitrate field in CAP may contain value 0 to indicate
+everything is supported.
+
+## Bidirectional Audio Configuration
+
+Bidirectional audio may be supported. Its Channel Configuration, Audio
+Location Configuration, and Limits Configuration have identical form
+to the forward direction, and represented by exactly similar
+structures.
+
+Namely:
+
+| Octet | Bits | Meaning |
+|-------|------|----------------------------------------------------|
+| 19-20 | 0-7 | Channel Configuration fields, for return direction |
+| 21-28 | 0-7 | Audio Location fields, for return direction |
+| 29-31 | 0-7 | Limits Configuration fields, for return direction |
+
+If no return channel is supported or selected, the number of channels
+is set to 0 in CAP or SEL.
+
+> **Note**
+>
+> This is a nonstandard extension to A2DP. The return direction audio
+> data is simply sent back via the underlying L2CAP connection, which
+> is bidirectional, in the same format as the forward direction audio.
+> This is similar to what aptX-LL and FastStream do.
+
+# Packet Structure
+
+Each packet consists of an RTP header, an RTP payload header, and a
+payload containing Opus Multistream data.
+
+| Octet | Bits | Meaning |
+|-------|------|--------------------------|
+| 0-11 | 0-7 | RTP header |
+| 12 | 0-7 | RTP payload header |
+| 13-N | 0-7 | Opus Multistream payload |
+
+For each Bluetooth packet, the payload shall contain exactly one Opus
+Multistream packet, or a fragment of one. The Opus Multistream packet
+may be fragmented to several consecutive Bluetooth packets.
+
+The format of the Multistream data is the same as in the audio packets
+of [RFC7845], or, as produced/consumed by the Opus Multistream API.
+
+> **Note**
+>
+> We DO NOT follow [RFC7587], as we want fragmentation and multichannel support.
+
+## RTP Header
+
+See [RFC3550].
+
+The RTP payload type is pt=96 (dynamic).
+
+## RTP Payload Header
+
+The RTP payload header is used to indicate if and how the Opus
+Multistream packet is fragmented across several consecutive Bluetooth
+packets.
+
+| Octet | Bits | Meaning
+|--------|------|--------------------------------------------------------
+| 0 | 0-3 | Frame Count
+| 4 | 4 | RFA
+| 4 | 5 | Is Last Fragment
+| 4 | 6 | Is First Fragment
+| 4 | 7 | Is Fragmented
+
+In each packet, Frame Count indicates how many Bluetooth packets are
+still to be received (including the present packet) before the Opus
+Multistream packet is complete.
+
+The Is Fragment flag indicates whether the present packet contains
+fragmented payload.
+
+The Is Last Fragment flag indicates whether the present packet is the
+last part of fragmented payload.
+
+The Is First Fragment flag indicates whether the present packet is the
+first part of fragmented payload.
+
+In non-fragmented packets, Frame Count shall be (1), and the other bits
+in the header zero.
+
+## Opus Payload
+
+The Opus payload is a single Opus Multistream packet, or its fragment.
+
+In case of fragmentation, as indicated by the RTP payload header,
+concatenating the payloads of the fragment Bluetooth packets shall
+yield the total Opus Multistream packet.
+
+The SRC should choose encoder parameters such that Bluetooth bandwidth
+limitations are not exceeded.
+
+The SRC may include FEC data. The SNK may enable forward error
+correction instead of PLC.
+
+
+# References
+
+1. Bluetooth [AVDTP v1.3]
+2. IETF [RFC3550]
+3. IETF [RFC7587]
+4. IETF [RFC7845]
+5. Bluetooth [Assigned Numbers, Generic Audio]
+
+[AVDTP v1.3]: https://www.bluetooth.com/specifications/specs/a-v-distribution-transport-protocol-1-3/
+[RFC3550]: https://datatracker.ietf.org/doc/html/rfc3550
+[RFC7587]: https://datatracker.ietf.org/doc/html/rfc7587
+[RFC7845]: https://datatracker.ietf.org/doc/html/rfc7845
+[Assigned Numbers, Generic Audio]: https://www.bluetooth.com/specifications/assigned-numbers/
diff --git a/spa/plugins/bluez5/README-SBC-XQ.md b/spa/plugins/bluez5/README-SBC-XQ.md
new file mode 100644
index 0000000..3b393aa
--- /dev/null
+++ b/spa/plugins/bluez5/README-SBC-XQ.md
@@ -0,0 +1,54 @@
+## SBC XQ
+
+SBC XQ is standard SBC codec operating at high bitrates and thus reaching the
+transparent audio transport quality of AptX (HD) or other proprietary codecs.
+
+A2DP specification (A2DP SPEC) defines SBC parameters. These parameters are
+negotiated between the source (SRC) and the receiver (SNK) at connection time :
+
+- Audio channel mode : Joint Stereo, Stereo, Dual Channel, Mono : all modes
+ are MANDATORY for the SNK according to A2DP specification
+- Number of subbands: 4 or 8 - both MANDATORY for the SNK implementation
+- Blocks Length: 4, 8, 12, 16 - all MANDATORY for the SNK implementation
+- Allocation Method: Loudness, SNR - both MANDATORY for the SNK implementation
+- Maximum and minimum bit pool : between 2 to 250, expressed in 8 bit uint
+ (Unsigned integer, Most significant bit first) :
+ - A2DP spec v1.2 states that requires all SNK implementation shall handle
+ bitrates of up to 512 kbps (which correspond to bitpool = 76).
+ - A2DP spec v1.3 doesn't specify any bitrate limit, and some high-end SNK
+ devices announce bitpool between 62 and 94 (bitpool 94 = 551kbps bitrate).
+
+Bluetooth standard radio capabilities are as follow :
+
+| Bluetooth speed EDR | EDR 2Mbps | | EDR 3Mbps |
+|-------------------------|-----------|-------|-----------|
+| Speed (b/s) | 2097152 | | 3145728 |
+| Radio slot length (s) | 0.000625 | | 0.000625 |
+| Radio slots / s | 1600 | | 1600 |
+| Slot size (B) | 163.84 | | 245.76 |
+| Max payload/5 slots (B) | 676.2 | | 1085.8 |
+| max bitrate (Kb/s) | 1408.75 | | 2262.08 |
+
+The A2DP specification V1.3 provides RECOMMENDATIONS for bitpool implementation
+for the encoder of the SRC : it is required to support AT LEAST the following
+settings :
+
+- STEREO MODE : 53
+- MONO MODE : 31
+- DUAL CHANNEL : unspecified, so let's assume that the MONO value can be used : 3
+
+According to http://soundexpert.org/articles/-/blogs/audio-quality-of-sbc-xq-bluetooth-audio-codec ,
+AptX quality can be reached either :
+
+- in STEREO MODE, with bitpool ~ 76
+- in DUAL CHANNEL MODE, with bitpool ~ 38 per channel
+
+| sampling Freq (Hz) | 44100 | 48000 |
+|-------------------------|-----------|-------|
+| bitpool / channel | 38 | 35 |
+| Frame length DUAL (B) | 164 | 152 |
+| Frame length JST (B) | 165 | 153 |
+| Frame length ST (B) | 164 | 152 |
+| bitrate DUAL CH (kb/s) | 452 | 456 |
+| bitrate JOINT ST (kb/s) | 454 | 459 |
+| bitrate STEREO (kb/s) | 452 | 456 |
diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c
new file mode 100644
index 0000000..46a8740
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-aac.c
@@ -0,0 +1,661 @@
+/* Spa A2DP AAC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/utils/dict.h>
+
+#include <fdk-aac/aacenc_lib.h>
+#include <fdk-aac/aacdecoder_lib.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.aac");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_AAC_BITRATE 320000
+#define MIN_AAC_BITRATE 64000
+
+struct props {
+ int bitratemode;
+};
+
+struct impl {
+ HANDLE_AACENCODER aacenc;
+ HANDLE_AACDECODER aacdec;
+
+ struct rtp_header *header;
+
+ size_t mtu;
+ int codesize;
+
+ int max_bitrate;
+ int cur_bitrate;
+
+ uint32_t rate;
+ uint32_t channels;
+ int samplesize;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_aac_t a2dp_aac = {
+ .object_type =
+ /* NOTE: AAC Long Term Prediction and AAC Scalable are
+ * not supported by the FDK-AAC library. */
+ AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC,
+ AAC_INIT_FREQUENCY(
+ AAC_SAMPLING_FREQ_8000 |
+ AAC_SAMPLING_FREQ_11025 |
+ AAC_SAMPLING_FREQ_12000 |
+ AAC_SAMPLING_FREQ_16000 |
+ AAC_SAMPLING_FREQ_22050 |
+ AAC_SAMPLING_FREQ_24000 |
+ AAC_SAMPLING_FREQ_32000 |
+ AAC_SAMPLING_FREQ_44100 |
+ AAC_SAMPLING_FREQ_48000 |
+ AAC_SAMPLING_FREQ_64000 |
+ AAC_SAMPLING_FREQ_88200 |
+ AAC_SAMPLING_FREQ_96000)
+ .channels =
+ AAC_CHANNELS_1 |
+ AAC_CHANNELS_2,
+ .vbr = 1,
+ AAC_INIT_BITRATE(DEFAULT_AAC_BITRATE)
+ };
+
+ memcpy(caps, &a2dp_aac, sizeof(a2dp_aac));
+ return sizeof(a2dp_aac);
+}
+
+static const struct media_codec_config
+aac_frequencies[] = {
+ { AAC_SAMPLING_FREQ_48000, 48000, 11 },
+ { AAC_SAMPLING_FREQ_44100, 44100, 10 },
+ { AAC_SAMPLING_FREQ_96000, 96000, 9 },
+ { AAC_SAMPLING_FREQ_88200, 88200, 8 },
+ { AAC_SAMPLING_FREQ_64000, 64000, 7 },
+ { AAC_SAMPLING_FREQ_32000, 32000, 6 },
+ { AAC_SAMPLING_FREQ_24000, 24000, 5 },
+ { AAC_SAMPLING_FREQ_22050, 22050, 4 },
+ { AAC_SAMPLING_FREQ_16000, 16000, 3 },
+ { AAC_SAMPLING_FREQ_12000, 12000, 2 },
+ { AAC_SAMPLING_FREQ_11025, 11025, 1 },
+ { AAC_SAMPLING_FREQ_8000, 8000, 0 },
+};
+
+static const struct media_codec_config
+aac_channel_modes[] = {
+ { AAC_CHANNELS_2, 2, 1 },
+ { AAC_CHANNELS_1, 1, 0 },
+};
+
+static int get_valid_aac_bitrate(a2dp_aac_t *conf)
+{
+ if (AAC_GET_BITRATE(*conf) < MIN_AAC_BITRATE) {
+ /* Unknown (0) or bogus bitrate */
+ return DEFAULT_AAC_BITRATE;
+ } else {
+ return SPA_MIN(AAC_GET_BITRATE(*conf), DEFAULT_AAC_BITRATE);
+ }
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aac_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ conf = *(a2dp_aac_t*)caps;
+
+ if (conf.object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC)
+ conf.object_type = AAC_OBJECT_TYPE_MPEG2_AAC_LC;
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC)
+ conf.object_type = AAC_OBJECT_TYPE_MPEG4_AAC_LC;
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LTP)
+ return -ENOTSUP; /* Not supported by FDK-AAC */
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_SCA)
+ return -ENOTSUP; /* Not supported by FDK-AAC */
+ else
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(aac_frequencies,
+ SPA_N_ELEMENTS(aac_frequencies),
+ AAC_GET_FREQUENCY(conf),
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ AAC_SET_FREQUENCY(conf, aac_frequencies[i].config);
+
+ if ((i = media_codec_select_config(aac_channel_modes,
+ SPA_N_ELEMENTS(aac_channel_modes),
+ conf.channels,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channels = aac_channel_modes[i].config;
+
+ AAC_SET_BITRATE(conf, get_valid_aac_bitrate(&conf));
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_aac_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+ if (AAC_GET_FREQUENCY(conf) & f->config) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, f->value);
+ spa_pod_builder_int(b, f->value);
+ }
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+
+ if (SPA_FLAG_IS_SET(conf.channels, AAC_CHANNELS_1 | AAC_CHANNELS_2)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channels & AAC_CHANNELS_1) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 1, position),
+ 0);
+ } else if (conf.channels & AAC_CHANNELS_2) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else
+ return -EINVAL;
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ a2dp_aac_t conf;
+ size_t j;
+
+ if (caps == NULL || caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16;
+
+ /*
+ * A2DP v1.3.2, 4.5.2: only one bit shall be set in bitfields.
+ * However, there is a report (#1342) of device setting multiple
+ * bits for AAC object type. It's not clear if this was due to
+ * a BlueZ bug, but we can be lax here and below in codec_init.
+ */
+ if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC)))
+ return -EINVAL;
+ j = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+ if (AAC_GET_FREQUENCY(conf) & f->config) {
+ info->info.raw.rate = f->value;
+ j++;
+ break;
+ }
+ }
+ if (j == 0)
+ return -EINVAL;
+
+ if (conf.channels & AAC_CHANNELS_2) {
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ } else if (conf.channels & AAC_CHANNELS_1) {
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ } else {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p = calloc(1, sizeof(struct props));
+ const char *str;
+
+ if (p == NULL)
+ return NULL;
+
+ if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.aac.bitratemode")) == NULL)
+ str = "0";
+
+ p->bitratemode = SPA_CLAMP(atoi(str), 0, 5);
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct impl *this;
+ a2dp_aac_t *conf = config;
+ struct props *p = props;
+ UINT bitratemode;
+ int res;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL) {
+ res = -errno;
+ goto error;
+ }
+ this->mtu = mtu;
+ this->rate = info->info.raw.rate;
+ this->channels = info->info.raw.channels;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->samplesize = 2;
+
+ bitratemode = p ? p->bitratemode : 0;
+
+ res = aacEncOpen(&this->aacenc, 0, this->channels);
+ if (res != AACENC_OK)
+ goto error;
+
+ if (!(conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC))) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_SAMPLERATE, this->rate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_CHANNELMODE, this->channels);
+ if (res != AACENC_OK)
+ goto error;
+
+ if (conf->vbr) {
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATEMODE,
+ bitratemode);
+ if (res != AACENC_OK)
+ goto error;
+ }
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AUDIOMUXVER, 2);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_SIGNALING_MODE, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ // Fragmentation is not implemented yet,
+ // so make sure every encoded AAC frame fits in (mtu - header)
+ this->max_bitrate = ((this->mtu - sizeof(struct rtp_header)) * 8 * this->rate) / 1024;
+ this->max_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf));
+ this->cur_bitrate = this->max_bitrate;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_PEAK_BITRATE, this->max_bitrate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_TRANSMUX, TT_MP4_LATM_MCP1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_HEADER_PERIOD, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AFTERBURNER, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncEncode(this->aacenc, NULL, NULL, NULL, NULL);
+ if (res != AACENC_OK)
+ goto error;
+
+ AACENC_InfoStruct enc_info = {};
+ res = aacEncInfo(this->aacenc, &enc_info);
+ if (res != AACENC_OK)
+ goto error;
+
+ this->codesize = enc_info.frameLength * this->channels * this->samplesize;
+
+ this->aacdec = aacDecoder_Open(TT_MP4_LATM_MCP1, 1);
+ if (!this->aacdec) {
+ res = -EINVAL;
+ goto error;
+ }
+
+#ifdef AACDECODER_LIB_VL0
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MIN_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set min output channels: 0x%04X", res);
+ goto error;
+ }
+
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MAX_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set max output channels: 0x%04X", res);
+ goto error;
+ }
+#else
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set output channels: 0x%04X", res);
+ goto error;
+ }
+#endif
+
+ return this;
+
+error:
+ if (this && this->aacenc)
+ aacEncClose(&this->aacenc);
+ if (this && this->aacdec)
+ aacDecoder_Close(this->aacdec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ if (this->aacenc)
+ aacEncClose(&this->aacenc);
+ if (this->aacdec)
+ aacDecoder_Close(this->aacdec);
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ memset(this->header, 0, sizeof(struct rtp_header));
+
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ void *in_bufs[] = {(void *) src};
+ int in_buf_ids[] = {IN_AUDIO_DATA};
+ int in_buf_sizes[] = {src_size};
+ int in_buf_el_sizes[] = {this->samplesize};
+ AACENC_BufDesc in_buf_desc = {
+ .numBufs = 1,
+ .bufs = in_bufs,
+ .bufferIdentifiers = in_buf_ids,
+ .bufSizes = in_buf_sizes,
+ .bufElSizes = in_buf_el_sizes,
+ };
+ AACENC_InArgs in_args = {
+ .numInSamples = src_size / this->samplesize,
+ };
+
+ void *out_bufs[] = {dst};
+ int out_buf_ids[] = {OUT_BITSTREAM_DATA};
+ int out_buf_sizes[] = {dst_size};
+ int out_buf_el_sizes[] = {this->samplesize};
+ AACENC_BufDesc out_buf_desc = {
+ .numBufs = 1,
+ .bufs = out_bufs,
+ .bufferIdentifiers = out_buf_ids,
+ .bufSizes = out_buf_sizes,
+ .bufElSizes = out_buf_el_sizes,
+ };
+ AACENC_OutArgs out_args = {};
+
+ res = aacEncEncode(this->aacenc, &in_buf_desc, &out_buf_desc, &in_args, &out_args);
+ if (res != AACENC_OK)
+ return -EINVAL;
+
+ *dst_out = out_args.numOutBytes;
+ *need_flush = NEED_FLUSH_ALL;
+
+ /* RFC6416: It is set to 1 to indicate that the RTP packet contains a complete
+ * audioMuxElement or the last fragment of an audioMuxElement */
+ this->header->m = 1;
+
+ return out_args.numInSamples * this->samplesize;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ uint data_size = (uint)src_size;
+ uint bytes_valid = data_size;
+ CStreamInfo *aacinf;
+ int res;
+
+ res = aacDecoder_Fill(this->aacdec, (UCHAR **)&src, &data_size, &bytes_valid);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "AAC buffer fill error: 0x%04X", res);
+ return -EINVAL;
+ }
+
+ res = aacDecoder_DecodeFrame(this->aacdec, dst, dst_size, 0);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "AAC decode frame error: 0x%04X", res);
+ return -EINVAL;
+ }
+
+ aacinf = aacDecoder_GetStreamInfo(this->aacdec);
+ if (!aacinf) {
+ spa_log_debug(log, "AAC get stream info failed");
+ return -EINVAL;
+ }
+ *dst_out = aacinf->frameSize * aacinf->numChannels * this->samplesize;
+
+ return src_size - bytes_valid;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_change_bitrate(struct impl *this, int new_bitrate)
+{
+ int res;
+
+ new_bitrate = SPA_MIN(new_bitrate, this->max_bitrate);
+ new_bitrate = SPA_MAX(new_bitrate, 64000);
+
+ if (new_bitrate == this->cur_bitrate)
+ return 0;
+
+ this->cur_bitrate = new_bitrate;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate);
+ if (res != AACENC_OK)
+ return -EINVAL;
+
+ return this->cur_bitrate;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_change_bitrate(this, (this->cur_bitrate * 2) / 3);
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_change_bitrate(this, (this->cur_bitrate * 4) / 3);
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+ log = global_log;
+ spa_log_topic_init(log, &log_topic);
+}
+
+const struct media_codec a2dp_codec_aac = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC,
+ .codec_id = A2DP_CODEC_MPEG24,
+ .name = "aac",
+ .description = "AAC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .init_props = codec_init_props,
+ .clear_props = codec_clear_props,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .abr_process = codec_abr_process,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+ .set_log = codec_set_log,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "aac",
+ &a2dp_codec_aac
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c
new file mode 100644
index 0000000..6938e47
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-aptx.c
@@ -0,0 +1,748 @@
+/* Spa A2DP aptX codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <sbc/sbc.h>
+
+#include <freeaptx.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define APTX_LL_LEVEL1(level) (((level) >> 8) & 0xFF)
+#define APTX_LL_LEVEL2(level) (((level) >> 0) & 0xFF)
+#define APTX_LL_LEVEL(level1, level2) ((((level1) & 0xFF) << 8) | (((level2) & 0xFF) << 0))
+
+#define MSBC_DECODED_SIZE 240
+#define MSBC_ENCODED_SIZE 60
+#define MSBC_PAYLOAD_SIZE 57
+
+/*
+ * XXX: Bump requested device buffer levels up by 50% from defaults,
+ * XXX: increasing latency similarly. This seems to be necessary for
+ * XXX: stable output when moving headphones. It might be possible to
+ * XXX: reduce this by changing the scheduling of the socket writes.
+ */
+#define LL_LEVEL_ADJUSTMENT 3/2
+
+struct impl {
+ struct aptx_context *aptx;
+
+ struct rtp_header *header;
+
+ size_t mtu;
+ int codesize;
+ int frame_length;
+ int frame_count;
+ int max_frames;
+
+ bool hd;
+};
+
+struct msbc_impl {
+ sbc_t msbc;
+};
+
+static inline bool codec_is_hd(const struct media_codec *codec)
+{
+ return codec->vendor.codec_id == APTX_HD_CODEC_ID
+ && codec->vendor.vendor_id == APTX_HD_VENDOR_ID;
+}
+
+static inline bool codec_is_ll(const struct media_codec *codec)
+{
+ return (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL) ||
+ (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX);
+}
+
+static inline size_t codec_get_caps_size(const struct media_codec *codec)
+{
+ if (codec_is_hd(codec))
+ return sizeof(a2dp_aptx_hd_t);
+ else if (codec_is_ll(codec))
+ return sizeof(a2dp_aptx_ll_t);
+ else
+ return sizeof(a2dp_aptx_t);
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ size_t actual_conf_size = codec_get_caps_size(codec);
+ const a2dp_aptx_t a2dp_aptx = {
+ .info = codec->vendor,
+ .frequency =
+ APTX_SAMPLING_FREQ_16000 |
+ APTX_SAMPLING_FREQ_32000 |
+ APTX_SAMPLING_FREQ_44100 |
+ APTX_SAMPLING_FREQ_48000,
+ .channel_mode =
+ APTX_CHANNEL_MODE_STEREO,
+ };
+ const a2dp_aptx_ll_t a2dp_aptx_ll = {
+ .aptx = a2dp_aptx,
+ .bidirect_link = codec->duplex_codec ? true : false,
+ .has_new_caps = false,
+ };
+ if (codec_is_ll(codec))
+ memcpy(caps, &a2dp_aptx_ll, sizeof(a2dp_aptx_ll));
+ else
+ memcpy(caps, &a2dp_aptx, sizeof(a2dp_aptx));
+ return actual_conf_size;
+}
+
+static const struct media_codec_config
+aptx_frequencies[] = {
+ { APTX_SAMPLING_FREQ_48000, 48000, 3 },
+ { APTX_SAMPLING_FREQ_44100, 44100, 2 },
+ { APTX_SAMPLING_FREQ_32000, 32000, 1 },
+ { APTX_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aptx_t conf;
+ int i;
+ size_t actual_conf_size = codec_get_caps_size(codec);
+
+ if (caps_size < sizeof(conf) || actual_conf_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(aptx_frequencies,
+ SPA_N_ELEMENTS(aptx_frequencies),
+ conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = aptx_frequencies[i].config;
+
+ if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO)
+ conf.channel_mode = APTX_CHANNEL_MODE_STEREO;
+ else
+ return -ENOTSUP;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return actual_conf_size;
+}
+
+static int codec_select_config_ll(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aptx_ll_ext_t conf = { 0 };
+ size_t actual_conf_size;
+ int res;
+
+ /* caps may contain only conf.base, or also the extended attributes */
+
+ if (caps_size < sizeof(conf.base))
+ return -EINVAL;
+
+ memcpy(&conf, caps, SPA_MIN(caps_size, sizeof(conf)));
+
+ actual_conf_size = conf.base.has_new_caps ? sizeof(conf) : sizeof(conf.base);
+ if (caps_size < actual_conf_size)
+ return -EINVAL;
+
+ if (codec->duplex_codec && !conf.base.bidirect_link)
+ return -ENOTSUP;
+
+ if ((res = codec_select_config(codec, flags, caps, caps_size, info, settings, config)) < 0)
+ return res;
+
+ memcpy(&conf.base.aptx, config, sizeof(conf.base.aptx));
+
+ if (conf.base.has_new_caps) {
+ int target_level = APTX_LL_LEVEL(conf.target_level1, conf.target_level2);
+ int initial_level = APTX_LL_LEVEL(conf.initial_level1, conf.initial_level2);
+ int good_working_level = APTX_LL_LEVEL(conf.good_working_level1, conf.good_working_level2);
+
+ target_level = SPA_MAX(target_level, APTX_LL_TARGET_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
+ initial_level = SPA_MAX(initial_level, APTX_LL_INITIAL_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
+ good_working_level = SPA_MAX(good_working_level, APTX_LL_GOOD_WORKING_LEVEL * LL_LEVEL_ADJUSTMENT);
+
+ conf.target_level1 = APTX_LL_LEVEL1(target_level);
+ conf.target_level2 = APTX_LL_LEVEL2(target_level);
+ conf.initial_level1 = APTX_LL_LEVEL1(initial_level);
+ conf.initial_level2 = APTX_LL_LEVEL2(initial_level);
+ conf.good_working_level1 = APTX_LL_LEVEL1(good_working_level);
+ conf.good_working_level2 = APTX_LL_LEVEL2(good_working_level);
+
+ if (conf.sra_max_rate == 0)
+ conf.sra_max_rate = APTX_LL_SRA_MAX_RATE;
+ if (conf.sra_avg_time == 0)
+ conf.sra_avg_time = APTX_LL_SRA_AVG_TIME;
+ }
+
+ memcpy(config, &conf, actual_conf_size);
+
+ return actual_conf_size;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_aptx_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & APTX_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_32000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 32000);
+ spa_pod_builder_int(b, 32000);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_16000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (SPA_FLAG_IS_SET(conf.channel_mode, APTX_CHANNEL_MODE_MONO | APTX_CHANNEL_MODE_STEREO)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & APTX_CHANNEL_MODE_MONO) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 1, position),
+ 0);
+ } else if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else
+ return -EINVAL;
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct impl *this;
+ int res;
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ this->hd = codec_is_hd(codec);
+
+ if ((this->aptx = aptx_init(this->hd)) == NULL)
+ goto error_errno;
+
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->frame_length = this->hd ? 6 : 4;
+ this->codesize = 4 * 3 * 2;
+
+ if (this->hd)
+ this->max_frames = (this->mtu - sizeof(struct rtp_header)) / this->frame_length;
+ else if (codec_is_ll(codec))
+ this->max_frames = SPA_MIN(256u, this->mtu) / this->frame_length;
+ else
+ this->max_frames = this->mtu / this->frame_length;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ if (this->aptx)
+ aptx_finish(this->aptx);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ aptx_finish(this->aptx);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->frame_count = 0;
+
+ if (!this->hd)
+ return 0;
+
+ this->header = (struct rtp_header *)dst;
+ memset(this->header, 0, sizeof(struct rtp_header));
+
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ return sizeof(struct rtp_header);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ size_t avail_dst_size;
+ int res;
+
+ avail_dst_size = (this->max_frames - this->frame_count) * this->frame_length;
+ if (SPA_UNLIKELY(dst_size < avail_dst_size)) {
+ *need_flush = NEED_FLUSH_ALL;
+ return 0;
+ }
+
+ res = aptx_encode(this->aptx, src, src_size,
+ dst, avail_dst_size, dst_out);
+ if(SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+
+ this->frame_count += *dst_out / this->frame_length;
+ *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+
+ if (!this->hd)
+ return 0;
+
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header);
+
+ spa_return_val_if_fail(src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int res;
+
+ res = aptx_decode(this->aptx, src, src_size,
+ dst, dst_size, dst_out);
+
+ return res;
+}
+
+/*
+ * mSBC duplex codec
+ *
+ * When connected as SRC to SNK, aptX-LL sink may send back mSBC data.
+ */
+
+static int msbc_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ struct spa_audio_info_raw info = { 0, };
+
+ if (caps_size < sizeof(a2dp_aptx_ll_t))
+ return -EINVAL;
+
+ if (idx > 0)
+ return 0;
+
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ info.rate = 16000;
+
+ *param = spa_format_audio_raw_build(b, id, &info);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int msbc_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE;
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ info->info.raw.rate = 16000;
+ return 0;
+}
+
+static int msbc_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_get_block_size(void *data)
+{
+ return MSBC_DECODED_SIZE;
+}
+
+static void *msbc_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct msbc_impl *this = NULL;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct msbc_impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init_msbc(&this->msbc, 0)) < 0)
+ goto error;
+
+ this->msbc.endian = SBC_LE;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void msbc_deinit(void *data)
+{
+ struct msbc_impl *this = data;
+ sbc_finish(&this->msbc);
+ free(this);
+}
+
+static int msbc_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static int msbc_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct msbc_impl *this = data;
+ const uint8_t sync[3] = { 0xAD, 0x00, 0x00 };
+ size_t processed = 0;
+ int res;
+
+ spa_assert(sizeof(sync) <= MSBC_PAYLOAD_SIZE);
+
+ *dst_out = 0;
+
+ /* Scan for msbc sync sequence.
+ * We could probably assume fixed (<57-byte payload><1-byte pad>)+ format
+ * which devices seem to be sending. Don't know if there are variations,
+ * so we make weaker assumption here.
+ */
+ while (src_size >= MSBC_PAYLOAD_SIZE) {
+ if (memcmp(src, sync, sizeof(sync)) == 0)
+ break;
+ src = (uint8_t*)src + 1;
+ --src_size;
+ ++processed;
+ }
+
+ res = sbc_decode(&this->msbc, src, src_size,
+ dst, dst_size, dst_out);
+ if (res <= 0)
+ res = SPA_MIN((size_t)MSBC_PAYLOAD_SIZE, src_size); /* skip bad payload */
+
+ processed += res;
+ return processed;
+}
+
+
+const struct media_codec a2dp_codec_aptx = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = APTX_VENDOR_ID,
+ .codec_id = APTX_CODEC_ID },
+ .name = "aptx",
+ .description = "aptX",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+
+const struct media_codec a2dp_codec_aptx_hd = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = APTX_HD_VENDOR_ID,
+ .codec_id = APTX_HD_CODEC_ID },
+ .name = "aptx_hd",
+ .description = "aptX HD",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+#define APTX_LL_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .description = "aptX-LL", \
+ .fill_caps = codec_fill_caps, \
+ .select_config = codec_select_config_ll, \
+ .enum_config = codec_enum_config, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool
+
+
+const struct media_codec a2dp_codec_aptx_ll_0 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll",
+ .endpoint_name = "aptx_ll_0",
+};
+
+const struct media_codec a2dp_codec_aptx_ll_1 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll",
+ .endpoint_name = "aptx_ll_1",
+};
+
+/* Voice channel mSBC, not a real A2DP codec */
+static const struct media_codec aptx_ll_msbc = {
+ .codec_id = A2DP_CODEC_VENDOR,
+ .name = "aptx_ll_msbc",
+ .description = "aptX-LL mSBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config_ll,
+ .enum_config = msbc_enum_config,
+ .validate_config = msbc_validate_config,
+ .init = msbc_init,
+ .deinit = msbc_deinit,
+ .get_block_size = msbc_get_block_size,
+ .abr_process = msbc_abr_process,
+ .start_encode = msbc_start_encode,
+ .encode = msbc_encode,
+ .start_decode = msbc_start_decode,
+ .decode = msbc_decode,
+ .reduce_bitpool = msbc_reduce_bitpool,
+ .increase_bitpool = msbc_increase_bitpool,
+};
+
+static const struct spa_dict_item duplex_info_items[] = {
+ { "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
+const struct media_codec a2dp_codec_aptx_ll_duplex_0 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll_duplex",
+ .endpoint_name = "aptx_ll_duplex_0",
+ .duplex_codec = &aptx_ll_msbc,
+ .info = &duplex_info,
+};
+
+const struct media_codec a2dp_codec_aptx_ll_duplex_1 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll_duplex",
+ .endpoint_name = "aptx_ll_duplex_1",
+ .duplex_codec = &aptx_ll_msbc,
+ .info = &duplex_info,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "aptx",
+ &a2dp_codec_aptx_hd,
+ &a2dp_codec_aptx,
+ &a2dp_codec_aptx_ll_0,
+ &a2dp_codec_aptx_ll_1,
+ &a2dp_codec_aptx_ll_duplex_0,
+ &a2dp_codec_aptx_ll_duplex_1
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h
new file mode 100644
index 0000000..9f72592
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-caps.h
@@ -0,0 +1,460 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2006-2010 Nokia Corporation
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ * Copyright (C) 2018 Pali Rohár <pali.rohar@gmail.com>
+ *
+ * This library 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.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef SPA_BLUEZ5_A2DP_CODEC_CAPS_H_
+#define SPA_BLUEZ5_A2DP_CODEC_CAPS_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define A2DP_CODEC_SBC 0x00
+#define A2DP_CODEC_MPEG12 0x01
+#define A2DP_CODEC_MPEG24 0x02
+#define A2DP_CODEC_ATRAC 0x03
+#define A2DP_CODEC_VENDOR 0xFF
+
+#define A2DP_MAX_CAPS_SIZE 254
+
+/* customized 16-bit vendor extension */
+#define A2DP_CODEC_VENDOR_APTX 0x4FFF
+#define A2DP_CODEC_VENDOR_LDAC 0x2DFF
+
+#define SBC_SAMPLING_FREQ_48000 (1 << 0)
+#define SBC_SAMPLING_FREQ_44100 (1 << 1)
+#define SBC_SAMPLING_FREQ_32000 (1 << 2)
+#define SBC_SAMPLING_FREQ_16000 (1 << 3)
+
+#define SBC_CHANNEL_MODE_JOINT_STEREO (1 << 0)
+#define SBC_CHANNEL_MODE_STEREO (1 << 1)
+#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define SBC_CHANNEL_MODE_MONO (1 << 3)
+
+#define SBC_BLOCK_LENGTH_16 (1 << 0)
+#define SBC_BLOCK_LENGTH_12 (1 << 1)
+#define SBC_BLOCK_LENGTH_8 (1 << 2)
+#define SBC_BLOCK_LENGTH_4 (1 << 3)
+
+#define SBC_SUBBANDS_8 (1 << 0)
+#define SBC_SUBBANDS_4 (1 << 1)
+
+#define SBC_ALLOCATION_LOUDNESS (1 << 0)
+#define SBC_ALLOCATION_SNR (1 << 1)
+
+#define SBC_MIN_BITPOOL 2
+#define SBC_MAX_BITPOOL 64
+
+#define MPEG_CHANNEL_MODE_JOINT_STEREO (1 << 0)
+#define MPEG_CHANNEL_MODE_STEREO (1 << 1)
+#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define MPEG_CHANNEL_MODE_MONO (1 << 3)
+
+#define MPEG_LAYER_MP3 (1 << 0)
+#define MPEG_LAYER_MP2 (1 << 1)
+#define MPEG_LAYER_MP1 (1 << 2)
+
+#define MPEG_SAMPLING_FREQ_48000 (1 << 0)
+#define MPEG_SAMPLING_FREQ_44100 (1 << 1)
+#define MPEG_SAMPLING_FREQ_32000 (1 << 2)
+#define MPEG_SAMPLING_FREQ_24000 (1 << 3)
+#define MPEG_SAMPLING_FREQ_22050 (1 << 4)
+#define MPEG_SAMPLING_FREQ_16000 (1 << 5)
+
+#define MPEG_BIT_RATE_VBR 0x8000
+#define MPEG_BIT_RATE_320000 0x4000
+#define MPEG_BIT_RATE_256000 0x2000
+#define MPEG_BIT_RATE_224000 0x1000
+#define MPEG_BIT_RATE_192000 0x0800
+#define MPEG_BIT_RATE_160000 0x0400
+#define MPEG_BIT_RATE_128000 0x0200
+#define MPEG_BIT_RATE_112000 0x0100
+#define MPEG_BIT_RATE_96000 0x0080
+#define MPEG_BIT_RATE_80000 0x0040
+#define MPEG_BIT_RATE_64000 0x0020
+#define MPEG_BIT_RATE_56000 0x0010
+#define MPEG_BIT_RATE_48000 0x0008
+#define MPEG_BIT_RATE_40000 0x0004
+#define MPEG_BIT_RATE_32000 0x0002
+#define MPEG_BIT_RATE_FREE 0x0001
+
+#define AAC_OBJECT_TYPE_MPEG2_AAC_LC 0x80
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LC 0x40
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LTP 0x20
+#define AAC_OBJECT_TYPE_MPEG4_AAC_SCA 0x10
+
+#define AAC_SAMPLING_FREQ_8000 0x0800
+#define AAC_SAMPLING_FREQ_11025 0x0400
+#define AAC_SAMPLING_FREQ_12000 0x0200
+#define AAC_SAMPLING_FREQ_16000 0x0100
+#define AAC_SAMPLING_FREQ_22050 0x0080
+#define AAC_SAMPLING_FREQ_24000 0x0040
+#define AAC_SAMPLING_FREQ_32000 0x0020
+#define AAC_SAMPLING_FREQ_44100 0x0010
+#define AAC_SAMPLING_FREQ_48000 0x0008
+#define AAC_SAMPLING_FREQ_64000 0x0004
+#define AAC_SAMPLING_FREQ_88200 0x0002
+#define AAC_SAMPLING_FREQ_96000 0x0001
+
+#define AAC_CHANNELS_1 0x02
+#define AAC_CHANNELS_2 0x01
+
+#define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \
+ (a).bitrate2 << 8 | (a).bitrate3)
+#define AAC_GET_FREQUENCY(a) ((a).frequency1 << 4 | (a).frequency2)
+
+#define AAC_SET_BITRATE(a, b) \
+ do { \
+ (a).bitrate1 = ((b) >> 16) & 0x7f; \
+ (a).bitrate2 = ((b) >> 8) & 0xff; \
+ (a).bitrate3 = (b) & 0xff; \
+ } while (0)
+#define AAC_SET_FREQUENCY(a, f) \
+ do { \
+ (a).frequency1 = ((f) >> 4) & 0xff; \
+ (a).frequency2 = (f) & 0x0f; \
+ } while (0)
+
+#define AAC_INIT_BITRATE(b) \
+ .bitrate1 = ((b) >> 16) & 0x7f, \
+ .bitrate2 = ((b) >> 8) & 0xff, \
+ .bitrate3 = (b) & 0xff,
+#define AAC_INIT_FREQUENCY(f) \
+ .frequency1 = ((f) >> 4) & 0xff, \
+ .frequency2 = (f) & 0x0f,
+
+#define APTX_VENDOR_ID 0x0000004f
+#define APTX_CODEC_ID 0x0001
+
+#define APTX_CHANNEL_MODE_MONO 0x01
+#define APTX_CHANNEL_MODE_STEREO 0x02
+
+#define APTX_SAMPLING_FREQ_16000 0x08
+#define APTX_SAMPLING_FREQ_32000 0x04
+#define APTX_SAMPLING_FREQ_44100 0x02
+#define APTX_SAMPLING_FREQ_48000 0x01
+
+#define APTX_HD_VENDOR_ID 0x000000D7
+#define APTX_HD_CODEC_ID 0x0024
+
+#define APTX_HD_CHANNEL_MODE_MONO 0x1
+#define APTX_HD_CHANNEL_MODE_STEREO 0x2
+
+#define APTX_HD_SAMPLING_FREQ_16000 0x8
+#define APTX_HD_SAMPLING_FREQ_32000 0x4
+#define APTX_HD_SAMPLING_FREQ_44100 0x2
+#define APTX_HD_SAMPLING_FREQ_48000 0x1
+
+#define APTX_LL_VENDOR_ID 0x0000000a
+#define APTX_LL_VENDOR_ID2 0x000000d7
+#define APTX_LL_CODEC_ID 0x0002
+
+/**
+ * Default parameters for aptX LL (Sprint) encoder
+ */
+#define APTX_LL_TARGET_CODEC_LEVEL 180 /* target codec buffer level */
+#define APTX_LL_INITIAL_CODEC_LEVEL 360 /* initial codec buffer level */
+#define APTX_LL_SRA_MAX_RATE 50 /* x/10000 = 0.005 SRA rate */
+#define APTX_LL_SRA_AVG_TIME 1 /* SRA averaging time = 1s */
+#define APTX_LL_GOOD_WORKING_LEVEL 180 /* good working buffer level */
+
+#define LDAC_VENDOR_ID 0x0000012d
+#define LDAC_CODEC_ID 0x00aa
+
+#define LDAC_CHANNEL_MODE_MONO 0x04
+#define LDAC_CHANNEL_MODE_DUAL_CHANNEL 0x02
+#define LDAC_CHANNEL_MODE_STEREO 0x01
+
+#define LDAC_SAMPLING_FREQ_44100 0x20
+#define LDAC_SAMPLING_FREQ_48000 0x10
+#define LDAC_SAMPLING_FREQ_88200 0x08
+#define LDAC_SAMPLING_FREQ_96000 0x04
+#define LDAC_SAMPLING_FREQ_176400 0x02
+#define LDAC_SAMPLING_FREQ_192000 0x01
+
+#define FASTSTREAM_VENDOR_ID 0x0000000a
+#define FASTSTREAM_CODEC_ID 0x0001
+
+#define FASTSTREAM_DIRECTION_SINK 0x1
+#define FASTSTREAM_DIRECTION_SOURCE 0x2
+
+#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2
+#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1
+
+#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2
+
+#define LC3PLUS_HR_GET_FRAME_DURATION(a) ((a).frame_duration & 0xf0)
+#define LC3PLUS_HR_INIT_FRAME_DURATION(v) \
+ .frame_duration = ((v) & 0xf0),
+#define LC3PLUS_HR_SET_FRAME_DURATION(a, v) \
+ do { \
+ (a).frame_duration = ((v) & 0xf0); \
+ } while (0)
+
+#define LC3PLUS_HR_GET_FREQUENCY(a) (((a).frequency1 << 8) | (a).frequency2)
+#define LC3PLUS_HR_INIT_FREQUENCY(v) \
+ .frequency1 = (((v) >> 8) & 0xff), \
+ .frequency2 = ((v) & 0xff),
+#define LC3PLUS_HR_SET_FREQUENCY(a, v) \
+ do { \
+ (a).frequency1 = ((v) >> 8) & 0xff; \
+ (a).frequency2 = (v) & 0xff; \
+ } while (0)
+
+#define LC3PLUS_HR_VENDOR_ID 0x000008a9
+#define LC3PLUS_HR_CODEC_ID 0x0001
+
+#define LC3PLUS_HR_FRAME_DURATION_10MS (1 << 6)
+#define LC3PLUS_HR_FRAME_DURATION_5MS (1 << 5)
+#define LC3PLUS_HR_FRAME_DURATION_2_5MS (1 << 4)
+
+#define LC3PLUS_HR_CHANNELS_1 (1 << 7)
+#define LC3PLUS_HR_CHANNELS_2 (1 << 6)
+
+#define LC3PLUS_HR_SAMPLING_FREQ_48000 (1 << 8)
+#define LC3PLUS_HR_SAMPLING_FREQ_96000 (1 << 7)
+
+#define OPUS_05_VENDOR_ID 0x000005f1
+#define OPUS_05_CODEC_ID 0x1005
+
+#define OPUS_05_MAPPING_FAMILY_0 (1 << 0)
+#define OPUS_05_MAPPING_FAMILY_1 (1 << 1)
+#define OPUS_05_MAPPING_FAMILY_255 (1 << 2)
+
+#define OPUS_05_FRAME_DURATION_25 (1 << 0)
+#define OPUS_05_FRAME_DURATION_50 (1 << 1)
+#define OPUS_05_FRAME_DURATION_100 (1 << 2)
+#define OPUS_05_FRAME_DURATION_200 (1 << 3)
+#define OPUS_05_FRAME_DURATION_400 (1 << 4)
+
+#define OPUS_05_GET_UINT16(a, field) \
+ (((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT16(field, v) \
+ .field ## 1 = ((v) & 0xff), \
+ .field ## 2 = (((v) >> 8) & 0xff),
+#define OPUS_05_SET_UINT16(a, field, v) \
+ do { \
+ (a).field ## 1 = ((v) & 0xff); \
+ (a).field ## 2 = (((v) >> 8) & 0xff); \
+ } while (0)
+#define OPUS_05_GET_UINT32(a, field) \
+ (((a).field ## 4 << 24) | ((a).field ## 3 << 16) | \
+ ((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT32(field, v) \
+ .field ## 1 = ((v) & 0xff), \
+ .field ## 2 = (((v) >> 8) & 0xff), \
+ .field ## 3 = (((v) >> 16) & 0xff), \
+ .field ## 4 = (((v) >> 24) & 0xff),
+#define OPUS_05_SET_UINT32(a, field, v) \
+ do { \
+ (a).field ## 1 = ((v) & 0xff); \
+ (a).field ## 2 = (((v) >> 8) & 0xff); \
+ (a).field ## 3 = (((v) >> 16) & 0xff); \
+ (a).field ## 4 = (((v) >> 24) & 0xff); \
+ } while (0)
+
+#define OPUS_05_GET_LOCATION(a) OPUS_05_GET_UINT32(a, location)
+#define OPUS_05_INIT_LOCATION(v) OPUS_05_INIT_UINT32(location, v)
+#define OPUS_05_SET_LOCATION(a, v) OPUS_05_SET_UINT32(a, location, v)
+
+#define OPUS_05_GET_BITRATE(a) OPUS_05_GET_UINT16(a, bitrate)
+#define OPUS_05_INIT_BITRATE(v) OPUS_05_INIT_UINT16(bitrate, v)
+#define OPUS_05_SET_BITRATE(a, v) OPUS_05_SET_UINT16(a, bitrate, v)
+
+
+typedef struct {
+ uint32_t vendor_id;
+ uint16_t codec_id;
+} __attribute__ ((packed)) a2dp_vendor_codec_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency;
+ uint8_t channel_mode;
+} __attribute__ ((packed)) a2dp_ldac_t;
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+ uint8_t allocation_method:2;
+ uint8_t subbands:2;
+ uint8_t block_length:4;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t crc:1;
+ uint8_t layer:3;
+ uint8_t frequency:6;
+ uint8_t mpf:1;
+ uint8_t rfa:1;
+ uint16_t bitrate;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t rfa:2;
+ uint8_t channels:2;
+ uint8_t frequency2:4;
+ uint8_t bitrate1:7;
+ uint8_t vbr:1;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+ uint32_t rfa;
+} __attribute__ ((packed)) a2dp_aptx_hd_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t bidirect_link:1;
+ uint8_t has_new_caps:1;
+ uint8_t reserved:6;
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t sink_frequency:4;
+ uint8_t source_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+#elif __BYTE_ORDER == __BIG_ENDIAN
+
+typedef struct {
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+ uint8_t block_length:4;
+ uint8_t subbands:2;
+ uint8_t allocation_method:2;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t layer:3;
+ uint8_t crc:1;
+ uint8_t channel_mode:4;
+ uint8_t rfa:1;
+ uint8_t mpf:1;
+ uint8_t frequency:6;
+ uint16_t bitrate;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t frequency2:4;
+ uint8_t channels:2;
+ uint8_t rfa:2;
+ uint8_t vbr:1;
+ uint8_t bitrate1:7;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+ uint32_t rfa;
+} __attribute__ ((packed)) a2dp_aptx_hd_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t reserved:6;
+ uint8_t has_new_caps:1;
+ uint8_t bidirect_link:1;
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t source_frequency:4;
+ uint8_t sink_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+#else
+#error "Unknown byte order"
+#endif
+
+typedef struct {
+ a2dp_aptx_ll_t base;
+ uint8_t reserved;
+ uint8_t target_level2;
+ uint8_t target_level1;
+ uint8_t initial_level2;
+ uint8_t initial_level1;
+ uint8_t sra_max_rate;
+ uint8_t sra_avg_time;
+ uint8_t good_working_level2;
+ uint8_t good_working_level1;
+} __attribute__ ((packed)) a2dp_aptx_ll_ext_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frame_duration;
+ uint8_t channels;
+ uint8_t frequency1;
+ uint8_t frequency2;
+} __attribute__ ((packed)) a2dp_lc3plus_hr_t;
+
+typedef struct {
+ uint8_t channels;
+ uint8_t coupled_streams;
+ uint8_t location1;
+ uint8_t location2;
+ uint8_t location3;
+ uint8_t location4;
+ uint8_t frame_duration;
+ uint8_t bitrate1;
+ uint8_t bitrate2;
+} __attribute__ ((packed)) a2dp_opus_05_direction_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ a2dp_opus_05_direction_t main;
+ a2dp_opus_05_direction_t bidi;
+} __attribute__ ((packed)) a2dp_opus_05_t;
+
+#endif
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c
new file mode 100644
index 0000000..a579ead
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-faststream.c
@@ -0,0 +1,640 @@
+/* Spa A2DP FastStream codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#if __BYTE_ORDER != __LITTLE_ENDIAN
+#include <byteswap.h>
+#endif
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <sbc/sbc.h>
+
+#include "media-codecs.h"
+
+struct impl {
+ sbc_t sbc;
+
+ size_t mtu;
+ int codesize;
+ int frame_count;
+ int max_frames;
+};
+
+struct duplex_impl {
+ sbc_t sbc;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ const a2dp_faststream_t a2dp_faststream = {
+ .info = codec->vendor,
+ .direction = FASTSTREAM_DIRECTION_SINK |
+ (codec->duplex_codec ? FASTSTREAM_DIRECTION_SOURCE : 0),
+ .sink_frequency =
+ FASTSTREAM_SINK_SAMPLING_FREQ_44100 |
+ FASTSTREAM_SINK_SAMPLING_FREQ_48000,
+ .source_frequency =
+ FASTSTREAM_SOURCE_SAMPLING_FREQ_16000,
+ };
+
+ memcpy(caps, &a2dp_faststream, sizeof(a2dp_faststream));
+ return sizeof(a2dp_faststream);
+}
+
+static const struct media_codec_config
+frequencies[] = {
+ { FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 },
+ { FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 },
+};
+
+static const struct media_codec_config
+duplex_frequencies[] = {
+ { FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_faststream_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if (codec->duplex_codec && !(conf.direction & FASTSTREAM_DIRECTION_SOURCE))
+ return -ENOTSUP;
+
+ if (!(conf.direction & FASTSTREAM_DIRECTION_SINK))
+ return -ENOTSUP;
+
+ conf.direction = FASTSTREAM_DIRECTION_SINK;
+
+ if (codec->duplex_codec)
+ conf.direction |= FASTSTREAM_DIRECTION_SOURCE;
+
+ if ((i = media_codec_select_config(frequencies,
+ SPA_N_ELEMENTS(frequencies),
+ conf.sink_frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.sink_frequency = frequencies[i].config;
+
+ if ((i = media_codec_select_config(duplex_frequencies,
+ SPA_N_ELEMENTS(duplex_frequencies),
+ conf.source_frequency,
+ 16000
+ )) < 0)
+ return -ENOTSUP;
+ conf.source_frequency = duplex_frequencies[i].config;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_faststream_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static size_t ceil2(size_t v)
+{
+ if (v % 2 != 0 && v < SIZE_MAX)
+ v += 1;
+ return v;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ a2dp_faststream_t *conf = config;
+ struct impl *this;
+ bool sbc_initialized = false;
+ int res;
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init(&this->sbc, 0)) < 0)
+ goto error;
+
+ sbc_initialized = true;
+ this->sbc.endian = SBC_LE;
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->sink_frequency) {
+ case FASTSTREAM_SINK_SAMPLING_FREQ_44100:
+ this->sbc.frequency = SBC_FREQ_44100;
+ break;
+ case FASTSTREAM_SINK_SAMPLING_FREQ_48000:
+ this->sbc.frequency = SBC_FREQ_48000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->sbc.mode = SBC_MODE_JOINT_STEREO;
+ this->sbc.subbands = SBC_SB_8;
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ this->sbc.blocks = SBC_BLK_16;
+ this->sbc.bitpool = 29;
+
+ this->codesize = sbc_get_codesize(&this->sbc);
+
+ this->max_frames = 3;
+ if (this->mtu < this->max_frames * ceil2(sbc_get_frame_length(&this->sbc))) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (sbc_initialized)
+ sbc_finish(&this->sbc);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ this->frame_count = 0;
+ return 0;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_encode(&this->sbc, src, src_size,
+ dst, dst_size, (ssize_t*)dst_out);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ spa_assert(res == this->codesize);
+
+ if (*dst_out % 2 != 0 && *dst_out < dst_size) {
+ /* Pad similarly as in input stream */
+ *((uint8_t *)dst + *dst_out) = 0;
+ ++*dst_out;
+ }
+
+ this->frame_count += res / this->codesize;
+ *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static int do_decode(sbc_t *sbc,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ size_t processed = 0;
+ int res;
+
+ *dst_out = 0;
+
+ /* Scan for SBC syncword.
+ * We could probably assume 1-byte paddings instead,
+ * which devices seem to be sending.
+ */
+ while (src_size >= 1) {
+ if (*(uint8_t*)src == 0x9C)
+ break;
+ src = (uint8_t*)src + 1;
+ --src_size;
+ ++processed;
+ }
+
+ res = sbc_decode(sbc, src, src_size,
+ dst, dst_size, dst_out);
+ if (res <= 0)
+ res = SPA_MIN((size_t)1, src_size); /* skip bad payload */
+
+ processed += res;
+ return processed;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ return do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
+}
+
+/*
+ * Duplex codec
+ *
+ * When connected as SRC to SNK, FastStream sink may send back SBC data.
+ */
+
+static int duplex_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_faststream_t conf;
+ struct spa_audio_info_raw info = { 0, };
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ switch (conf.source_frequency) {
+ case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
+ info.rate = 16000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /*
+ * Some headsets send mono stream, others stereo. This information
+ * is contained in the SBC headers, and becomes known only when
+ * stream arrives. To be able to work in both cases, we will
+ * produce 2-channel output, and will double the channels
+ * in the decoding step if mono stream was received.
+ */
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 2;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ *param = spa_format_audio_raw_build(b, id, &info);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int duplex_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE;
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info->info.raw.rate = 16000;
+ return 0;
+}
+
+static int duplex_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_get_block_size(void *data)
+{
+ return 0;
+}
+
+static void *duplex_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ a2dp_faststream_t *conf = config;
+ struct duplex_impl *this = NULL;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct duplex_impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init(&this->sbc, 0)) < 0)
+ goto error;
+
+ switch (conf->source_frequency) {
+ case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
+ this->sbc.frequency = SBC_FREQ_16000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->sbc.endian = SBC_LE;
+ this->sbc.mode = SBC_MODE_MONO;
+ this->sbc.subbands = SBC_SB_8;
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ this->sbc.blocks = SBC_BLK_16;
+ this->sbc.bitpool = 32;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void duplex_deinit(void *data)
+{
+ struct duplex_impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int duplex_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+/** Convert S16LE stereo -> S16LE mono, in-place (only for testing purposes) */
+static SPA_UNUSED size_t convert_s16le_c2_to_c1(int16_t *data, size_t size, size_t max_size)
+{
+ size_t i;
+ for (i = 0; i < size / 2; ++i)
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ data[i] = data[2*i]/2 + data[2*i+1]/2;
+#else
+ data[i] = bswap_16(bswap_16(data[2*i])/2 + bswap_16(data[2*i+1])/2);
+#endif
+ return size / 2;
+}
+
+/** Convert S16LE mono -> S16LE stereo, in-place */
+static size_t convert_s16le_c1_to_c2(uint8_t *data, size_t size, size_t max_size)
+{
+ size_t pos;
+
+ pos = 2 * SPA_MIN(size / 2, max_size / 4);
+ size = 2 * pos;
+
+ /* We'll trust the compiler to optimize this */
+ while (pos >= 2) {
+ pos -= 2;
+ data[2*pos+3] = data[pos+1];
+ data[2*pos+2] = data[pos];
+ data[2*pos+1] = data[pos+1];
+ data[2*pos] = data[pos];
+ }
+
+ return size;
+}
+
+static int duplex_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct duplex_impl *this = data;
+ int res;
+
+ *dst_out = 0;
+ res = do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
+
+ /*
+ * Depending on headers of first frame, libsbc may output either
+ * 1 or 2 channels. This function should always produce 2 channels,
+ * so we'll just double the channels here.
+ */
+ if (this->sbc.mode == SBC_MODE_MONO)
+ *dst_out = convert_s16le_c1_to_c2(dst, *dst_out, dst_size);
+
+ return res;
+}
+
+/* Voice channel SBC, not a real A2DP codec */
+static const struct media_codec duplex_codec = {
+ .codec_id = A2DP_CODEC_VENDOR,
+ .name = "faststream_sbc",
+ .description = "FastStream duplex SBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = duplex_enum_config,
+ .validate_config = duplex_validate_config,
+ .init = duplex_init,
+ .deinit = duplex_deinit,
+ .get_block_size = duplex_get_block_size,
+ .abr_process = duplex_abr_process,
+ .start_encode = duplex_start_encode,
+ .encode = duplex_encode,
+ .start_decode = duplex_start_decode,
+ .decode = duplex_decode,
+ .reduce_bitpool = duplex_reduce_bitpool,
+ .increase_bitpool = duplex_increase_bitpool,
+};
+
+#define FASTSTREAM_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \
+ .codec_id = FASTSTREAM_CODEC_ID }, \
+ .description = "FastStream", \
+ .fill_caps = codec_fill_caps, \
+ .select_config = codec_select_config, \
+ .enum_config = codec_enum_config, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool
+
+const struct media_codec a2dp_codec_faststream = {
+ FASTSTREAM_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
+ .name = "faststream",
+};
+
+static const struct spa_dict_item duplex_info_items[] = {
+ { "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
+const struct media_codec a2dp_codec_faststream_duplex = {
+ FASTSTREAM_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+ .name = "faststream_duplex",
+ .duplex_codec = &duplex_codec,
+ .info = &duplex_info,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "faststream",
+ &a2dp_codec_faststream,
+ &a2dp_codec_faststream_duplex
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
new file mode 100644
index 0000000..2896624
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
@@ -0,0 +1,790 @@
+/* Spa A2DP LC3plus HR codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#if __BYTE_ORDER != __LITTLE_ENDIAN
+#include <byteswap.h>
+#endif
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#ifdef HAVE_LC3PLUS_H
+#include <lc3plus.h>
+#else
+#include <lc3.h>
+#endif
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define BITRATE_MIN 96000
+#define BITRATE_MAX 512000
+#define BITRATE_DEFAULT 160000
+
+struct dec_data {
+ int frame_size;
+ int fragment_size;
+ int fragment_count;
+ uint8_t fragment[LC3PLUS_MAX_BYTES];
+};
+
+struct enc_data {
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ int samples;
+ int codesize;
+
+ int packet_size;
+ int fragment_size;
+ int fragment_count;
+ void *fragment;
+
+ int bitrate;
+ int next_bitrate;
+};
+
+struct impl {
+ LC3PLUS_Enc *enc;
+ LC3PLUS_Dec *dec;
+
+ int mtu;
+ int samplerate;
+ int channels;
+ int frame_dms;
+ int bitrate;
+
+ struct dec_data d;
+ struct enc_data e;
+
+ int32_t buf[2][LC3PLUS_MAX_SAMPLES];
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ const a2dp_lc3plus_hr_t a2dp_lc3plus_hr = {
+ .info = codec->vendor,
+ LC3PLUS_HR_INIT_FRAME_DURATION(LC3PLUS_HR_FRAME_DURATION_10MS
+ | LC3PLUS_HR_FRAME_DURATION_5MS
+ | LC3PLUS_HR_FRAME_DURATION_2_5MS)
+ .channels = LC3PLUS_HR_CHANNELS_1 | LC3PLUS_HR_CHANNELS_2,
+ LC3PLUS_HR_INIT_FREQUENCY(LC3PLUS_HR_SAMPLING_FREQ_48000
+ | (lc3plus_samplerate_supported(96000) ? LC3PLUS_HR_SAMPLING_FREQ_96000 : 0))
+ };
+ memcpy(caps, &a2dp_lc3plus_hr, sizeof(a2dp_lc3plus_hr));
+ return sizeof(a2dp_lc3plus_hr);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_lc3plus_hr_t conf;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000)
+ && lc3plus_samplerate_supported(48000))
+ LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_48000);
+ else if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000)
+ && lc3plus_samplerate_supported(96000))
+ LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_96000);
+ else
+ return -ENOTSUP;
+
+ if ((conf.channels & LC3PLUS_HR_CHANNELS_2) &&
+ lc3plus_channels_supported(2))
+ conf.channels = LC3PLUS_HR_CHANNELS_2;
+ else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) &&
+ lc3plus_channels_supported(1))
+ conf.channels = LC3PLUS_HR_CHANNELS_1;
+ else
+ return -ENOTSUP;
+
+ if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_10MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_10MS);
+ else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_5MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_5MS);
+ else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_2_5MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_2_5MS);
+ else
+ return -ENOTSUP;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ a2dp_lc3plus_hr_t conf1, conf2;
+ a2dp_lc3plus_hr_t *conf;
+ int res1, res2;
+ int a, b;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->channels & LC3PLUS_HR_CHANNELS_2);
+ PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & (LC3PLUS_HR_SAMPLING_FREQ_48000 | LC3PLUS_HR_SAMPLING_FREQ_96000));
+ PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & LC3PLUS_HR_SAMPLING_FREQ_48000);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_lc3plus_hr_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000) &&
+ lc3plus_samplerate_supported(96000)) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 96000);
+ spa_pod_builder_int(b, 96000);
+ }
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000) &&
+ lc3plus_samplerate_supported(48000)) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((conf.channels & (LC3PLUS_HR_CHANNELS_2 | LC3PLUS_HR_CHANNELS_1)) &&
+ lc3plus_channels_supported(2) && lc3plus_channels_supported(1)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if ((conf.channels & LC3PLUS_HR_CHANNELS_2) && lc3plus_channels_supported(2)) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) && lc3plus_channels_supported(1)) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 1, position),
+ 0);
+ }
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const a2dp_lc3plus_hr_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S24_32;
+
+ switch (LC3PLUS_HR_GET_FREQUENCY(*conf)) {
+ case LC3PLUS_HR_SAMPLING_FREQ_96000:
+ if (!lc3plus_samplerate_supported(96000))
+ return -EINVAL;
+ info->info.raw.rate = 96000;
+ break;
+ case LC3PLUS_HR_SAMPLING_FREQ_48000:
+ if (!lc3plus_samplerate_supported(48000))
+ return -EINVAL;
+ info->info.raw.rate = 48000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->channels) {
+ case LC3PLUS_HR_CHANNELS_2:
+ if (!lc3plus_channels_supported(2))
+ return -EINVAL;
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ break;
+ case LC3PLUS_HR_CHANNELS_1:
+ if (!lc3plus_channels_supported(1))
+ return -EINVAL;
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) {
+ case LC3PLUS_HR_FRAME_DURATION_10MS:
+ case LC3PLUS_HR_FRAME_DURATION_5MS:
+ case LC3PLUS_HR_FRAME_DURATION_2_5MS:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static size_t ceildiv(size_t v, size_t divisor)
+{
+ if (v % divisor == 0)
+ return v / divisor;
+ else
+ return v / divisor + 1;
+}
+
+static bool check_mtu_vs_frame_dms(struct impl *this)
+{
+ /* Only 10ms frames can be fragmented (max 0xf fragments);
+ * others must fit in single MTU */
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ size_t max_fragments = (this->frame_dms == 100) ? 0xf : 1;
+ size_t payload_size = lc3plus_enc_get_num_bytes(this->enc);
+ return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ a2dp_lc3plus_hr_t *conf = config;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ int size;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+
+ this->mtu = mtu;
+ this->samplerate = config_info.info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->bitrate = BITRATE_DEFAULT * this->channels;
+
+ switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) {
+ case LC3PLUS_HR_FRAME_DURATION_10MS:
+ this->frame_dms = 100;
+ break;
+ case LC3PLUS_HR_FRAME_DURATION_5MS:
+ this->frame_dms = 50;
+ break;
+ case LC3PLUS_HR_FRAME_DURATION_2_5MS:
+ this->frame_dms = 25;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((size = lc3plus_enc_get_size(this->samplerate, this->channels)) == 0) {
+ res = -EIO;
+ goto error;
+ }
+ if ((this->enc = calloc(1, size)) == NULL)
+ goto error_errno;
+ if (lc3plus_enc_init(this->enc, this->samplerate, this->channels) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_enc_set_frame_ms(this->enc, this->frame_dms/10.0f) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_enc_set_hrmode(this->enc, 1) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ while (1) {
+ /* Find a valid bitrate */
+ if (lc3plus_enc_set_bitrate(this->enc, this->bitrate) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (check_mtu_vs_frame_dms(this))
+ break;
+ this->bitrate = this->bitrate * 3/4;
+ }
+
+ if ((size = lc3plus_dec_get_size(this->samplerate, this->channels)) == 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ if ((this->dec = calloc(1, size)) == NULL)
+ goto error_errno;
+ if (lc3plus_dec_init(this->dec, this->samplerate, this->channels, LC3PLUS_PLC_ADVANCED) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_dec_set_frame_ms(this->dec, this->frame_dms/10.0f) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_dec_set_hrmode(this->dec, 1) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->e.samples = lc3plus_enc_get_input_samples(this->enc);
+ this->e.codesize = this->e.samples * this->channels * sizeof(int32_t);
+
+ spa_assert(this->e.samples <= LC3PLUS_MAX_SAMPLES);
+
+ this->e.bitrate = this->bitrate;
+ this->e.next_bitrate = this->bitrate;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this && this->enc)
+ lc3plus_enc_free_memory(this->enc);
+ if (this && this->dec)
+ lc3plus_dec_free_memory(this->dec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ lc3plus_enc_free_memory(this->enc);
+ lc3plus_dec_free_memory(this->dec);
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->e.codesize;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_update_bitrate(struct impl *this)
+{
+ this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+
+ if (this->e.next_bitrate == this->e.bitrate)
+ return 0;
+
+ this->e.bitrate = this->e.next_bitrate;
+
+ if (lc3plus_enc_set_bitrate(this->enc, this->e.bitrate) != LC3PLUS_OK ||
+ !check_mtu_vs_frame_dms(this)) {
+ lc3plus_enc_set_bitrate(this->enc, this->bitrate);
+ return -EINVAL;
+ }
+
+ this->bitrate = this->e.bitrate;
+
+ return 0;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ if (dst_size <= header_size)
+ return -EINVAL;
+
+ codec_update_bitrate(this);
+
+ this->e.header = (struct rtp_header *)dst;
+ this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(dst, 0, header_size);
+
+ this->e.payload->frame_count = 0;
+ this->e.header->v = 2;
+ this->e.header->pt = 96;
+ this->e.header->sequence_number = htons(seqnum);
+ this->e.header->timestamp = htonl(timestamp);
+ this->e.header->ssrc = htonl(1);
+
+ this->e.packet_size = header_size;
+ return this->e.packet_size;
+}
+
+static void deinterleave_32_c2(int32_t * SPA_RESTRICT * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT src, size_t n_samples)
+{
+ /* We'll trust the compiler to optimize this */
+ const size_t n_channels = 2;
+ size_t i, j;
+ for (j = 0; j < n_samples; ++j)
+ for (i = 0; i < n_channels; ++i)
+ dst[i][j] = *src++;
+}
+
+static void interleave_32_c2(int32_t * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT * SPA_RESTRICT src, size_t n_samples)
+{
+ const size_t n_channels = 2;
+ size_t i, j;
+ for (j = 0; j < n_samples; ++j)
+ for (i = 0; i < n_channels; ++i)
+ *dst++ = src[i][j];
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int frame_bytes;
+ LC3PLUS_Error res;
+ int size, processed;
+ int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ int32_t *inputs[2];
+
+ if (src == NULL) {
+ /* Produce fragment packets.
+ *
+ * We assume the caller gives the same buffer here as in the previous
+ * calls to encode(), without changes in the buffer content.
+ */
+ if (this->e.fragment == NULL ||
+ this->e.fragment_count <= 1 ||
+ this->e.fragment < dst ||
+ SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) {
+ this->e.fragment = NULL;
+ return -EINVAL;
+ }
+
+ size = SPA_MIN(this->mtu - header_size, this->e.fragment_size);
+ memmove(dst, this->e.fragment, size);
+ *dst_out = size;
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->frame_count = --this->e.fragment_count;
+ this->e.payload->is_last_fragment = (this->e.fragment_count == 1);
+
+ if (this->e.fragment_size > size && this->e.fragment_count > 1) {
+ this->e.fragment = SPA_PTROFF(this->e.fragment, size, void);
+ this->e.fragment_size -= size;
+ *need_flush = NEED_FLUSH_FRAGMENT;
+ } else {
+ this->e.fragment = NULL;
+ *need_flush = NEED_FLUSH_ALL;
+ }
+ return 0;
+ }
+
+ frame_bytes = lc3plus_enc_get_num_bytes(this->enc);
+ processed = 0;
+
+ if (src_size < (size_t)this->e.codesize)
+ goto done;
+ if (dst_size < (size_t)frame_bytes)
+ goto done;
+ if (this->e.payload->frame_count > 0 &&
+ this->e.packet_size + frame_bytes > this->mtu)
+ goto done;
+
+ if (this->channels == 1) {
+ inputs[0] = (int32_t *)src;
+ res = lc3plus_enc24(this->enc, inputs, dst, &size);
+ } else {
+ inputs[0] = this->buf[0];
+ inputs[1] = this->buf[1];
+ deinterleave_32_c2(inputs, src, this->e.samples);
+ res = lc3plus_enc24(this->enc, inputs, dst, &size);
+ }
+ if (SPA_UNLIKELY(res != LC3PLUS_OK))
+ return -EINVAL;
+ *dst_out = size;
+
+ processed += this->e.codesize;
+ this->e.packet_size += size;
+ this->e.payload->frame_count++;
+
+done:
+ if (this->e.payload->frame_count == 0)
+ return processed;
+ if (this->e.payload->frame_count < 0xf &&
+ this->frame_dms * (this->e.payload->frame_count + 1) < 200 &&
+ this->e.packet_size + frame_bytes <= this->mtu)
+ return processed; /* add another frame */
+
+ if (this->e.packet_size > this->mtu) {
+ /* Fragment packet */
+ spa_assert(this->e.payload->frame_count == 1);
+ spa_assert(this->frame_dms == 100);
+
+ this->e.fragment_count = ceildiv(this->e.packet_size - header_size,
+ this->mtu - header_size);
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->is_first_fragment = 1;
+ this->e.payload->frame_count = this->e.fragment_count;
+
+ this->e.fragment_size = this->e.packet_size - this->mtu;
+ this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void);
+ *need_flush = NEED_FLUSH_FRAGMENT;
+
+ /*
+ * We keep the rest of the encoded frame in the same buffer, and rely
+ * that the caller won't overwrite it before the next call to encode()
+ */
+ *dst_out = SPA_PTRDIFF(this->e.fragment, dst);
+ } else {
+ *need_flush = NEED_FLUSH_ALL;
+ }
+
+ return processed;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+ const struct rtp_header *header = src;
+ const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void);
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ if (payload->is_fragmented) {
+ if (payload->is_first_fragment) {
+ this->d.fragment_size = 0;
+ } else if (payload->frame_count + 1 != this->d.fragment_count ||
+ (payload->frame_count == 1 && !payload->is_last_fragment)){
+ /* Fragments not in right order: drop packet */
+ return -EINVAL;
+ }
+ this->d.fragment_count = payload->frame_count;
+ this->d.frame_size = src_size - header_size;
+ } else {
+ if (payload->frame_count <= 0)
+ return -EINVAL;
+ this->d.fragment_count = 0;
+ this->d.frame_size = (src_size - header_size) / payload->frame_count;
+ if (this->d.frame_size <= 0)
+ return -EINVAL;
+ }
+
+ return header_size;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ LC3PLUS_Error res;
+ int32_t *outputs[2];
+ int consumed;
+ int samples;
+
+ if (this->d.fragment_count > 0) {
+ /* Fragmented frame */
+ size_t avail;
+ avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size);
+ memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail);
+
+ this->d.fragment_size += avail;
+ consumed = src_size;
+
+ if (this->d.fragment_count > 1) {
+ /* More fragments to come */
+ *dst_out = 0;
+ return consumed;
+ }
+
+ src = this->d.fragment;
+ src_size = this->d.fragment_size;
+
+ this->d.fragment_count = 0;
+ this->d.fragment_size = 0;
+ } else {
+ src_size = SPA_MIN((size_t)this->d.frame_size, src_size);
+ consumed = src_size;
+ }
+
+ samples = lc3plus_dec_get_output_samples(this->dec);
+ *dst_out = samples * this->channels * sizeof(int32_t);
+ if (dst_size < *dst_out)
+ return -EINVAL;
+
+ if (this->channels == 1) {
+ outputs[0] = (int32_t *)dst;
+ res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0);
+ } else {
+ outputs[0] = this->buf[0];
+ outputs[1] = this->buf[1];
+ res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0);
+ interleave_32_c2(dst, (const int32_t**)outputs, samples);
+ }
+ if (SPA_UNLIKELY(res != LC3PLUS_OK && res != LC3PLUS_DECODE_ERROR))
+ return -EINVAL;
+
+ return consumed;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ this->e.next_bitrate = SPA_CLAMP(this->bitrate * 3 / 4,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+ return this->e.next_bitrate;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ this->e.next_bitrate = SPA_CLAMP(this->bitrate * 5 / 4,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+ return this->e.next_bitrate;
+}
+
+const struct media_codec a2dp_codec_lc3plus_hr = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+ .name = "lc3plus_hr",
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = LC3PLUS_HR_VENDOR_ID,
+ .codec_id = LC3PLUS_HR_CODEC_ID },
+ .description = "LC3plus HR",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "lc3plus",
+ &a2dp_codec_lc3plus_hr
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c
new file mode 100644
index 0000000..624dd4a
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-ldac.c
@@ -0,0 +1,604 @@
+/* Spa A2DP LDAC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include <spa/pod/parser.h>
+#include <spa/param/props.h>
+#include <spa/param/audio/format.h>
+
+#include <ldacBT.h>
+
+#ifdef ENABLE_LDAC_ABR
+#include <ldacBT_abr.h>
+#endif
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define LDACBT_EQMID_AUTO -1
+
+#define LDAC_ABR_MAX_PACKET_NBYTES 1280
+
+#define LDAC_ABR_INTERVAL_MS 5 /* 2 frames * 128 lsu / 48000 */
+
+/* decrease ABR thresholds to increase stability */
+#define LDAC_ABR_THRESHOLD_CRITICAL 6
+#define LDAC_ABR_THRESHOLD_DANGEROUSTREND 4
+#define LDAC_ABR_THRESHOLD_SAFETY_FOR_HQSQ 3
+
+#define LDAC_ABR_SOCK_BUFFER_SIZE (LDAC_ABR_THRESHOLD_CRITICAL * LDAC_ABR_MAX_PACKET_NBYTES)
+
+
+struct props {
+ int eqmid;
+};
+
+struct impl {
+ HANDLE_LDAC_BT ldac;
+#ifdef ENABLE_LDAC_ABR
+ HANDLE_LDAC_ABR ldac_abr;
+#endif
+ bool enable_abr;
+
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ int mtu;
+ int eqmid;
+ int frequency;
+ int fmt;
+ int codesize;
+ int frame_length;
+ int frame_count;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_ldac_t a2dp_ldac = {
+ .info.vendor_id = LDAC_VENDOR_ID,
+ .info.codec_id = LDAC_CODEC_ID,
+ .frequency = LDACBT_SAMPLING_FREQ_044100 |
+ LDACBT_SAMPLING_FREQ_048000 |
+ LDACBT_SAMPLING_FREQ_088200 |
+ LDACBT_SAMPLING_FREQ_096000,
+ .channel_mode = LDACBT_CHANNEL_MODE_MONO |
+ LDACBT_CHANNEL_MODE_DUAL_CHANNEL |
+ LDACBT_CHANNEL_MODE_STEREO,
+ };
+
+ memcpy(caps, &a2dp_ldac, sizeof(a2dp_ldac));
+ return sizeof(a2dp_ldac);
+}
+
+static const struct media_codec_config
+ldac_frequencies[] = {
+ { LDACBT_SAMPLING_FREQ_044100, 44100, 3 },
+ { LDACBT_SAMPLING_FREQ_048000, 48000, 2 },
+ { LDACBT_SAMPLING_FREQ_088200, 88200, 1 },
+ { LDACBT_SAMPLING_FREQ_096000, 96000, 0 },
+};
+
+static const struct media_codec_config
+ldac_channel_modes[] = {
+ { LDACBT_CHANNEL_MODE_STEREO, 2, 2 },
+ { LDACBT_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 },
+ { LDACBT_CHANNEL_MODE_MONO, 1, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_ldac_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(ldac_frequencies,
+ SPA_N_ELEMENTS(ldac_frequencies),
+ conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = ldac_frequencies[i].config;
+
+ if ((i = media_codec_select_config(ldac_channel_modes,
+ SPA_N_ELEMENTS(ldac_channel_modes),
+ conf.channel_mode,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channel_mode = ldac_channel_modes[i].config;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_ldac_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t i = 0;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(5,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_S16),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_048000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_044100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_088200) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 88200);
+ spa_pod_builder_int(b, 88200);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_096000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 96000);
+ spa_pod_builder_int(b, 96000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO &&
+ conf.channel_mode & (LDACBT_CHANNEL_MODE_STEREO |
+ LDACBT_CHANNEL_MODE_DUAL_CHANNEL)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 1, position),
+ 0);
+ } else {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ }
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+#ifdef ENABLE_LDAC_ABR
+ return -ENOTSUP;
+#else
+ struct impl *this = data;
+ int res;
+ if (this->eqmid == LDACBT_EQMID_MQ || !this->enable_abr)
+ return this->eqmid;
+ res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_CONNECTION);
+ return res;
+#endif
+}
+
+static int codec_increase_bitpool(void *data)
+{
+#ifdef ENABLE_LDAC_ABR
+ return -ENOTSUP;
+#else
+ struct impl *this = data;
+ int res;
+ if (!this->enable_abr)
+ return this->eqmid;
+ res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_QUALITY);
+ return res;
+#endif
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static int string_to_eqmid(const char * eqmid)
+{
+ if (spa_streq("auto", eqmid))
+ return LDACBT_EQMID_AUTO;
+ else if (spa_streq("hq", eqmid))
+ return LDACBT_EQMID_HQ;
+ else if (spa_streq("sq", eqmid))
+ return LDACBT_EQMID_SQ;
+ else if (spa_streq("mq", eqmid))
+ return LDACBT_EQMID_MQ;
+ else
+ return LDACBT_EQMID_AUTO;
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p = calloc(1, sizeof(struct props));
+ const char *str;
+
+ if (p == NULL)
+ return NULL;
+
+ if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.ldac.quality")) == NULL)
+ str = "auto";
+
+ p->eqmid = string_to_eqmid(str);
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+static int codec_enum_props(void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ struct props *p = props;
+ struct spa_pod_frame f[2];
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (idx) {
+ case 0:
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
+ spa_pod_builder_id(b, SPA_PROP_quality);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
+ spa_pod_builder_string(b, "LDAC quality");
+
+ spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_int(b, p->eqmid);
+ spa_pod_builder_int(b, LDACBT_EQMID_AUTO);
+ spa_pod_builder_int(b, LDACBT_EQMID_HQ);
+ spa_pod_builder_int(b, LDACBT_EQMID_SQ);
+ spa_pod_builder_int(b, LDACBT_EQMID_MQ);
+ spa_pod_builder_pop(b, &f[1]);
+
+ spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, LDACBT_EQMID_AUTO);
+ spa_pod_builder_string(b, "auto");
+ spa_pod_builder_int(b, LDACBT_EQMID_HQ);
+ spa_pod_builder_string(b, "hq");
+ spa_pod_builder_int(b, LDACBT_EQMID_SQ);
+ spa_pod_builder_string(b, "sq");
+ spa_pod_builder_int(b, LDACBT_EQMID_MQ);
+ spa_pod_builder_string(b, "mq");
+ spa_pod_builder_pop(b, &f[1]);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (idx) {
+ case 0:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_quality, SPA_POD_Int(p->eqmid));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 1;
+}
+
+static int codec_set_props(void *props, const struct spa_pod *param)
+{
+ struct props *p = props;
+ const int prev_eqmid = p->eqmid;
+ if (param == NULL) {
+ p->eqmid = LDACBT_EQMID_AUTO;
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_quality, SPA_POD_OPT_Int(&p->eqmid));
+ if (p->eqmid != LDACBT_EQMID_AUTO &&
+ (p->eqmid < LDACBT_EQMID_HQ || p->eqmid > LDACBT_EQMID_MQ))
+ p->eqmid = prev_eqmid;
+ }
+
+ return prev_eqmid != p->eqmid;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct impl *this;
+ a2dp_ldac_t *conf = config;
+ int res;
+ struct props *p = props;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ goto error_errno;
+
+ this->ldac = ldacBT_get_handle();
+ if (this->ldac == NULL)
+ goto error_errno;
+
+#ifdef ENABLE_LDAC_ABR
+ this->ldac_abr = ldac_ABR_get_handle();
+ if (this->ldac_abr == NULL)
+ goto error_errno;
+#endif
+
+ if (p == NULL || p->eqmid == LDACBT_EQMID_AUTO) {
+ this->eqmid = LDACBT_EQMID_SQ;
+ this->enable_abr = true;
+ } else {
+ this->eqmid = p->eqmid;
+ this->enable_abr = false;
+ }
+
+ this->mtu = mtu;
+ this->frequency = info->info.raw.rate;
+ this->codesize = info->info.raw.channels * LDACBT_ENC_LSU;
+
+ switch (info->info.raw.format) {
+ case SPA_AUDIO_FORMAT_F32:
+ this->fmt = LDACBT_SMPL_FMT_F32;
+ this->codesize *= 4;
+ break;
+ case SPA_AUDIO_FORMAT_S32:
+ this->fmt = LDACBT_SMPL_FMT_S32;
+ this->codesize *= 4;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ this->fmt = LDACBT_SMPL_FMT_S24;
+ this->codesize *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S16:
+ this->fmt = LDACBT_SMPL_FMT_S16;
+ this->codesize *= 2;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ res = ldacBT_init_handle_encode(this->ldac,
+ this->mtu,
+ this->eqmid,
+ conf->channel_mode,
+ this->fmt,
+ this->frequency);
+ if (res < 0)
+ goto error;
+
+#ifdef ENABLE_LDAC_ABR
+ res = ldac_ABR_Init(this->ldac_abr, LDAC_ABR_INTERVAL_MS);
+ if (res < 0)
+ goto error;
+
+ res = ldac_ABR_set_thresholds(this->ldac_abr,
+ LDAC_ABR_THRESHOLD_CRITICAL,
+ LDAC_ABR_THRESHOLD_DANGEROUSTREND,
+ LDAC_ABR_THRESHOLD_SAFETY_FOR_HQSQ);
+ if (res < 0)
+ goto error;
+#endif
+
+ return this;
+
+error_errno:
+ res = -errno;
+error:
+ if (this && this->ldac)
+ ldacBT_free_handle(this->ldac);
+#ifdef ENABLE_LDAC_ABR
+ if (this && this->ldac_abr)
+ ldac_ABR_free_handle(this->ldac_abr);
+#endif
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ if (this->ldac)
+ ldacBT_free_handle(this->ldac);
+#ifdef ENABLE_LDAC_ABR
+ if (this->ldac_abr)
+ ldac_ABR_free_handle(this->ldac_abr);
+#endif
+ free(this);
+}
+
+static int codec_update_props(void *data, void *props)
+{
+ struct impl *this = data;
+ struct props *p = props;
+ int res;
+
+ if (p == NULL)
+ return 0;
+
+ if (p->eqmid == LDACBT_EQMID_AUTO) {
+ this->eqmid = LDACBT_EQMID_SQ;
+ this->enable_abr = true;
+ } else {
+ this->eqmid = p->eqmid;
+ this->enable_abr = false;
+ }
+
+ if ((res = ldacBT_set_eqmid(this->ldac, this->eqmid)) < 0)
+ goto error;
+ return 0;
+error:
+ return res;
+}
+
+static int codec_abr_process(void *data, size_t unsent)
+{
+#ifdef ENABLE_LDAC_ABR
+ struct impl *this = data;
+ int res;
+ res = ldac_ABR_Proc(this->ldac, this->ldac_abr,
+ unsent / LDAC_ABR_MAX_PACKET_NBYTES, this->enable_abr);
+ return res;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload));
+
+ this->payload->frame_count = 0;
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res, src_used, dst_used, frame_num = 0;
+
+ src_used = src_size;
+ dst_used = dst_size;
+
+ res = ldacBT_encode(this->ldac, (void*)src, &src_used, dst, &dst_used, &frame_num);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+
+ *dst_out = dst_used;
+
+ this->payload->frame_count += frame_num;
+ *need_flush = (this->payload->frame_count > 0) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+
+ return src_used;
+}
+
+const struct media_codec a2dp_codec_ldac = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = LDAC_VENDOR_ID,
+ .codec_id = LDAC_CODEC_ID },
+ .name = "ldac",
+ .description = "LDAC",
+#ifdef ENABLE_LDAC_ABR
+ .send_buf_size = LDAC_ABR_SOCK_BUFFER_SIZE,
+#endif
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init_props = codec_init_props,
+ .enum_props = codec_enum_props,
+ .set_props = codec_set_props,
+ .clear_props = codec_clear_props,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .update_props = codec_update_props,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "ldac",
+ &a2dp_codec_ldac
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c
new file mode 100644
index 0000000..32ae290
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-opus.c
@@ -0,0 +1,1444 @@
+/* Spa A2DP Opus Codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <string.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#if __BYTE_ORDER != __LITTLE_ENDIAN
+#include <byteswap.h>
+#endif
+
+#include <spa/debug/types.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <opus.h>
+#include <opus_multistream.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.opus");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define BUFSIZE_FROM_BITRATE(frame_dms,bitrate) ((bitrate)/8 * (frame_dms) / 10000 * 5/4) /* estimate */
+
+/*
+ * Opus CVBR target bitrate. When connecting, it is set to the INITIAL
+ * value, and after that adjusted according to link quality between the MIN and
+ * MAX values. The bitrate adjusts up to either MAX or the value at
+ * which the socket buffer starts filling up, whichever is lower.
+ *
+ * With perfect connection quality, the target bitrate converges to the MAX
+ * value. Under realistic conditions, the upper limit may often be as low as
+ * 300-500kbit/s, so the INITIAL values are not higher than this.
+ *
+ * The MAX is here set to 2-2.5x and INITIAL to 1.5x the upper Opus recommended
+ * values [1], to be safer quality-wise for CVBR, and MIN to the lower
+ * recommended value.
+ *
+ * [1] https://wiki.xiph.org/Opus_Recommended_Settings
+ */
+#define BITRATE_INITIAL 192000
+#define BITRATE_MAX 320000
+#define BITRATE_MIN 96000
+
+#define BITRATE_INITIAL_51 384000
+#define BITRATE_MAX_51 600000
+#define BITRATE_MIN_51 128000
+
+#define BITRATE_INITIAL_71 450000
+#define BITRATE_MAX_71 900000
+#define BITRATE_MIN_71 256000
+
+#define BITRATE_DUPLEX_BIDI 160000
+
+#define OPUS_05_MAX_BYTES (15 * 1024)
+
+struct props {
+ uint32_t channels;
+ uint32_t coupled_streams;
+ uint32_t location;
+ uint32_t max_bitrate;
+ uint8_t frame_duration;
+ int application;
+
+ uint32_t bidi_channels;
+ uint32_t bidi_coupled_streams;
+ uint32_t bidi_location;
+ uint32_t bidi_max_bitrate;
+ uint32_t bidi_frame_duration;
+ int bidi_application;
+};
+
+struct dec_data {
+ int fragment_size;
+ int fragment_count;
+ uint8_t fragment[OPUS_05_MAX_BYTES];
+};
+
+struct abr {
+ uint64_t now;
+ uint64_t last_update;
+
+ uint32_t buffer_level;
+ uint32_t packet_size;
+ uint32_t total_size;
+ bool bad;
+
+ uint64_t last_change;
+ uint64_t retry_interval;
+
+ bool prev_bad;
+};
+
+struct enc_data {
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ struct abr abr;
+
+ int samples;
+ int codesize;
+
+ int packet_size;
+ int fragment_size;
+ int fragment_count;
+ void *fragment;
+
+ int bitrate_min;
+ int bitrate_max;
+
+ int bitrate;
+ int next_bitrate;
+
+ int frame_dms;
+ int application;
+};
+
+struct impl {
+ OpusMSEncoder *enc;
+ OpusMSDecoder *dec;
+
+ int mtu;
+ int samplerate;
+ int application;
+
+ uint8_t channels;
+ uint8_t streams;
+ uint8_t coupled_streams;
+
+ bool is_bidi;
+
+ struct dec_data d;
+ struct enc_data e;
+};
+
+struct audio_location {
+ uint32_t mask;
+ enum spa_audio_channel position;
+};
+
+struct surround_encoder_mapping {
+ uint8_t channels;
+ uint8_t coupled_streams;
+ uint32_t location;
+ uint8_t mapping[8]; /**< permutation streams -> vorbis order */
+ uint8_t inv_mapping[8]; /**< permutation vorbis order -> streams */
+};
+
+/* Bluetooth SIG, Assigned Numbers, Generic Audio, Audio Location Definitions */
+#define BT_AUDIO_LOCATION_FL 0x00000001 /* Front Left */
+#define BT_AUDIO_LOCATION_FR 0x00000002 /* Front Right */
+#define BT_AUDIO_LOCATION_FC 0x00000004 /* Front Center */
+#define BT_AUDIO_LOCATION_LFE 0x00000008 /* Low Frequency Effects 1 */
+#define BT_AUDIO_LOCATION_RL 0x00000010 /* Back Left */
+#define BT_AUDIO_LOCATION_RR 0x00000020 /* Back Right */
+#define BT_AUDIO_LOCATION_FLC 0x00000040 /* Front Left of Center */
+#define BT_AUDIO_LOCATION_FRC 0x00000080 /* Front Right of Center */
+#define BT_AUDIO_LOCATION_RC 0x00000100 /* Back Center */
+#define BT_AUDIO_LOCATION_LFE2 0x00000200 /* Low Frequency Effects 2 */
+#define BT_AUDIO_LOCATION_SL 0x00000400 /* Side Left */
+#define BT_AUDIO_LOCATION_SR 0x00000800 /* Side Right */
+#define BT_AUDIO_LOCATION_TFL 0x00001000 /* Top Front Left */
+#define BT_AUDIO_LOCATION_TFR 0x00002000 /* Top Front Right */
+#define BT_AUDIO_LOCATION_TFC 0x00004000 /* Top Front Center */
+#define BT_AUDIO_LOCATION_TC 0x00008000 /* Top Center */
+#define BT_AUDIO_LOCATION_TRL 0x00010000 /* Top Back Left */
+#define BT_AUDIO_LOCATION_TRR 0x00020000 /* Top Back Right */
+#define BT_AUDIO_LOCATION_TSL 0x00040000 /* Top Side Left */
+#define BT_AUDIO_LOCATION_TSR 0x00080000 /* Top Side Right */
+#define BT_AUDIO_LOCATION_TRC 0x00100000 /* Top Back Center */
+#define BT_AUDIO_LOCATION_BC 0x00200000 /* Bottom Front Center */
+#define BT_AUDIO_LOCATION_BLC 0x00400000 /* Bottom Front Left */
+#define BT_AUDIO_LOCATION_BRC 0x00800000 /* Bottom Front Right */
+#define BT_AUDIO_LOCATION_FLW 0x01000000 /* Fron Left Wide */
+#define BT_AUDIO_LOCATION_FRW 0x02000000 /* Front Right Wide */
+#define BT_AUDIO_LOCATION_SSL 0x04000000 /* Left Surround */
+#define BT_AUDIO_LOCATION_SSR 0x08000000 /* Right Surround */
+
+#define BT_AUDIO_LOCATION_ANY 0x0fffffff
+
+static const struct audio_location audio_locations[] = {
+ { BT_AUDIO_LOCATION_FL, SPA_AUDIO_CHANNEL_FL },
+ { BT_AUDIO_LOCATION_FR, SPA_AUDIO_CHANNEL_FR },
+ { BT_AUDIO_LOCATION_SL, SPA_AUDIO_CHANNEL_SL },
+ { BT_AUDIO_LOCATION_SR, SPA_AUDIO_CHANNEL_SR },
+ { BT_AUDIO_LOCATION_RL, SPA_AUDIO_CHANNEL_RL },
+ { BT_AUDIO_LOCATION_RR, SPA_AUDIO_CHANNEL_RR },
+ { BT_AUDIO_LOCATION_FLC, SPA_AUDIO_CHANNEL_FLC },
+ { BT_AUDIO_LOCATION_FRC, SPA_AUDIO_CHANNEL_FRC },
+ { BT_AUDIO_LOCATION_TFL, SPA_AUDIO_CHANNEL_TFL },
+ { BT_AUDIO_LOCATION_TFR, SPA_AUDIO_CHANNEL_TFR },
+ { BT_AUDIO_LOCATION_TSL, SPA_AUDIO_CHANNEL_TSL },
+ { BT_AUDIO_LOCATION_TSR, SPA_AUDIO_CHANNEL_TSR },
+ { BT_AUDIO_LOCATION_TRL, SPA_AUDIO_CHANNEL_TRL },
+ { BT_AUDIO_LOCATION_TRR, SPA_AUDIO_CHANNEL_TRR },
+ { BT_AUDIO_LOCATION_BLC, SPA_AUDIO_CHANNEL_BLC },
+ { BT_AUDIO_LOCATION_BRC, SPA_AUDIO_CHANNEL_BRC },
+ { BT_AUDIO_LOCATION_FLW, SPA_AUDIO_CHANNEL_FLW },
+ { BT_AUDIO_LOCATION_FRW, SPA_AUDIO_CHANNEL_FRW },
+ { BT_AUDIO_LOCATION_SSL, SPA_AUDIO_CHANNEL_SL }, /* ~ Side Left */
+ { BT_AUDIO_LOCATION_SSR, SPA_AUDIO_CHANNEL_SR }, /* ~ Side Right */
+ { BT_AUDIO_LOCATION_FC, SPA_AUDIO_CHANNEL_FC },
+ { BT_AUDIO_LOCATION_RC, SPA_AUDIO_CHANNEL_RC },
+ { BT_AUDIO_LOCATION_TFC, SPA_AUDIO_CHANNEL_TFC },
+ { BT_AUDIO_LOCATION_TC, SPA_AUDIO_CHANNEL_TC },
+ { BT_AUDIO_LOCATION_TRC, SPA_AUDIO_CHANNEL_TRC },
+ { BT_AUDIO_LOCATION_BC, SPA_AUDIO_CHANNEL_BC },
+ { BT_AUDIO_LOCATION_LFE, SPA_AUDIO_CHANNEL_LFE },
+ { BT_AUDIO_LOCATION_LFE2, SPA_AUDIO_CHANNEL_LFE2 },
+};
+
+/* Opus surround encoder mapping tables for the supported channel configurations */
+static const struct surround_encoder_mapping surround_encoders[] = {
+ { 1, 0, (0x0),
+ { 0 }, { 0 } },
+ { 2, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR),
+ { 0, 1 }, { 0, 1 } },
+ { 3, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_FC),
+ { 0, 2, 1 }, { 0, 2, 1 } },
+ { 4, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR),
+ { 0, 1, 2, 3 }, { 0, 1, 2, 3 } },
+ { 5, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC),
+ { 0, 4, 1, 2, 3 }, { 0, 2, 3, 4, 1 } },
+ { 6, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_LFE),
+ { 0, 4, 1, 2, 3, 5 }, { 0, 2, 3, 4, 1, 5 } },
+ { 7, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+ BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_RC | BT_AUDIO_LOCATION_LFE),
+ { 0, 4, 1, 2, 3, 5, 6 }, { 0, 2, 3, 4, 1, 5, 6 } },
+ { 8, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+ BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_LFE),
+ { 0, 6, 1, 2, 3, 4, 5, 7 }, { 0, 2, 3, 4, 5, 6, 1, 7 } },
+};
+
+static uint32_t bt_channel_from_name(const char *name)
+{
+ size_t i;
+ enum spa_audio_channel position = SPA_AUDIO_CHANNEL_UNKNOWN;
+
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) {
+ position = spa_type_audio_channel[i].type;
+ break;
+ }
+ }
+ for (i = 0; i < SPA_N_ELEMENTS(audio_locations); i++) {
+ if (position == audio_locations[i].position)
+ return audio_locations[i].mask;
+ }
+ return 0;
+}
+
+static uint32_t parse_locations(const char *str)
+{
+ char *s, *p, *save = NULL;
+ uint32_t location = 0;
+
+ if (!str)
+ return 0;
+
+ s = strdup(str);
+ if (s == NULL)
+ return 0;
+
+ for (p = s; (p = strtok_r(p, ", ", &save)) != NULL; p = NULL) {
+ if (*p == '\0')
+ continue;
+ location |= bt_channel_from_name(p);
+ }
+ free(s);
+
+ return location;
+}
+
+static void parse_settings(struct props *props, const struct spa_dict *settings)
+{
+ const char *str;
+ uint32_t v;
+
+ /* Pro Audio settings */
+ spa_zero(*props);
+ props->channels = 8;
+ props->coupled_streams = 0;
+ props->location = 0;
+ props->max_bitrate = BITRATE_MAX;
+ props->frame_duration = OPUS_05_FRAME_DURATION_100;
+ props->application = OPUS_APPLICATION_AUDIO;
+
+ props->bidi_channels = 1;
+ props->bidi_coupled_streams = 0;
+ props->bidi_location = 0;
+ props->bidi_max_bitrate = BITRATE_DUPLEX_BIDI;
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+ props->bidi_application = OPUS_APPLICATION_AUDIO;
+
+ if (settings == NULL)
+ return;
+
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.channels"), &v, 0))
+ props->channels = SPA_CLAMP(v, 1u, SPA_AUDIO_MAX_CHANNELS);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.max-bitrate"), &v, 0))
+ props->max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.coupled-streams"), &v, 0))
+ props->coupled_streams = SPA_CLAMP(v, 0u, props->channels / 2);
+
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.channels"), &v, 0))
+ props->bidi_channels = SPA_CLAMP(v, 0u, SPA_AUDIO_MAX_CHANNELS);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.max-bitrate"), &v, 0))
+ props->bidi_max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.coupled-streams"), &v, 0))
+ props->bidi_coupled_streams = SPA_CLAMP(v, 0u, props->bidi_channels / 2);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.locations");
+ props->location = parse_locations(str);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.locations");
+ props->bidi_location = parse_locations(str);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.frame-dms");
+ if (spa_streq(str, "25"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_25;
+ else if (spa_streq(str, "50"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (spa_streq(str, "100"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (spa_streq(str, "200"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (spa_streq(str, "400"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_400;
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.frame-dms");
+ if (spa_streq(str, "25"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_25;
+ else if (spa_streq(str, "50"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (spa_streq(str, "100"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (spa_streq(str, "200"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (spa_streq(str, "400"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.application");
+ if (spa_streq(str, "audio"))
+ props->application = OPUS_APPLICATION_AUDIO;
+ else if (spa_streq(str, "voip"))
+ props->application = OPUS_APPLICATION_VOIP;
+ else if (spa_streq(str, "lowdelay"))
+ props->application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.application");
+ if (spa_streq(str, "audio"))
+ props->bidi_application = OPUS_APPLICATION_AUDIO;
+ else if (spa_streq(str, "voip"))
+ props->bidi_application = OPUS_APPLICATION_VOIP;
+ else if (spa_streq(str, "lowdelay"))
+ props->bidi_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+}
+
+static int set_channel_conf(const struct media_codec *codec, a2dp_opus_05_t *caps, const struct props *props)
+{
+ /*
+ * Predefined codec profiles
+ */
+ if (caps->main.channels < 1)
+ return -EINVAL;
+
+ caps->main.coupled_streams = 0;
+ OPUS_05_SET_LOCATION(caps->main, 0);
+
+ caps->bidi.coupled_streams = 0;
+ OPUS_05_SET_LOCATION(caps->bidi, 0);
+
+ switch (codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+ caps->main.channels = SPA_MIN(2, caps->main.channels);
+ if (caps->main.channels == 2) {
+ caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+ }
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+ if (caps->main.channels < 6)
+ return -EINVAL;
+ caps->main.channels = surround_encoders[5].channels;
+ caps->main.coupled_streams = surround_encoders[5].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[5].location);
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+ if (caps->main.channels < 8)
+ return -EINVAL;
+ caps->main.channels = surround_encoders[7].channels;
+ caps->main.coupled_streams = surround_encoders[7].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[7].location);
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+ if (caps->bidi.channels < 1)
+ return -EINVAL;
+ caps->main.channels = SPA_MIN(2, caps->main.channels);
+ if (caps->main.channels == 2) {
+ caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+ }
+ caps->bidi.channels = SPA_MIN(2, caps->bidi.channels);
+ if (caps->bidi.channels == 2) {
+ caps->bidi.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->bidi, surround_encoders[1].location);
+ }
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+ if (caps->main.channels < props->channels)
+ return -EINVAL;
+ if (props->bidi_channels == 0 && caps->bidi.channels != 0)
+ return -EINVAL;
+ if (caps->bidi.channels < props->bidi_channels)
+ return -EINVAL;
+ caps->main.channels = props->channels;
+ caps->main.coupled_streams = props->coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, props->location);
+ caps->bidi.channels = props->bidi_channels;
+ caps->bidi.coupled_streams = props->bidi_coupled_streams;
+ OPUS_05_SET_LOCATION(caps->bidi, props->bidi_location);
+ break;
+ default:
+ spa_assert(false);
+ };
+
+ return 0;
+}
+
+static void get_default_bitrates(const struct media_codec *codec, bool bidi, int *min, int *max, int *init)
+{
+ int tmp;
+
+ if (min == NULL)
+ min = &tmp;
+ if (max == NULL)
+ max = &tmp;
+ if (init == NULL)
+ init = &tmp;
+
+ if (bidi) {
+ *min = SPA_MIN(BITRATE_MIN, BITRATE_DUPLEX_BIDI);
+ *max = BITRATE_DUPLEX_BIDI;
+ *init = BITRATE_DUPLEX_BIDI;
+ return;
+ }
+
+ switch (codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+ *min = BITRATE_MIN;
+ *max = BITRATE_MAX;
+ *init = BITRATE_INITIAL;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+ *min = BITRATE_MIN_51;
+ *max = BITRATE_MAX_51;
+ *init = BITRATE_INITIAL_51;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+ *min = BITRATE_MIN_71;
+ *max = BITRATE_MAX_71;
+ *init = BITRATE_INITIAL_71;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+ *min = BITRATE_MIN;
+ *max = BITRATE_MAX;
+ *init = BITRATE_INITIAL;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+ default:
+ spa_assert_not_reached();
+ };
+}
+
+static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direction_t *conf,
+ bool use_surround_encoder, uint8_t *streams_ret, uint8_t *coupled_streams_ret,
+ const uint8_t **surround_mapping, uint32_t *positions)
+{
+ const uint8_t channels = conf->channels;
+ const uint32_t location = OPUS_05_GET_LOCATION(*conf);
+ const uint8_t coupled_streams = conf->coupled_streams;
+ const uint8_t *permutation = NULL;
+ size_t i, j;
+
+ if (channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+ if (2 * coupled_streams > channels)
+ return -EINVAL;
+
+ if (streams_ret)
+ *streams_ret = channels - coupled_streams;
+ if (coupled_streams_ret)
+ *coupled_streams_ret = coupled_streams;
+
+ if (channels == 0)
+ return 0;
+
+ if (use_surround_encoder) {
+ /* Opus surround encoder supports only some channel configurations, and
+ * needs a specific input channel ordering */
+ for (i = 0; i < SPA_N_ELEMENTS(surround_encoders); ++i) {
+ const struct surround_encoder_mapping *m = &surround_encoders[i];
+
+ if (m->channels == channels &&
+ m->coupled_streams == coupled_streams &&
+ m->location == location)
+ {
+ spa_assert(channels <= SPA_N_ELEMENTS(m->inv_mapping));
+ permutation = m->inv_mapping;
+ if (surround_mapping)
+ *surround_mapping = m->mapping;
+ break;
+ }
+ }
+ if (permutation == NULL && surround_mapping)
+ *surround_mapping = NULL;
+ }
+
+ if (positions) {
+ for (i = 0, j = 0; i < SPA_N_ELEMENTS(audio_locations) && j < channels; ++i) {
+ const struct audio_location loc = audio_locations[i];
+
+ if (location & loc.mask) {
+ if (permutation)
+ positions[permutation[j++]] = loc.position;
+ else
+ positions[j++] = loc.position;
+ }
+ }
+ for (i = SPA_AUDIO_CHANNEL_START_Aux; j < channels; ++i, ++j)
+ positions[j] = i;
+ }
+
+ return 0;
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_opus_05_t a2dp_opus_05 = {
+ .info = codec->vendor,
+ .main = {
+ .channels = SPA_AUDIO_MAX_CHANNELS,
+ .frame_duration = (OPUS_05_FRAME_DURATION_25 |
+ OPUS_05_FRAME_DURATION_50 |
+ OPUS_05_FRAME_DURATION_100 |
+ OPUS_05_FRAME_DURATION_200 |
+ OPUS_05_FRAME_DURATION_400),
+ OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+ OPUS_05_INIT_BITRATE(0)
+ },
+ .bidi = {
+ .channels = SPA_AUDIO_MAX_CHANNELS,
+ .frame_duration = (OPUS_05_FRAME_DURATION_25 |
+ OPUS_05_FRAME_DURATION_50 |
+ OPUS_05_FRAME_DURATION_100 |
+ OPUS_05_FRAME_DURATION_200 |
+ OPUS_05_FRAME_DURATION_400),
+ OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+ OPUS_05_INIT_BITRATE(0)
+ }
+ };
+
+ /* Only duplex/pro codec has bidi, since bluez5-device has to know early
+ * whether to show nodes or not. */
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX &&
+ codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ spa_zero(a2dp_opus_05.bidi);
+
+ memcpy(caps, &a2dp_opus_05, sizeof(a2dp_opus_05));
+ return sizeof(a2dp_opus_05);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ struct props props;
+ a2dp_opus_05_t conf;
+ int res;
+ int max;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ parse_settings(&props, global_settings);
+
+ /* Channel Configuration & Audio Location */
+ if ((res = set_channel_conf(codec, &conf, &props)) < 0)
+ return res;
+
+ /* Limits */
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+ max = props.max_bitrate;
+ if (OPUS_05_GET_BITRATE(conf.main) != 0)
+ OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+ max = props.bidi_max_bitrate;
+ if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+ OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+
+ if (conf.main.frame_duration & props.frame_duration)
+ conf.main.frame_duration = props.frame_duration;
+ else
+ return -EINVAL;
+
+ if (conf.bidi.channels == 0)
+ true;
+ else if (conf.bidi.frame_duration & props.bidi_frame_duration)
+ conf.bidi.frame_duration = props.bidi_frame_duration;
+ else
+ return -EINVAL;
+ } else {
+ if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_100)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_200)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_400)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_400;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_50)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_25)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_25;
+ else
+ return -EINVAL;
+
+ get_default_bitrates(codec, false, NULL, &max, NULL);
+
+ if (OPUS_05_GET_BITRATE(conf.main) != 0)
+ OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+ /* longer bidi frames appear to work better */
+ if (conf.bidi.channels == 0)
+ true;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_200)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_100)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_400)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_400;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_50)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_25)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_25;
+ else
+ return -EINVAL;
+
+ get_default_bitrates(codec, true, NULL, &max, NULL);
+
+ if (conf.bidi.channels == 0)
+ true;
+ else if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+ OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+ }
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings)
+{
+ a2dp_opus_05_t conf1, conf2, cap1, cap2;
+ a2dp_opus_05_t *conf;
+ int res1, res2;
+ int a, b;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, flags, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, flags, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ memcpy(&cap1, caps1, sizeof(cap1));
+ memcpy(&cap2, caps2, sizeof(cap2));
+
+ if (conf1.bidi.channels == 0 && conf2.bidi.channels == 0) {
+ /* If no bidi, prefer the SEP that has none */
+ a = (cap1.bidi.channels == 0);
+ b = (cap2.bidi.channels == 0);
+ if (a != b)
+ return b - a;
+ }
+
+ PREFER_EXPR(conf->main.channels);
+ PREFER_EXPR(conf->bidi.channels);
+ PREFER_EXPR(OPUS_05_GET_BITRATE(conf->main));
+ PREFER_EXPR(OPUS_05_GET_BITRATE(conf->bidi));
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static bool is_duplex_codec(const struct media_codec *codec)
+{
+ return codec->id == 0;
+}
+
+static bool use_surround_encoder(const struct media_codec *codec, bool is_sink)
+{
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ return false;
+
+ if (is_duplex_codec(codec))
+ return is_sink;
+ else
+ return !is_sink;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ a2dp_opus_05_t conf;
+ a2dp_opus_05_direction_t *dir;
+ struct spa_pod_frame f[1];
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ dir = !is_duplex_codec(codec) ? &conf.main : &conf.bidi;
+
+ if (get_mapping(codec, dir, surround_encoder, NULL, NULL, NULL, position) < 0)
+ return -EINVAL;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_F32),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(6,
+ 48000, 48000, 24000, 16000, 12000, 8000),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(dir->channels),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dir->channels, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ const a2dp_opus_05_direction_t *dir1, *dir2;
+ const a2dp_opus_05_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_F32;
+ info->info.raw.rate = 0; /* not specified by config */
+
+ if (2 * conf->main.coupled_streams > conf->main.channels)
+ return -EINVAL;
+ if (2 * conf->bidi.coupled_streams > conf->bidi.channels)
+ return -EINVAL;
+
+ if (!is_duplex_codec(codec)) {
+ dir1 = &conf->main;
+ dir2 = &conf->bidi;
+ } else {
+ dir1 = &conf->bidi;
+ dir2 = &conf->main;
+ }
+
+ info->info.raw.channels = dir1->channels;
+ if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0)
+ return -EINVAL;
+ if (get_mapping(codec, dir2, surround_encoder, NULL, NULL, NULL, NULL) < 0)
+ return -EINVAL;
+
+ return 0;
+}
+
+static size_t ceildiv(size_t v, size_t divisor)
+{
+ if (v % divisor == 0)
+ return v / divisor;
+ else
+ return v / divisor + 1;
+}
+
+static bool check_bitrate_vs_frame_dms(struct impl *this, size_t bitrate)
+{
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ size_t max_fragments = 0xf;
+ size_t payload_size = BUFSIZE_FROM_BITRATE(bitrate, this->e.frame_dms);
+ return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
+}
+
+static int parse_frame_dms(int bitfield)
+{
+ switch (bitfield) {
+ case OPUS_05_FRAME_DURATION_25:
+ return 25;
+ case OPUS_05_FRAME_DURATION_50:
+ return 50;
+ case OPUS_05_FRAME_DURATION_100:
+ return 100;
+ case OPUS_05_FRAME_DURATION_200:
+ return 200;
+ case OPUS_05_FRAME_DURATION_400:
+ return 400;
+ default:
+ return -EINVAL;
+ }
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p;
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ return NULL;
+
+ p = calloc(1, sizeof(struct props));
+ if (p == NULL)
+ return NULL;
+
+ parse_settings(p, settings);
+
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ a2dp_opus_05_t *conf = config;
+ a2dp_opus_05_direction_t *dir;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ const uint8_t *enc_mapping = NULL;
+ unsigned char mapping[256];
+ size_t i;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_F32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ this->is_bidi = is_duplex_codec(codec);
+ dir = !this->is_bidi ? &conf->main : &conf->bidi;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+ if ((res = get_mapping(codec, dir, surround_encoder, &this->streams, &this->coupled_streams,
+ &enc_mapping, NULL)) < 0)
+ goto error;
+ if (config_info.info.raw.channels != info->info.raw.channels) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->mtu = mtu;
+ this->samplerate = info->info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->application = OPUS_APPLICATION_AUDIO;
+
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO && props) {
+ struct props *p = props;
+ this->application = !this->is_bidi ? p->application :
+ p->bidi_application;
+ }
+
+ /*
+ * Setup encoder
+ */
+ if (enc_mapping) {
+ int streams, coupled_streams;
+ bool incompatible_opus_surround_encoder = false;
+
+ this->enc = opus_multistream_surround_encoder_create(
+ this->samplerate, this->channels, 1, &streams, &coupled_streams,
+ mapping, this->application, &res);
+
+ if (this->enc) {
+ /* Check surround encoder channel mapping is what we want */
+ if (streams != this->streams || coupled_streams != this->coupled_streams)
+ incompatible_opus_surround_encoder = true;
+ for (i = 0; i < this->channels; ++i)
+ if (enc_mapping[i] != mapping[i])
+ incompatible_opus_surround_encoder = true;
+ }
+
+ /* Assert: this should never happen */
+ spa_assert(!incompatible_opus_surround_encoder);
+ if (incompatible_opus_surround_encoder) {
+ res = -EINVAL;
+ goto error;
+ }
+ } else {
+ for (i = 0; i < this->channels; ++i)
+ mapping[i] = i;
+ this->enc = opus_multistream_encoder_create(
+ this->samplerate, this->channels, this->streams, this->coupled_streams,
+ mapping, this->application, &res);
+ }
+ if (this->enc == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this->e.frame_dms = parse_frame_dms(dir->frame_duration)) < 0) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+ get_default_bitrates(codec, this->is_bidi, &this->e.bitrate_min,
+ &this->e.bitrate_max, &this->e.bitrate);
+ this->e.bitrate_max = SPA_MIN(this->e.bitrate_max,
+ OPUS_05_GET_BITRATE(*dir) * 1024);
+ } else {
+ this->e.bitrate_max = OPUS_05_GET_BITRATE(*dir) * 1024;
+ this->e.bitrate_min = BITRATE_MIN;
+ this->e.bitrate = BITRATE_INITIAL;
+ }
+
+ this->e.bitrate_min = SPA_MIN(this->e.bitrate_min, this->e.bitrate_max);
+ this->e.bitrate = SPA_CLAMP(this->e.bitrate, this->e.bitrate_min, this->e.bitrate_max);
+
+ this->e.next_bitrate = this->e.bitrate;
+ opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+
+ this->e.samples = this->e.frame_dms * this->samplerate / 10000;
+ this->e.codesize = this->e.samples * (int)this->channels * sizeof(float);
+
+
+ /*
+ * Setup decoder
+ */
+ for (i = 0; i < this->channels; ++i)
+ mapping[i] = i;
+ this->dec = opus_multistream_decoder_create(
+ this->samplerate, this->channels,
+ this->streams, this->coupled_streams,
+ mapping, &res);
+ if (this->dec == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this && this->enc)
+ opus_multistream_encoder_destroy(this->enc);
+ if (this && this->dec)
+ opus_multistream_decoder_destroy(this->dec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ opus_multistream_encoder_destroy(this->enc);
+ opus_multistream_decoder_destroy(this->dec);
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->e.codesize;
+}
+
+static int codec_update_bitrate(struct impl *this)
+{
+ this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate,
+ this->e.bitrate_min, this->e.bitrate_max);
+
+ if (!check_bitrate_vs_frame_dms(this, this->e.next_bitrate)) {
+ this->e.next_bitrate = this->e.bitrate;
+ return 0;
+ }
+
+ this->e.bitrate = this->e.next_bitrate;
+ opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+ return 0;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ if (dst_size <= header_size)
+ return -EINVAL;
+
+ codec_update_bitrate(this);
+
+ this->e.header = (struct rtp_header *)dst;
+ this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(dst, 0, header_size);
+
+ this->e.payload->frame_count = 0;
+ this->e.header->v = 2;
+ this->e.header->pt = 96;
+ this->e.header->sequence_number = htons(seqnum);
+ this->e.header->timestamp = htonl(timestamp);
+ this->e.header->ssrc = htonl(1);
+
+ this->e.packet_size = header_size;
+ return this->e.packet_size;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ const int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ int size;
+ int res;
+
+ if (src == NULL) {
+ /* Produce fragment packets.
+ *
+ * We assume the caller gives the same buffer here as in the previous
+ * calls to encode(), without changes in the buffer content.
+ */
+ if (this->e.fragment == NULL ||
+ this->e.fragment_count <= 1 ||
+ this->e.fragment < dst ||
+ SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) {
+ this->e.fragment = NULL;
+ return -EINVAL;
+ }
+
+ size = SPA_MIN(this->mtu - header_size, this->e.fragment_size);
+ memmove(dst, this->e.fragment, size);
+ *dst_out = size;
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->frame_count = --this->e.fragment_count;
+ this->e.payload->is_last_fragment = (this->e.fragment_count == 1);
+
+ if (this->e.fragment_size > size && this->e.fragment_count > 1) {
+ this->e.fragment = SPA_PTROFF(this->e.fragment, size, void);
+ this->e.fragment_size -= size;
+ *need_flush = NEED_FLUSH_FRAGMENT;
+ } else {
+ this->e.fragment = NULL;
+ *need_flush = NEED_FLUSH_ALL;
+ }
+ return 0;
+ }
+
+ if (src_size < (size_t)this->e.codesize) {
+ *dst_out = 0;
+ return 0;
+ }
+
+ res = opus_multistream_encode_float(
+ this->enc, src, this->e.samples, dst, dst_size);
+ if (res < 0)
+ return -EINVAL;
+ *dst_out = res;
+
+ this->e.packet_size += res;
+ this->e.payload->frame_count++;
+
+ if (this->e.packet_size > this->mtu) {
+ /* Fragment packet */
+ this->e.fragment_count = ceildiv(this->e.packet_size - header_size,
+ this->mtu - header_size);
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->is_first_fragment = 1;
+ this->e.payload->frame_count = this->e.fragment_count;
+
+ this->e.fragment_size = this->e.packet_size - this->mtu;
+ this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void);
+ *need_flush = NEED_FLUSH_FRAGMENT;
+
+ /*
+ * We keep the rest of the encoded frame in the same buffer, and rely
+ * that the caller won't overwrite it before the next call to encode()
+ */
+ *dst_out = SPA_PTRDIFF(this->e.fragment, dst);
+ } else {
+ *need_flush = NEED_FLUSH_ALL;
+ }
+
+ return this->e.codesize;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+ const struct rtp_header *header = src;
+ const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void);
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ if (payload->is_fragmented) {
+ if (payload->is_first_fragment) {
+ this->d.fragment_size = 0;
+ } else if (payload->frame_count + 1 != this->d.fragment_count ||
+ (payload->frame_count == 1 && !payload->is_last_fragment)){
+ /* Fragments not in right order: drop packet */
+ return -EINVAL;
+ }
+ this->d.fragment_count = payload->frame_count;
+ } else {
+ if (payload->frame_count != 1)
+ return -EINVAL;
+ this->d.fragment_count = 0;
+ }
+
+ return header_size;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int consumed = src_size;
+ int res;
+ int dst_samples;
+
+ if (this->d.fragment_count > 0) {
+ /* Fragmented frame */
+ size_t avail;
+ avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size);
+ memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail);
+
+ this->d.fragment_size += avail;
+
+ if (this->d.fragment_count > 1) {
+ /* More fragments to come */
+ *dst_out = 0;
+ return consumed;
+ }
+
+ src = this->d.fragment;
+ src_size = this->d.fragment_size;
+
+ this->d.fragment_count = 0;
+ this->d.fragment_size = 0;
+ }
+
+ dst_samples = dst_size / (sizeof(float) * this->channels);
+ res = opus_multistream_decode_float(this->dec, src, src_size, dst, dst_samples, 0);
+ if (res < 0)
+ return -EINVAL;
+ *dst_out = (size_t)res * this->channels * sizeof(float);
+
+ return consumed;
+}
+
+static int codec_abr_process(void *data, size_t unsent)
+{
+ const uint64_t interval = SPA_NSEC_PER_SEC;
+ struct impl *this = data;
+ struct abr *abr = &this->e.abr;
+ bool level_bad, level_good;
+ uint32_t actual_bitrate;
+
+ abr->total_size += this->e.packet_size;
+
+ if (this->e.payload->is_fragmented && !this->e.payload->is_first_fragment)
+ return 0;
+
+ abr->now += this->e.frame_dms * SPA_NSEC_PER_MSEC / 10;
+
+ abr->buffer_level = SPA_MAX(abr->buffer_level, unsent);
+ abr->packet_size = SPA_MAX(abr->packet_size, (uint32_t)this->e.packet_size);
+ abr->packet_size = SPA_MAX(abr->packet_size, 128u);
+
+ level_bad = abr->buffer_level > 2 * (uint32_t)this->mtu || abr->bad;
+ level_good = abr->buffer_level == 0;
+
+ if (!(abr->last_update + interval <= abr->now ||
+ (level_bad && abr->last_change + interval <= abr->now)))
+ return 0;
+
+ actual_bitrate = (uint64_t)abr->total_size*8*SPA_NSEC_PER_SEC
+ / SPA_MAX(1u, abr->now - abr->last_update);
+
+ spa_log_debug(log, "opus ABR bitrate:%d actual:%d level:%d (%s) bad:%d retry:%ds size:%d",
+ (int)this->e.bitrate,
+ (int)actual_bitrate,
+ (int)abr->buffer_level,
+ level_bad ? "bad" : (level_good ? "good" : "-"),
+ (int)abr->bad,
+ (int)(abr->retry_interval / SPA_NSEC_PER_SEC),
+ (int)abr->packet_size);
+
+ if (level_bad) {
+ this->e.next_bitrate = this->e.bitrate * 11 / 12;
+ abr->last_change = abr->now;
+ abr->retry_interval = SPA_MIN(abr->retry_interval + 10*interval,
+ 30 * interval);
+ } else if (!level_good) {
+ abr->last_change = abr->now;
+ } else if (abr->now < abr->last_change + abr->retry_interval) {
+ /* noop */
+ } else if (actual_bitrate*3/2 < (uint32_t)this->e.bitrate) {
+ /* actual bitrate is small compared to target; probably silence */
+ } else {
+ this->e.next_bitrate = this->e.bitrate
+ + SPA_MAX(1, this->e.bitrate_max / 40);
+ abr->last_change = abr->now;
+ abr->retry_interval = SPA_MAX(abr->retry_interval, (5+4)*interval)
+ - 4*interval;
+ }
+
+ abr->last_update = abr->now;
+ abr->buffer_level = 0;
+ abr->bad = false;
+ abr->packet_size = 0;
+ abr->total_size = 0;
+
+ return 0;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ struct abr *abr = &this->e.abr;
+ abr->bad = true;
+ return 0;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return 0;
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+ log = global_log;
+ spa_log_topic_init(log, &log_topic);
+}
+
+#define OPUS_05_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .vendor = { .vendor_id = OPUS_05_VENDOR_ID, \
+ .codec_id = OPUS_05_CODEC_ID }, \
+ .select_config = codec_select_config, \
+ .enum_config = codec_enum_config, \
+ .validate_config = codec_validate_config, \
+ .caps_preference_cmp = codec_caps_preference_cmp, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool, \
+ .set_log = codec_set_log
+
+#define OPUS_05_COMMON_FULL_DEFS \
+ OPUS_05_COMMON_DEFS, \
+ .start_decode = codec_start_decode, \
+ .decode = codec_decode
+
+const struct media_codec a2dp_codec_opus_05 = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+ .name = "opus_05",
+ .description = "Opus",
+ .fill_caps = codec_fill_caps,
+};
+
+const struct media_codec a2dp_codec_opus_05_51 = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+ .name = "opus_05_51",
+ .description = "Opus 5.1 Surround",
+ .endpoint_name = "opus_05",
+ .fill_caps = NULL,
+};
+
+const struct media_codec a2dp_codec_opus_05_71 = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+ .name = "opus_05_71",
+ .description = "Opus 7.1 Surround",
+ .endpoint_name = "opus_05",
+ .fill_caps = NULL,
+};
+
+/* Bidi return channel codec: doesn't have endpoints */
+const struct media_codec a2dp_codec_opus_05_return = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = 0,
+ .name = "opus_05_duplex_bidi",
+ .description = "Opus Duplex Bidi channel",
+};
+
+const struct media_codec a2dp_codec_opus_05_duplex = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+ .name = "opus_05_duplex",
+ .description = "Opus Duplex",
+ .duplex_codec = &a2dp_codec_opus_05_return,
+ .fill_caps = codec_fill_caps,
+};
+
+const struct media_codec a2dp_codec_opus_05_pro = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+ .name = "opus_05_pro",
+ .description = "Opus Pro Audio",
+ .init_props = codec_init_props,
+ .clear_props = codec_clear_props,
+ .duplex_codec = &a2dp_codec_opus_05_return,
+ .endpoint_name = "opus_05_duplex",
+ .fill_caps = NULL,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "opus",
+ &a2dp_codec_opus_05,
+ &a2dp_codec_opus_05_51,
+ &a2dp_codec_opus_05_71,
+ &a2dp_codec_opus_05_duplex,
+ &a2dp_codec_opus_05_pro
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c
new file mode 100644
index 0000000..27a57bd
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-sbc.c
@@ -0,0 +1,689 @@
+/* Spa A2DP SBC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/utils/string.h>
+
+#include <sbc/sbc.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define MAX_FRAME_COUNT 16
+
+struct impl {
+ sbc_t sbc;
+
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ size_t mtu;
+ int codesize;
+ int max_frames;
+
+ int min_bitpool;
+ int max_bitpool;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_sbc_t a2dp_sbc = {
+ .frequency =
+ SBC_SAMPLING_FREQ_16000 |
+ SBC_SAMPLING_FREQ_32000 |
+ SBC_SAMPLING_FREQ_44100 |
+ SBC_SAMPLING_FREQ_48000,
+ .channel_mode =
+ SBC_CHANNEL_MODE_MONO |
+ SBC_CHANNEL_MODE_DUAL_CHANNEL |
+ SBC_CHANNEL_MODE_STEREO |
+ SBC_CHANNEL_MODE_JOINT_STEREO,
+ .block_length =
+ SBC_BLOCK_LENGTH_4 |
+ SBC_BLOCK_LENGTH_8 |
+ SBC_BLOCK_LENGTH_12 |
+ SBC_BLOCK_LENGTH_16,
+ .subbands =
+ SBC_SUBBANDS_4 |
+ SBC_SUBBANDS_8,
+ .allocation_method =
+ SBC_ALLOCATION_SNR |
+ SBC_ALLOCATION_LOUDNESS,
+ .min_bitpool = SBC_MIN_BITPOOL,
+ .max_bitpool = SBC_MAX_BITPOOL,
+ };
+
+ memcpy(caps, &a2dp_sbc, sizeof(a2dp_sbc));
+ return sizeof(a2dp_sbc);
+}
+
+static uint8_t default_bitpool(uint8_t freq, uint8_t mode, bool xq)
+{
+ /* A2DP spec v1.2 states that all SNK implementation shall handle bitrates
+ * of up to 512 kbps (~ bitpool = 86 stereo, or 2x43 dual channel at 44.1KHz
+ * or ~ bitpool = 78 stereo, or 2x39 dual channel at 48KHz). */
+ switch (freq) {
+ case SBC_SAMPLING_FREQ_16000:
+ case SBC_SAMPLING_FREQ_32000:
+ return 64;
+
+ case SBC_SAMPLING_FREQ_44100:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return xq ? 43 : 32;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return xq ? 86 : 64;
+ }
+ return xq ? 86 : 64;
+ case SBC_SAMPLING_FREQ_48000:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return xq ? 39 : 29;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return xq ? 78 : 58;
+ }
+ return xq ? 78 : 58;
+ }
+ return xq ? 86 : 64;
+}
+
+
+static const struct media_codec_config
+sbc_frequencies[] = {
+ { SBC_SAMPLING_FREQ_48000, 48000, 3 },
+ { SBC_SAMPLING_FREQ_44100, 44100, 2 },
+ { SBC_SAMPLING_FREQ_32000, 32000, 1 },
+ { SBC_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static const struct media_codec_config
+sbc_xq_frequencies[] = {
+ { SBC_SAMPLING_FREQ_44100, 44100, 1 },
+ { SBC_SAMPLING_FREQ_48000, 48000, 0 },
+};
+
+static const struct media_codec_config
+sbc_channel_modes[] = {
+ { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 3 },
+ { SBC_CHANNEL_MODE_STEREO, 2, 2 },
+ { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 },
+ { SBC_CHANNEL_MODE_MONO, 1, 0 },
+};
+
+static const struct media_codec_config
+sbc_xq_channel_modes[] = {
+ { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 2 },
+ { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 1 },
+ { SBC_CHANNEL_MODE_STEREO, 2, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_sbc_t conf;
+ int bitpool, i;
+ size_t n;
+ const struct media_codec_config *configs;
+ bool xq = false;
+
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ xq = (spa_streq(codec->name, "sbc_xq"));
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (xq) {
+ configs = sbc_xq_frequencies;
+ n = SPA_N_ELEMENTS(sbc_xq_frequencies);
+ } else {
+ configs = sbc_frequencies;
+ n = SPA_N_ELEMENTS(sbc_frequencies);
+ }
+ if ((i = media_codec_select_config(configs, n, conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = configs[i].config;
+
+ if (xq) {
+ configs = sbc_xq_channel_modes;
+ n = SPA_N_ELEMENTS(sbc_xq_channel_modes);
+ } else {
+ configs = sbc_channel_modes;
+ n = SPA_N_ELEMENTS(sbc_channel_modes);
+ }
+ if ((i = media_codec_select_config(configs, n, conf.channel_mode,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channel_mode = configs[i].config;
+
+ if (conf.block_length & SBC_BLOCK_LENGTH_16)
+ conf.block_length = SBC_BLOCK_LENGTH_16;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_12)
+ conf.block_length = SBC_BLOCK_LENGTH_12;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_8)
+ conf.block_length = SBC_BLOCK_LENGTH_8;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_4)
+ conf.block_length = SBC_BLOCK_LENGTH_4;
+ else
+ return -ENOTSUP;
+
+ if (conf.subbands & SBC_SUBBANDS_8)
+ conf.subbands = SBC_SUBBANDS_8;
+ else if (conf.subbands & SBC_SUBBANDS_4)
+ conf.subbands = SBC_SUBBANDS_4;
+ else
+ return -ENOTSUP;
+
+ if (conf.allocation_method & SBC_ALLOCATION_LOUDNESS)
+ conf.allocation_method = SBC_ALLOCATION_LOUDNESS;
+ else if (conf.allocation_method & SBC_ALLOCATION_SNR)
+ conf.allocation_method = SBC_ALLOCATION_SNR;
+ else
+ return -ENOTSUP;
+
+ bitpool = default_bitpool(conf.frequency, conf.channel_mode, xq);
+
+ conf.min_bitpool = SPA_MAX(SBC_MIN_BITPOOL, conf.min_bitpool);
+ conf.max_bitpool = SPA_MIN(bitpool, conf.max_bitpool);
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ a2dp_sbc_t conf1, conf2;
+ a2dp_sbc_t *conf;
+ int res1, res2;
+ int a, b;
+ bool xq = (spa_streq(codec->name, "sbc_xq"));
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_sbc_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_sbc_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->frequency & (SBC_SAMPLING_FREQ_48000 | SBC_SAMPLING_FREQ_44100));
+
+ if (xq)
+ PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL);
+ else
+ PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO);
+
+ PREFER_EXPR(conf->max_bitpool);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const a2dp_sbc_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16;
+
+ switch (conf->frequency) {
+ case SBC_SAMPLING_FREQ_16000:
+ info->info.raw.rate = 16000;
+ break;
+ case SBC_SAMPLING_FREQ_32000:
+ info->info.raw.rate = 32000;
+ break;
+ case SBC_SAMPLING_FREQ_44100:
+ info->info.raw.rate = 44100;
+ break;
+ case SBC_SAMPLING_FREQ_48000:
+ info->info.raw.rate = 48000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->channel_mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ break;
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->subbands) {
+ case SBC_SUBBANDS_4:
+ case SBC_SUBBANDS_8:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->block_length) {
+ case SBC_BLOCK_LENGTH_4:
+ case SBC_BLOCK_LENGTH_8:
+ case SBC_BLOCK_LENGTH_12:
+ case SBC_BLOCK_LENGTH_16:
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int codec_set_bitpool(struct impl *this, int bitpool)
+{
+ size_t rtp_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ this->sbc.bitpool = SPA_CLAMP(bitpool, this->min_bitpool, this->max_bitpool);
+ this->codesize = sbc_get_codesize(&this->sbc);
+ this->max_frames = (this->mtu - rtp_size) / sbc_get_frame_length(&this->sbc);
+ if (this->max_frames > 15)
+ this->max_frames = 15;
+ return this->sbc.bitpool;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_sbc_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t i = 0;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & SBC_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_32000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 32000);
+ spa_pod_builder_int(b, 32000);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_16000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (conf.channel_mode & SBC_CHANNEL_MODE_MONO &&
+ conf.channel_mode & (SBC_CHANNEL_MODE_JOINT_STEREO |
+ SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_DUAL_CHANNEL)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & SBC_CHANNEL_MODE_MONO) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 1, position),
+ 0);
+ } else {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ }
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_set_bitpool(this, this->sbc.bitpool - 2);
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_set_bitpool(this, this->sbc.bitpool + 1);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct impl *this;
+ a2dp_sbc_t *conf = config;
+ int res;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ sbc_init(&this->sbc, 0);
+ this->sbc.endian = SBC_LE;
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->frequency) {
+ case SBC_SAMPLING_FREQ_16000:
+ this->sbc.frequency = SBC_FREQ_16000;
+ break;
+ case SBC_SAMPLING_FREQ_32000:
+ this->sbc.frequency = SBC_FREQ_32000;
+ break;
+ case SBC_SAMPLING_FREQ_44100:
+ this->sbc.frequency = SBC_FREQ_44100;
+ break;
+ case SBC_SAMPLING_FREQ_48000:
+ this->sbc.frequency = SBC_FREQ_48000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->channel_mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ this->sbc.mode = SBC_MODE_MONO;
+ break;
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ this->sbc.mode = SBC_MODE_DUAL_CHANNEL;
+ break;
+ case SBC_CHANNEL_MODE_STEREO:
+ this->sbc.mode = SBC_MODE_STEREO;
+ break;
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ this->sbc.mode = SBC_MODE_JOINT_STEREO;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->subbands) {
+ case SBC_SUBBANDS_4:
+ this->sbc.subbands = SBC_SB_4;
+ break;
+ case SBC_SUBBANDS_8:
+ this->sbc.subbands = SBC_SB_8;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (conf->allocation_method & SBC_ALLOCATION_LOUDNESS)
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ else
+ this->sbc.allocation = SBC_AM_SNR;
+
+ switch (conf->block_length) {
+ case SBC_BLOCK_LENGTH_4:
+ this->sbc.blocks = SBC_BLK_4;
+ break;
+ case SBC_BLOCK_LENGTH_8:
+ this->sbc.blocks = SBC_BLK_8;
+ break;
+ case SBC_BLOCK_LENGTH_12:
+ this->sbc.blocks = SBC_BLK_12;
+ break;
+ case SBC_BLOCK_LENGTH_16:
+ this->sbc.blocks = SBC_BLK_16;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->min_bitpool = SPA_MAX(conf->min_bitpool, 12);
+ this->max_bitpool = conf->max_bitpool;
+
+ codec_set_bitpool(this, conf->max_bitpool);
+
+ return this;
+error:
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload));
+
+ this->payload->frame_count = 0;
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_encode(&this->sbc, src, src_size,
+ dst, dst_size, (ssize_t*)dst_out);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ spa_assert(res == this->codesize);
+
+ this->payload->frame_count += res / this->codesize;
+ *need_flush = (this->payload->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_decode(&this->sbc, src, src_size,
+ dst, dst_size, dst_out);
+
+ return res;
+}
+
+const struct media_codec a2dp_codec_sbc = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC,
+ .codec_id = A2DP_CODEC_SBC,
+ .name = "sbc",
+ .description = "SBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+const struct media_codec a2dp_codec_sbc_xq = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
+ .codec_id = A2DP_CODEC_SBC,
+ .name = "sbc_xq",
+ .description = "SBC-XQ",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "sbc",
+ &a2dp_codec_sbc,
+ &a2dp_codec_sbc_xq
+);
diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c
new file mode 100644
index 0000000..93f512d
--- /dev/null
+++ b/spa/plugins/bluez5/backend-hsphfpd.c
@@ -0,0 +1,1588 @@
+/* Spa hsphfpd backend
+ *
+ * Based on previous work for pulseaudio by: Pali Rohár <pali.rohar@gmail.com>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.hsphfpd");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_dbus *dbus;
+ DBusConnection *conn;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_list endpoint_list;
+ bool endpoints_listed;
+
+ char *hsphfpd_service_id;
+
+ bool acquire_in_progress;
+
+ unsigned int filters_added:1;
+ unsigned int msbc_supported:1;
+};
+
+enum hsphfpd_volume_control {
+ HSPHFPD_VOLUME_CONTROL_NONE = 1,
+ HSPHFPD_VOLUME_CONTROL_LOCAL,
+ HSPHFPD_VOLUME_CONTROL_REMOTE,
+};
+
+enum hsphfpd_profile {
+ HSPHFPD_PROFILE_HEADSET = 1,
+ HSPHFPD_PROFILE_HANDSFREE,
+};
+
+enum hsphfpd_role {
+ HSPHFPD_ROLE_CLIENT = 1,
+ HSPHFPD_ROLE_GATEWAY,
+};
+
+struct hsphfpd_transport_data {
+ char *transport_path;
+ bool rx_soft_volume;
+ bool tx_soft_volume;
+ int rx_volume_gain;
+ int tx_volume_gain;
+ int max_rx_volume_gain;
+ int max_tx_volume_gain;
+ enum hsphfpd_volume_control rx_volume_control;
+ enum hsphfpd_volume_control tx_volume_control;
+};
+
+struct hsphfpd_endpoint {
+ struct spa_list link;
+ char *path;
+ bool valid;
+ bool connected;
+ char *remote_address;
+ char *local_address;
+ enum hsphfpd_profile profile;
+ enum hsphfpd_role role;
+ int air_codecs;
+};
+
+#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+#define HSPHFPD_APPLICATION_MANAGER_INTERFACE HSPHFPD_SERVICE ".ApplicationManager"
+#define HSPHFPD_ENDPOINT_INTERFACE HSPHFPD_SERVICE ".Endpoint"
+#define HSPHFPD_AUDIO_AGENT_INTERFACE HSPHFPD_SERVICE ".AudioAgent"
+#define HSPHFPD_AUDIO_TRANSPORT_INTERFACE HSPHFPD_SERVICE ".AudioTransport"
+
+#define APPLICATION_OBJECT_MANAGER_PATH "/Profile/hsphfpd/manager"
+#define HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ "/Profile/hsphfpd/pcm_s16le_8khz_agent"
+#define HSPHFP_AUDIO_CLIENT_MSBC "/Profile/hsphfpd/msbc_agent"
+
+#define HSPHFP_AIR_CODEC_CVSD "CVSD"
+#define HSPHFP_AIR_CODEC_MSBC "mSBC"
+#define HSPHFP_AGENT_CODEC_PCM "PCM_s16le_8kHz"
+#define HSPHFP_AGENT_CODEC_MSBC "mSBC"
+
+#define APPLICATION_OBJECT_MANAGER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"" DBUS_INTERFACE_OBJECTMANAGER "\">\n" \
+ " <method name=\"GetManagedObjects\">\n" \
+ " <arg name=\"objects\" direction=\"out\" type=\"a{oa{sa{sv}}}\"/>\n" \
+ " </method>\n" \
+ " <signal name=\"InterfacesAdded\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"a{sa{sv}}\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"InterfacesRemoved\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"as\"/>\n" \
+ " </signal>\n" \
+ " </interface>\n" \
+ " <interface name=\"" DBUS_INTERFACE_INTROSPECTABLE "\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#define AUDIO_AGENT_ENDPOINT_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"" HSPHFPD_AUDIO_AGENT_INTERFACE "\">\n" \
+ " <method name=\"NewConnection\">\n" \
+ " <arg name=\"audio_transport\" direction=\"in\" type=\"o\"/>\n" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>\n" \
+ " <arg name=\"properties\" direction=\"in\" type=\"a{sv}\"/>\n" \
+ " </method>\n" \
+ " <property name=\"AgentCodec\" type=\"s\" access=\"read\"/>\n" \
+ " </interface>\n" \
+ " <interface name=\"" DBUS_INTERFACE_INTROSPECTABLE "\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#define HSPHFPD_ERROR_INVALID_ARGUMENTS HSPHFPD_SERVICE ".Error.InvalidArguments"
+#define HSPHFPD_ERROR_ALREADY_EXISTS HSPHFPD_SERVICE ".Error.AlreadyExists"
+#define HSPHFPD_ERROR_DOES_NOT_EXISTS HSPHFPD_SERVICE ".Error.DoesNotExist"
+#define HSPHFPD_ERROR_NOT_CONNECTED HSPHFPD_SERVICE ".Error.NotConnected"
+#define HSPHFPD_ERROR_ALREADY_CONNECTED HSPHFPD_SERVICE ".Error.AlreadyConnected"
+#define HSPHFPD_ERROR_IN_PROGRESS HSPHFPD_SERVICE ".Error.InProgress"
+#define HSPHFPD_ERROR_IN_USE HSPHFPD_SERVICE ".Error.InUse"
+#define HSPHFPD_ERROR_NOT_SUPPORTED HSPHFPD_SERVICE ".Error.NotSupported"
+#define HSPHFPD_ERROR_NOT_AVAILABLE HSPHFPD_SERVICE ".Error.NotAvailable"
+#define HSPHFPD_ERROR_FAILED HSPHFPD_SERVICE ".Error.Failed"
+#define HSPHFPD_ERROR_REJECTED HSPHFPD_SERVICE ".Error.Rejected"
+#define HSPHFPD_ERROR_CANCELED HSPHFPD_SERVICE ".Error.Canceled"
+
+static struct hsphfpd_endpoint *endpoint_find(struct impl *backend, const char *path)
+{
+ struct hsphfpd_endpoint *d;
+ spa_list_for_each(d, &backend->endpoint_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+static void endpoint_free(struct hsphfpd_endpoint *endpoint)
+{
+ spa_list_remove(&endpoint->link);
+ free(endpoint->path);
+ if (endpoint->local_address)
+ free(endpoint->local_address);
+ if (endpoint->remote_address)
+ free(endpoint->remote_address);
+}
+
+static bool hsphfpd_cmp_transport_path(struct spa_bt_transport *t, const void *data)
+{
+ struct hsphfpd_transport_data *td = t->user_data;
+ if (spa_streq(td->transport_path, data))
+ return true;
+
+ return false;
+}
+
+static inline bool check_signature(DBusMessage *m, const char sig[])
+{
+ return spa_streq(dbus_message_get_signature(m), sig);
+}
+
+static int set_dbus_property(struct impl *backend,
+ const char *service,
+ const char *path,
+ const char *interface,
+ const char *property,
+ int type,
+ void *value)
+{
+ DBusMessage *m, *r;
+ DBusMessageIter iter;
+ DBusError err;
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "Set");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &interface, DBUS_TYPE_STRING, &property, DBUS_TYPE_INVALID);
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_append_basic(&iter, type, value);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_error(backend->log, "Transport Set() failed for transport %s (%s)", path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Set() returned error: %s", dbus_message_get_error_name(r));
+ return -EIO;
+ }
+
+ dbus_message_unref(r);
+ return 0;
+}
+
+static inline void set_rx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain)
+{
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport->fd < 0 || transport_data->rx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE)
+ return;
+ if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path,
+ HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "RxVolumeGain",
+ DBUS_TYPE_UINT16, &gain))
+ spa_log_error(backend->log, "Changing rx volume gain to %u for transport %s failed",
+ (unsigned)gain, transport_data->transport_path);
+}
+
+static inline void set_tx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain)
+{
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport->fd < 0 || transport_data->tx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE)
+ return;
+ if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path,
+ HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "TxVolumeGain",
+ DBUS_TYPE_UINT16, &gain))
+ spa_log_error(backend->log, "Changing tx volume gain to %u for transport %s failed",
+ (unsigned)gain, transport_data->transport_path);
+}
+
+static void parse_transport_properties_values(struct impl *backend,
+ const char *transport_path,
+ DBusMessageIter *i,
+ const char **endpoint_path,
+ const char **air_codec,
+ enum hsphfpd_volume_control *rx_volume_control,
+ enum hsphfpd_volume_control *tx_volume_control,
+ uint16_t *rx_volume_gain,
+ uint16_t *tx_volume_gain,
+ uint16_t *mtu)
+{
+ DBusMessageIter element_i;
+
+ spa_assert(i);
+
+ dbus_message_iter_recurse(i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter dict_i, variant_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &dict_i);
+
+ if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_STRING) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ dbus_message_iter_get_basic(&dict_i, &key);
+
+ if (!dbus_message_iter_next(&dict_i)) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_VARIANT) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ dbus_message_iter_recurse(&dict_i, &variant_i);
+
+ switch (dbus_message_iter_get_arg_type(&variant_i)) {
+ case DBUS_TYPE_STRING:
+ if (spa_streq(key, "RxVolumeControl") || spa_streq(key, "TxVolumeControl")) {
+ const char *value;
+ enum hsphfpd_volume_control volume_control;
+
+ dbus_message_iter_get_basic(&variant_i, &value);
+ if (spa_streq(value, "none"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_NONE;
+ else if (spa_streq(value, "local"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_LOCAL;
+ else if (spa_streq(value, "remote"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_REMOTE;
+ else
+ volume_control = 0;
+
+ if (!volume_control)
+ spa_log_warn(backend->log, "Transport %s received invalid '%s' property value '%s', ignoring", transport_path, key, value);
+ else if (spa_streq(key, "RxVolumeControl"))
+ *rx_volume_control = volume_control;
+ else if (spa_streq(key, "TxVolumeControl"))
+ *tx_volume_control = volume_control;
+ } else if (spa_streq(key, "AirCodec"))
+ dbus_message_iter_get_basic(&variant_i, air_codec);
+ break;
+
+ case DBUS_TYPE_UINT16:
+ if (spa_streq(key, "MTU"))
+ dbus_message_iter_get_basic(&variant_i, mtu);
+ else if (spa_streq(key, "RxVolumeGain"))
+ dbus_message_iter_get_basic(&variant_i, rx_volume_gain);
+ else if (spa_streq(key, "TxVolumeGain"))
+ dbus_message_iter_get_basic(&variant_i, tx_volume_gain);
+ break;
+
+ case DBUS_TYPE_OBJECT_PATH:
+ if (spa_streq(key, "Endpoint"))
+ dbus_message_iter_get_basic(&variant_i, endpoint_path);
+ break;
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+}
+
+static void hsphfpd_parse_transport_properties(struct impl *backend, struct spa_bt_transport *transport, DBusMessageIter *i)
+{
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+ const char *endpoint_path = NULL;
+ const char *air_codec = NULL;
+ enum hsphfpd_volume_control rx_volume_control = 0;
+ enum hsphfpd_volume_control tx_volume_control = 0;
+ uint16_t rx_volume_gain = -1;
+ uint16_t tx_volume_gain = -1;
+ uint16_t mtu = 0;
+ bool rx_volume_gain_changed = false;
+ bool tx_volume_gain_changed = false;
+ bool rx_volume_control_changed = false;
+ bool tx_volume_control_changed = false;
+ bool rx_soft_volume_changed = false;
+ bool tx_soft_volume_changed = false;
+
+ parse_transport_properties_values(backend, transport_data->transport_path, i, &endpoint_path,
+ &air_codec, &rx_volume_control, &tx_volume_control,
+ &rx_volume_gain, &tx_volume_gain, &mtu);
+
+ if (endpoint_path)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "Endpoint");
+
+ if (air_codec)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "AirCodec");
+
+ if (mtu)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "MTU");
+
+ if (rx_volume_control) {
+ if (!!transport_data->rx_soft_volume != !!(rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) {
+ spa_log_info(backend->log, "Transport %s changed rx soft volume from %d to %d",
+ transport_data->transport_path, transport_data->rx_soft_volume,
+ (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE));
+ transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ rx_soft_volume_changed = true;
+ }
+ if (transport_data->rx_volume_control != rx_volume_control) {
+ transport_data->rx_volume_control = rx_volume_control;
+ rx_volume_control_changed = true;
+ }
+ }
+
+ if (tx_volume_control) {
+ if (!!transport_data->tx_soft_volume != !!(tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) {
+ spa_log_info(backend->log, "Transport %s changed tx soft volume from %d to %d",
+ transport_data->transport_path, transport_data->rx_soft_volume,
+ (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE));
+ transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ tx_soft_volume_changed = true;
+ }
+ if (transport_data->tx_volume_control != tx_volume_control) {
+ transport_data->tx_volume_control = tx_volume_control;
+ tx_volume_control_changed = true;
+ }
+ }
+
+ if (rx_volume_gain != (uint16_t)-1) {
+ if (transport_data->rx_volume_gain != rx_volume_gain) {
+ spa_log_info(backend->log, "Transport %s changed rx volume gain from %u to %u",
+ transport_data->transport_path, (unsigned)transport_data->rx_volume_gain, (unsigned)rx_volume_gain);
+ transport_data->rx_volume_gain = rx_volume_gain;
+ rx_volume_gain_changed = true;
+ }
+ }
+
+ if (tx_volume_gain != (uint16_t)-1) {
+ if (transport_data->tx_volume_gain != tx_volume_gain) {
+ spa_log_info(backend->log, "Transport %s changed tx volume gain from %u to %u",
+ transport_data->transport_path, (unsigned)transport_data->tx_volume_gain, (unsigned)tx_volume_gain);
+ transport_data->tx_volume_gain = tx_volume_gain;
+ tx_volume_gain_changed = true;
+ }
+ }
+
+#if 0
+ if (rx_volume_gain_changed || rx_soft_volume_changed)
+ pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport);
+
+ if (tx_volume_gain_changed || tx_soft_volume_changed)
+ pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport);
+#else
+ spa_log_debug(backend->log, "RX volume gain changed: %d, soft volume changed: %d", rx_volume_gain_changed, rx_soft_volume_changed);
+ spa_log_debug(backend->log, "TX volume gain changed: %d, soft volume changed: %d", tx_volume_gain_changed, tx_soft_volume_changed);
+#endif
+
+ if (rx_volume_control_changed)
+ set_rx_volume_gain_property(transport, transport_data->rx_volume_gain);
+
+ if (tx_volume_control_changed)
+ set_tx_volume_gain_property(transport, transport_data->tx_volume_gain);
+}
+
+static DBusHandlerResult audio_agent_get_property(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ const char *interface;
+ const char *property;
+ const char *agent_codec;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "ss")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_STRING, &property,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!spa_streq(property, "AgentCodec")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid property in method call");
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call");
+ goto fail;
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+fail:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult audio_agent_getall_properties(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ const char *interface;
+ DBusMessageIter iter, array, dict, data;
+ const char *agent_codec_key = "AgentCodec";
+ const char *agent_codec;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "s")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call");
+ goto fail;
+ }
+
+ if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array);
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &dict);
+ dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &agent_codec_key);
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &data);
+ dbus_message_iter_append_basic(&data, DBUS_TYPE_BOOLEAN, &agent_codec);
+ dbus_message_iter_close_container(&dict, &data);
+ dbus_message_iter_close_container(&array, &dict);
+ dbus_message_iter_close_container(&iter, &array);
+
+fail:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult hsphfpd_new_audio_connection(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessageIter arg_i;
+ const char *transport_path;
+ int fd;
+ const char *sender;
+ const char *endpoint_path = NULL;
+ const char *air_codec = NULL;
+ enum hsphfpd_volume_control rx_volume_control = 0;
+ enum hsphfpd_volume_control tx_volume_control = 0;
+ uint16_t rx_volume_gain = -1;
+ uint16_t tx_volume_gain = -1;
+ uint16_t mtu = 0;
+ unsigned int codec;
+ struct hsphfpd_endpoint *endpoint;
+ struct spa_bt_transport *transport;
+ struct hsphfpd_transport_data *transport_data;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "oha{sv}")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (!dbus_message_iter_init(m, &arg_i))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_get_basic(&arg_i, &transport_path);
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_get_basic(&arg_i, &fd);
+
+ spa_log_debug(backend->log, "NewConnection %s, fd %d", transport_path, fd);
+
+ sender = dbus_message_get_sender(m);
+ if (!spa_streq(sender, backend->hsphfpd_service_id)) {
+ close(fd);
+ spa_log_error(backend->log, "Sender '%s' is not authorized", sender);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Sender '%s' is not authorized", sender);
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ codec = HFP_AUDIO_CODEC_CVSD;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ codec = HFP_AUDIO_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Invalid path");
+ goto fail;
+ }
+
+ dbus_message_iter_next(&arg_i);
+ parse_transport_properties_values(backend, transport_path, &arg_i,
+ &endpoint_path, &air_codec,
+ &rx_volume_control, &tx_volume_control,
+ &rx_volume_gain, &tx_volume_gain,
+ &mtu);
+
+ if (!endpoint_path) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Endpoint property was not specified");
+ goto fail;
+ }
+
+ if (!air_codec) {
+ close(fd);
+ spa_log_error(backend->log, "AirCodec property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "AirCodec property was not specified");
+ goto fail;
+ }
+
+ if (!rx_volume_control) {
+ close(fd);
+ spa_log_error(backend->log, "RxVolumeControl property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeControl property was not specified");
+ goto fail;
+ }
+
+ if (!tx_volume_control) {
+ close(fd);
+ spa_log_error(backend->log, "TxVolumeControl property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeControl property was not specified");
+ goto fail;
+ }
+
+ if (rx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) {
+ if (rx_volume_gain == (uint16_t)-1) {
+ close(fd);
+ spa_log_error(backend->log, "RxVolumeGain property was not specified, but VolumeControl is not none");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeGain property was not specified, but VolumeControl is not none");
+ goto fail;
+ }
+ } else {
+ rx_volume_gain = 15; /* No volume control, so set maximal value */
+ }
+
+ if (tx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) {
+ if (tx_volume_gain == (uint16_t)-1) {
+ close(fd);
+ spa_log_error(backend->log, "TxVolumeGain property was not specified, but VolumeControl is not none");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeGain property was not specified, but VolumeControl is not none");
+ goto fail;
+ }
+ } else {
+ tx_volume_gain = 15; /* No volume control, so set maximal value */
+ }
+
+ if (!mtu) {
+ close(fd);
+ spa_log_error(backend->log, "MTU property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "MTU property was not specified");
+ goto fail;
+ }
+
+ endpoint = endpoint_find(backend, endpoint_path);
+ if (!endpoint) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s does not exist", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s does not exist", endpoint_path);
+ goto fail;
+ }
+
+ if (!endpoint->valid) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s is not valid", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not valid", endpoint_path);
+ goto fail;
+ }
+
+ transport = spa_bt_transport_find(backend->monitor, endpoint_path);
+ if (!transport) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s is not connected", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not connected", endpoint_path);
+ goto fail;
+ }
+
+ if (transport->codec != codec)
+ spa_log_warn(backend->log, "Expecting codec to be %d, got %d", transport->codec, codec);
+
+ if (transport->fd >= 0) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s has already active transport", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s has already active transport", endpoint_path);
+ goto fail;
+ }
+
+ transport_data = transport->user_data;
+ transport_data->transport_path = strdup(transport_path);
+ transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ transport_data->rx_volume_gain = rx_volume_gain;
+ transport_data->tx_volume_gain = tx_volume_gain;
+ transport_data->rx_volume_control = rx_volume_control;
+ transport_data->tx_volume_control = tx_volume_control;
+
+#if 0
+ pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport);
+ pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport);
+#endif
+
+ transport->read_mtu = mtu;
+ transport->write_mtu = mtu;
+
+ transport->fd = fd;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+fail:
+ if (r) {
+ DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ res = DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+ return res;
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult audio_agent_endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = AUDIO_AGENT_ENDPOINT_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get"))
+ res = audio_agent_get_property(c, m, path, userdata);
+ else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll"))
+ res = audio_agent_getall_properties(c, m, path, userdata);
+ else if (dbus_message_is_method_call(m, HSPHFPD_AUDIO_AGENT_INTERFACE, "NewConnection"))
+ res = hsphfpd_new_audio_connection(c, m, path, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void append_audio_agent_object(DBusMessageIter *iter, const char *endpoint, const char *agent_codec)
+{
+ const char *interface_name = HSPHFPD_AUDIO_AGENT_INTERFACE;
+ DBusMessageIter object, array, entry, dict, codec, data;
+ char *str = "AgentCodec";
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object);
+ dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint);
+
+ dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array);
+
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name);
+
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &codec);
+ dbus_message_iter_append_basic(&codec, DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&codec, DBUS_TYPE_VARIANT, "s", &data);
+ dbus_message_iter_append_basic(&data, DBUS_TYPE_STRING, &agent_codec);
+ dbus_message_iter_close_container(&codec, &data);
+ dbus_message_iter_close_container(&dict, &codec);
+
+ dbus_message_iter_close_container(&entry, &dict);
+ dbus_message_iter_close_container(&array, &entry);
+ dbus_message_iter_close_container(&object, &array);
+ dbus_message_iter_close_container(iter, &object);
+}
+
+static DBusHandlerResult application_object_manager_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = APPLICATION_OBJECT_MANAGER_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects")) {
+ DBusMessageIter iter, array;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_init_append(r, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array);
+
+ append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ, HSPHFP_AGENT_CODEC_PCM);
+ if (backend->msbc_supported)
+ append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_MSBC, HSPHFP_AGENT_CODEC_MSBC);
+
+ dbus_message_iter_close_container(&iter, &array);
+ } else
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void hsphfpd_audio_acquire_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ const char *transport_path;
+ const char *service_id;
+ const char *agent_path;
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ backend->acquire_in_progress = false;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) {
+ spa_log_error(backend->log, "Reply for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() from invalid sender");
+ goto finish;
+ }
+
+ if (!check_signature(r, "oso")) {
+ spa_log_error(backend->log, "Invalid reply signature for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio()");
+ goto finish;
+ }
+
+ if (dbus_message_get_args(r, &error,
+ DBUS_TYPE_OBJECT_PATH, &transport_path,
+ DBUS_TYPE_STRING, &service_id,
+ DBUS_TYPE_OBJECT_PATH, &agent_path,
+ DBUS_TYPE_INVALID) == FALSE) {
+ spa_log_error(backend->log, "Failed to parse " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() reply: %s", error.message);
+ goto finish;
+ }
+
+ if (!spa_streq(service_id, dbus_bus_get_unique_name(backend->conn))) {
+ spa_log_warn(backend->log, HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() failed: Other audio application took audio socket");
+ goto finish;
+ }
+
+ spa_log_debug(backend->log, "hsphfpd audio acquired");
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int hsphfpd_audio_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ DBusMessage *m;
+ const char *air_codec = HSPHFP_AIR_CODEC_CVSD;
+ const char *agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ DBusPendingCall *call;
+ DBusError err;
+
+ spa_log_debug(backend->log, "transport %p: Acquire %s",
+ transport, transport->path);
+
+ if (backend->acquire_in_progress)
+ return -EINPROGRESS;
+
+ if (transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ air_codec = HSPHFP_AIR_CODEC_MSBC;
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ }
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE,
+ transport->path,
+ HSPHFPD_ENDPOINT_INTERFACE,
+ "ConnectAudio");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &air_codec, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, hsphfpd_audio_acquire_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ /* The ConnectAudio method triggers Introspect and NewConnection calls,
+ which will set the fd to use for the SCO data.
+ We need to run the DBus loop to be able to reply to those method calls */
+ backend->acquire_in_progress = true;
+ while (backend->acquire_in_progress && dbus_connection_read_write_dispatch(backend->conn, -1))
+ ; // empty loop body
+
+ return 0;
+}
+
+static int hsphfpd_audio_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ spa_log_debug(backend->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ if (transport_data->transport_path) {
+ free(transport_data->transport_path);
+ transport_data->transport_path = NULL;
+ }
+ transport->fd = -1;
+
+ return 0;
+}
+
+static int hsphfpd_audio_destroy(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport_data->transport_path) {
+ free(transport_data->transport_path);
+ transport_data->transport_path = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_bt_transport_implementation hsphfpd_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = hsphfpd_audio_acquire,
+ .release = hsphfpd_audio_release,
+ .destroy = hsphfpd_audio_destroy,
+};
+
+static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, struct hsphfpd_endpoint *endpoint, DBusMessageIter *i)
+{
+ DBusMessageIter element_i;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t;
+
+ dbus_message_iter_recurse(i, &element_i);
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter dict_i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &dict_i);
+ dbus_message_iter_get_basic(&dict_i, &key);
+ dbus_message_iter_next(&dict_i);
+ dbus_message_iter_recurse(&dict_i, &value_i);
+ switch (dbus_message_iter_get_arg_type(&value_i)) {
+ case DBUS_TYPE_STRING:
+ {
+ const char *value;
+ dbus_message_iter_get_basic(&value_i, &value);
+ if (spa_streq(key, "RemoteAddress"))
+ endpoint->remote_address = strdup(value);
+ else if (spa_streq(key, "LocalAddress"))
+ endpoint->local_address = strdup(value);
+ else if (spa_streq(key, "Profile")) {
+ if (endpoint->profile)
+ spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key);
+ else if (spa_streq(value, "headset"))
+ endpoint->profile = HSPHFPD_PROFILE_HEADSET;
+ else if (spa_streq(value, "handsfree"))
+ endpoint->profile = HSPHFPD_PROFILE_HANDSFREE;
+ else
+ spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value);
+ } else if (spa_streq(key, "Role")) {
+ if (endpoint->role)
+ spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key);
+ else if (spa_streq(value, "client"))
+ endpoint->role = HSPHFPD_ROLE_CLIENT;
+ else if (spa_streq(value, "gateway"))
+ endpoint->role = HSPHFPD_ROLE_GATEWAY;
+ else
+ spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value);
+ }
+ spa_log_trace(backend->log, " %s: %s (%p)", key, value, endpoint);
+ }
+ break;
+
+ case DBUS_TYPE_BOOLEAN:
+ {
+ bool value;
+ dbus_message_iter_get_basic(&value_i, &value);
+ if (spa_streq(key, "Connected"))
+ endpoint->connected = value;
+ spa_log_trace(backend->log, " %s: %d", key, value);
+ }
+ break;
+
+ case DBUS_TYPE_ARRAY:
+ {
+ if (spa_streq(key, "AudioCodecs")) {
+ DBusMessageIter array_i;
+ const char *value;
+
+ endpoint->air_codecs = 0;
+ dbus_message_iter_recurse(&value_i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ dbus_message_iter_get_basic(&array_i, &value);
+ if (spa_streq(value, HSPHFP_AIR_CODEC_CVSD))
+ endpoint->air_codecs |= HFP_AUDIO_CODEC_CVSD;
+ if (spa_streq(value, HSPHFP_AIR_CODEC_MSBC))
+ endpoint->air_codecs |= HFP_AUDIO_CODEC_MSBC;
+ dbus_message_iter_next(&array_i);
+ }
+ }
+ }
+ break;
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+
+ if (!endpoint->valid && endpoint->local_address && endpoint->remote_address && endpoint->profile && endpoint->role)
+ endpoint->valid = true;
+
+ if (!endpoint->remote_address || !endpoint->local_address) {
+ spa_log_debug(backend->log, "Missing addresses for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ d = spa_bt_device_find_by_address(backend->monitor, endpoint->remote_address, endpoint->local_address);
+ if (!d || !d->adapter) {
+ spa_log_debug(backend->log, "No device for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ if ((t = spa_bt_transport_find(backend->monitor, endpoint->path)) != NULL) {
+ /* Release transport on disconnection, or when mSBC is supported if there
+ is an update of the remote codecs */
+ if (!endpoint->connected || (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC) && t->codec == HFP_AUDIO_CODEC_CVSD)) {
+ spa_bt_transport_free(t);
+ spa_bt_device_check_profiles(d, false);
+ spa_log_debug(backend->log, "Transport released for %s", endpoint->path);
+ } else {
+ spa_log_debug(backend->log, "Transport already configured for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ }
+
+ if (!endpoint->valid || !endpoint->connected)
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+ char *t_path = strdup(endpoint->path);
+ t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct hsphfpd_transport_data));
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ free(t_path);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ spa_bt_transport_set_implementation(t, &hsphfpd_transport_impl, t);
+
+ t->device = d;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->backend = &backend->this;
+ t->profile = SPA_BT_PROFILE_NULL;
+ if (endpoint->profile == HSPHFPD_PROFILE_HEADSET) {
+ if (endpoint->role == HSPHFPD_ROLE_CLIENT)
+ t->profile = SPA_BT_PROFILE_HSP_HS;
+ else if (endpoint->role == HSPHFPD_ROLE_GATEWAY)
+ t->profile = SPA_BT_PROFILE_HSP_AG;
+ } else if (endpoint->profile == HSPHFPD_PROFILE_HANDSFREE) {
+ if (endpoint->role == HSPHFPD_ROLE_CLIENT)
+ t->profile = SPA_BT_PROFILE_HFP_HF;
+ else if (endpoint->role == HSPHFPD_ROLE_GATEWAY)
+ t->profile = SPA_BT_PROFILE_HFP_AG;
+ }
+ if (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC))
+ t->codec = HFP_AUDIO_CODEC_MSBC;
+ else
+ t->codec = HFP_AUDIO_CODEC_CVSD;
+
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ spa_bt_device_add_profile(d, t->profile);
+ spa_bt_device_connect_profile(t->device, t->profile);
+
+ spa_log_debug(backend->log, "Transport %s available for hsphfpd", endpoint->path);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult hsphfpd_parse_interfaces(struct impl *backend, DBusMessageIter *dict_i)
+{
+ DBusMessageIter element_i;
+ const char *path;
+
+ spa_assert(backend);
+ spa_assert(dict_i);
+
+ dbus_message_iter_get_basic(dict_i, &path);
+ dbus_message_iter_next(dict_i);
+ dbus_message_iter_recurse(dict_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter iface_i;
+ const char *interface;
+
+ dbus_message_iter_recurse(&element_i, &iface_i);
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+
+ if (spa_streq(interface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint;
+
+ endpoint = endpoint_find(backend, path);
+ if (!endpoint) {
+ endpoint = calloc(1, sizeof(struct hsphfpd_endpoint));
+ endpoint->path = strdup(path);
+ spa_list_append(&backend->endpoint_list, &endpoint->link);
+ spa_log_debug(backend->log, "Found endpoint %s", path);
+ }
+ hsphfpd_parse_endpoint_properties(backend, endpoint, &iface_i);
+ } else
+ spa_log_debug(backend->log, "Unknown interface %s found, skipping", interface);
+
+ dbus_message_iter_next(&element_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void hsphfpd_get_endpoints_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get a list of endpoints from hsphfpd: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) {
+ spa_log_error(backend->log, "Reply for GetManagedObjects() from invalid sender");
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !check_signature(r, "a{oa{sa{sv}}}")) {
+ spa_log_error(backend->log, "Invalid arguments in GetManagedObjects() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter dict_i;
+
+ dbus_message_iter_recurse(&array_i, &dict_i);
+ hsphfpd_parse_interfaces(backend, &dict_i);
+ dbus_message_iter_next(&array_i);
+ }
+
+ backend->endpoints_listed = true;
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int backend_hsphfpd_register(void *data)
+{
+ struct impl *backend = data;
+ DBusMessage *m, *r;
+ const char *path = APPLICATION_OBJECT_MANAGER_PATH;
+ DBusPendingCall *call;
+ DBusError err;
+ int res;
+
+ spa_log_debug(backend->log, "Registering to hsphfpd");
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ HSPHFPD_APPLICATION_MANAGER_INTERFACE, "RegisterApplication");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+ spa_log_info(backend->log, "hsphfpd not available: %s",
+ err.message);
+ res = -ENOTSUP;
+ } else {
+ spa_log_warn(backend->log, "Registering application %s failed: %s (%s)",
+ path, err.message, err.name);
+ res = -EIO;
+ }
+ dbus_error_free(&err);
+ return res;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+ dbus_message_unref(r);
+
+ backend->hsphfpd_service_id = strdup(dbus_message_get_sender(r));
+
+ spa_log_debug(backend->log, "Registered to hsphfpd");
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects");
+ if (m == NULL)
+ goto finish;
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, hsphfpd_get_endpoints_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+
+finish:
+ dbus_message_unref(r);
+ return -EIO;
+}
+
+static int backend_hsphfpd_unregistered(void *data)
+{
+ struct impl *backend = data;
+ struct hsphfpd_endpoint *endpoint;
+
+ if (backend->hsphfpd_service_id) {
+ free(backend->hsphfpd_service_id);
+ backend->hsphfpd_service_id = NULL;
+ }
+ backend->endpoints_listed = false;
+ spa_list_consume(endpoint, &backend->endpoint_list, link)
+ endpoint_free(endpoint);
+
+ return 0;
+}
+
+static DBusHandlerResult hsphfpd_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ const char *sender;
+ struct impl *backend = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ sender = dbus_message_get_sender(m);
+
+ if (backend->hsphfpd_service_id && spa_streq(sender, backend->hsphfpd_service_id)) {
+ if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesAdded")) {
+ DBusMessageIter arg_i;
+
+ spa_log_warn(backend->log, "sender: %s", dbus_message_get_sender(m));
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oa{sa{sv}}")) {
+ spa_log_error(backend->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ hsphfpd_parse_interfaces(backend, &arg_i);
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesRemoved")) {
+ const char *path;
+ DBusMessageIter arg_i, element_i;
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oas")) {
+ spa_log_error(backend->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &path);
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_recurse(&arg_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) {
+ const char *iface;
+
+ dbus_message_iter_get_basic(&element_i, &iface);
+
+ if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint;
+ struct spa_bt_transport *transport = spa_bt_transport_find(backend->monitor, path);
+
+ if (transport)
+ spa_bt_transport_free(transport);
+
+ spa_log_debug(backend->log, "Remove endpoint %s", path);
+ endpoint = endpoint_find(backend, path);
+ if (endpoint)
+ endpoint_free(endpoint);
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged")) {
+ DBusMessageIter arg_i;
+ const char *iface;
+ const char *path;
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "sa{sv}as")) {
+ spa_log_error(backend->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &iface);
+ dbus_message_iter_next(&arg_i);
+
+ path = dbus_message_get_path(m);
+
+ if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint = endpoint_find(backend, path);
+ if (!endpoint) {
+ spa_log_warn(backend->log, "Properties changed on unknown endpoint %s", path);
+ goto finish;
+ }
+ spa_log_debug(backend->log, "Properties changed on endpoint %s", path);
+ hsphfpd_parse_endpoint_properties(backend, endpoint, &arg_i);
+ } else if (spa_streq(iface, HSPHFPD_AUDIO_TRANSPORT_INTERFACE)) {
+ struct spa_bt_transport *transport = spa_bt_transport_find_full(backend->monitor,
+ hsphfpd_cmp_transport_path,
+ (const void *)path);
+ if (!transport) {
+ spa_log_warn(backend->log, "Properties changed on unknown transport %s", path);
+ goto finish;
+ }
+ spa_log_debug(backend->log, "Properties changed on transport %s", path);
+ hsphfpd_parse_transport_properties(backend, transport, &arg_i);
+ }
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(void *data)
+{
+ struct impl *backend = data;
+ DBusError err;
+
+ if (backend->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(backend->conn, hsphfpd_filter_cb, backend, NULL)) {
+ spa_log_error(backend->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesAdded'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesRemoved'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged',"
+ "arg0='" HSPHFPD_ENDPOINT_INTERFACE "'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged',"
+ "arg0='" HSPHFPD_AUDIO_TRANSPORT_INTERFACE "'", &err);
+
+ backend->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static int backend_hsphfpd_free(void *data)
+{
+ struct impl *backend = data;
+ struct hsphfpd_endpoint *endpoint;
+
+ if (backend->filters_added) {
+ dbus_connection_remove_filter(backend->conn, hsphfpd_filter_cb, backend);
+ backend->filters_added = false;
+ }
+
+ if (backend->msbc_supported)
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC);
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+
+ spa_list_consume(endpoint, &backend->endpoint_list, link)
+ endpoint_free(endpoint);
+
+ free(backend);
+
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_hsphfpd_free,
+ .register_profiles = backend_hsphfpd_register,
+ .unregister_profiles = backend_hsphfpd_unregistered,
+};
+
+static bool is_available(struct impl *backend)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ DBUS_INTERFACE_INTROSPECTABLE, "Introspect");
+ if (m == NULL)
+ return false;
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
+ success = true;
+
+ if (r)
+ dbus_message_unref(r);
+ else
+ dbus_error_free(&err);
+
+ return success;
+}
+
+struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+ const char *str;
+ static const DBusObjectPathVTable vtable_application_object_manager = {
+ .message_function = application_object_manager_handler,
+ };
+ static const DBusObjectPathVTable vtable_audio_agent_endpoint = {
+ .message_function = audio_agent_endpoint_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "hsphfpd";
+ backend->this.exclusive = true;
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->conn = dbus_connection;
+ if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc")))
+ backend->msbc_supported = spa_atob(str);
+ else
+ backend->msbc_supported = false;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ spa_list_init(&backend->endpoint_list);
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ APPLICATION_OBJECT_MANAGER_PATH,
+ &vtable_application_object_manager, backend)) {
+ free(backend);
+ return NULL;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ,
+ &vtable_audio_agent_endpoint, backend)) {
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ if (backend->msbc_supported && !dbus_connection_register_object_path(backend->conn,
+ HSPHFP_AUDIO_CLIENT_MSBC,
+ &vtable_audio_agent_endpoint, backend)) {
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ if (add_filters(backend) < 0) {
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC);
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ backend->this.available = is_available(backend);
+
+ return &backend->this;
+}
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c
new file mode 100644
index 0000000..5eec82c
--- /dev/null
+++ b/spa/plugins/bluez5/backend-native.c
@@ -0,0 +1,2838 @@
+/* Spa HSP/HFP native backend
+ *
+ * Copyright © 2018 Wim Taymans
+ * Copyright © 2021 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sco.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+#ifdef HAVE_LIBUSB
+#include <libusb.h>
+#endif
+
+#include "modemmanager.h"
+#include "upower.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles"
+
+#define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000
+#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000
+
+#define INTERNATIONAL_NUMBER 145
+#define NATIONAL_NUMBER 129
+
+#define MAX_HF_INDICATORS 16
+
+enum {
+ HFP_AG_INITIAL_CODEC_SETUP_NONE = 0,
+ HFP_AG_INITIAL_CODEC_SETUP_SEND,
+ HFP_AG_INITIAL_CODEC_SETUP_WAIT
+};
+
+#define CIND_INDICATORS "(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5)),(\"roam\",(0-1)),(\"battchg\",(0-5))"
+enum {
+ CIND_SERVICE = 1,
+ CIND_CALL,
+ CIND_CALLSETUP,
+ CIND_CALLHELD,
+ CIND_SIGNAL,
+ CIND_ROAM,
+ CIND_BATTERY_LEVEL,
+ CIND_MAX
+};
+
+struct modem {
+ bool network_has_service;
+ unsigned int signal_strength;
+ bool network_is_roaming;
+ char *operator_name;
+ char *own_number;
+ bool active_call;
+ unsigned int call_setup;
+};
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_loop_utils *loop_utils;
+ struct spa_dbus *dbus;
+ DBusConnection *conn;
+
+#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG)
+ enum spa_bt_profile enabled_profiles;
+
+ struct spa_source sco;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_list rfcomm_list;
+ unsigned int defer_setup_enabled:1;
+
+ struct modem modem;
+ unsigned int battery_level;
+
+ void *modemmanager;
+ struct spa_source *ring_timer;
+ void *upower;
+};
+
+struct transport_data {
+ struct rfcomm *rfcomm;
+ struct spa_source sco;
+};
+
+enum hfp_hf_state {
+ hfp_hf_brsf,
+ hfp_hf_bac,
+ hfp_hf_cind1,
+ hfp_hf_cind2,
+ hfp_hf_cmer,
+ hfp_hf_slc1,
+ hfp_hf_slc2,
+ hfp_hf_vgs,
+ hfp_hf_vgm,
+ hfp_hf_bcs
+};
+
+enum hsp_hs_state {
+ hsp_hs_init1,
+ hsp_hs_init2,
+ hsp_hs_vgs,
+ hsp_hs_vgm,
+};
+
+struct rfcomm_volume {
+ bool active;
+ int hw_volume;
+};
+
+struct rfcomm {
+ struct spa_list link;
+ struct spa_source source;
+ struct impl *backend;
+ struct spa_bt_device *device;
+ struct spa_hook device_listener;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ enum spa_bt_profile profile;
+ struct spa_source timer;
+ struct spa_source *volume_sync_timer;
+ char* path;
+ bool has_volume;
+ struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM];
+ unsigned int broken_mic_hw_volume:1;
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ unsigned int slc_configured:1;
+ unsigned int codec_negotiation_supported:1;
+ unsigned int msbc_supported_by_hfp:1;
+ unsigned int hfp_ag_switching_codec:1;
+ unsigned int hfp_ag_initial_codec_setup:2;
+ unsigned int cind_call_active:1;
+ unsigned int cind_call_notify:1;
+ unsigned int extended_error_reporting:1;
+ unsigned int clip_notify:1;
+ enum hfp_hf_state hf_state;
+ enum hsp_hs_state hs_state;
+ unsigned int codec;
+ uint32_t cind_enabled_indicators;
+ char *hf_indicators[MAX_HF_INDICATORS];
+#endif
+};
+
+static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ DBusMessage *r;
+
+ r = dbus_message_new_error(m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void transport_destroy(void *data)
+{
+ struct rfcomm *rfcomm = data;
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "transport %p destroy", rfcomm->transport);
+ rfcomm->transport = NULL;
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static const struct spa_bt_transport_implementation sco_transport_impl;
+
+static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm)
+{
+ struct impl *backend = rfcomm->backend;
+ struct spa_bt_transport *t = NULL;
+ struct transport_data *td;
+ char* pathfd;
+
+ if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL)
+ return NULL;
+
+ t = spa_bt_transport_create(backend->monitor, pathfd, sizeof(struct transport_data));
+ if (t == NULL)
+ goto finish;
+ spa_bt_transport_set_implementation(t, &sco_transport_impl, t);
+
+ t->device = rfcomm->device;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->profile = rfcomm->profile;
+ t->backend = &backend->this;
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ td = t->user_data;
+ td->rfcomm = rfcomm;
+
+ if (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME;
+ t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME;
+ } else {
+ t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME;
+ t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME;
+ }
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) {
+ t->volumes[i].active = rfcomm->volumes[i].active;
+ t->volumes[i].hw_volume_max = SPA_BT_VOLUME_HS_MAX;
+ if (rfcomm->volumes[i].active && rfcomm->volumes[i].hw_volume != SPA_BT_VOLUME_INVALID)
+ t->volumes[i].volume =
+ spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t->volumes[i].hw_volume_max);
+ }
+
+ spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);
+
+finish:
+ return t;
+}
+
+static int codec_switch_stop_timer(struct rfcomm *rfcomm);
+
+static void volume_sync_stop_timer(struct rfcomm *rfcomm);
+
+static void rfcomm_free(struct rfcomm *rfcomm)
+{
+ codec_switch_stop_timer(rfcomm);
+ for (int i = 0; i < MAX_HF_INDICATORS; i++) {
+ if (rfcomm->hf_indicators[i]) {
+ free(rfcomm->hf_indicators[i]);
+ }
+ }
+ spa_list_remove(&rfcomm->link);
+ if (rfcomm->path)
+ free(rfcomm->path);
+ if (rfcomm->transport) {
+ spa_hook_remove(&rfcomm->transport_listener);
+ spa_bt_transport_free(rfcomm->transport);
+ }
+ if (rfcomm->device) {
+ spa_bt_device_report_battery_level(rfcomm->device, SPA_BT_NO_BATTERY);
+ spa_hook_remove(&rfcomm->device_listener);
+ rfcomm->device = NULL;
+ }
+ if (rfcomm->source.fd >= 0) {
+ if (rfcomm->source.loop)
+ spa_loop_remove_source(rfcomm->source.loop, &rfcomm->source);
+ shutdown(rfcomm->source.fd, SHUT_RDWR);
+ close (rfcomm->source.fd);
+ rfcomm->source.fd = -1;
+ }
+ if (rfcomm->volume_sync_timer)
+ spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer);
+ free(rfcomm);
+}
+
+#define RFCOMM_MESSAGE_MAX_LENGTH 256
+
+/* from HF/HS to AG */
+SPA_PRINTF_FUNC(2, 3)
+static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...)
+{
+ struct impl *backend = rfcomm->backend;
+ char message[RFCOMM_MESSAGE_MAX_LENGTH + 1];
+ ssize_t len;
+ va_list args;
+
+ va_start(args, format);
+ len = vsnprintf(message, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args);
+ va_end(args);
+
+ if (len < 0)
+ return -EINVAL;
+
+ if (len > RFCOMM_MESSAGE_MAX_LENGTH)
+ return -E2BIG;
+
+ spa_log_debug(backend->log, "RFCOMM >> %s", message);
+
+ /*
+ * The format of an AT command from the HF to the AG shall be: <AT command><cr>
+ * - HFP 1.8, 4.34.1
+ *
+ * The format for a command from the HS to the AG is thus: AT<cmd>=<value><cr>
+ * - HSP 1.2, 4.8.1
+ */
+ message[len] = '\r';
+ /* `message` is no longer null-terminated */
+
+ len = write(rfcomm->source.fd, message, len + 1);
+ /* we ignore any errors, it's not critical and real errors should
+ * be caught with the HANGUP and ERROR events handled above */
+ if (len < 0) {
+ len = -errno;
+ spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno));
+ }
+
+ return len;
+}
+
+/* from AG to HF/HS */
+SPA_PRINTF_FUNC(2, 3)
+static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format, ...)
+{
+ struct impl *backend = rfcomm->backend;
+ char message[RFCOMM_MESSAGE_MAX_LENGTH + 4];
+ ssize_t len;
+ va_list args;
+
+ va_start(args, format);
+ len = vsnprintf(&message[2], RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args);
+ va_end(args);
+
+ if (len < 0)
+ return -EINVAL;
+
+ if (len > RFCOMM_MESSAGE_MAX_LENGTH)
+ return -E2BIG;
+
+ spa_log_debug(backend->log, "RFCOMM >> %s", &message[2]);
+
+ /*
+ * The format of the OK code from the AG to the HF shall be: <cr><lf>OK<cr><lf>
+ * The format of the generic ERROR code from the AG to the HF shall be: <cr><lf>ERROR<cr><lf>
+ * The format of an unsolicited result code from the AG to the HF shall be: <cr><lf><result code><cr><lf>
+ * - HFP 1.8, 4.34.1
+ *
+ * If the command is processed successfully, the resulting response from the AG to the HS is: <cr><lf>OK<cr><lf>
+ * If the command is not processed successfully, or is not recognized,
+ * the resulting response from the AG to the HS is: <cr><lf>ERROR<cr><lf>
+ * The format for an unsolicited result code (such as RING) from the AG to the HS is: <cr><lf><result code><cr><lf>
+ * - HSP 1.2, 4.8.1
+ */
+ message[0] = '\r';
+ message[1] = '\n';
+ message[len + 2] = '\r';
+ message[len + 3] = '\n';
+ /* `message` is no longer null-terminated */
+
+ len = write(rfcomm->source.fd, message, len + 4);
+ /* we ignore any errors, it's not critical and real errors should
+ * be caught with the HANGUP and ERROR events handled above */
+ if (len < 0) {
+ len = -errno;
+ spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno));
+ }
+
+ return len;
+}
+
+static void rfcomm_send_error(const struct rfcomm *rfcomm, enum cmee_error error)
+{
+ if (rfcomm->extended_error_reporting)
+ rfcomm_send_reply(rfcomm, "+CME ERROR: %d", error);
+ else
+ rfcomm_send_reply(rfcomm, "ERROR");
+}
+
+static bool rfcomm_volume_enabled(struct rfcomm *rfcomm)
+{
+ return rfcomm->device != NULL
+ && (rfcomm->device->hw_volume_profiles & rfcomm->profile);
+}
+
+static void rfcomm_emit_volume_changed(struct rfcomm *rfcomm, int id, int hw_volume)
+{
+ struct spa_bt_transport_volume *t_volume;
+
+ if (!rfcomm_volume_enabled(rfcomm))
+ return;
+
+ if ((id == SPA_BT_VOLUME_ID_RX || id == SPA_BT_VOLUME_ID_TX) && hw_volume >= 0) {
+ rfcomm->volumes[id].active = true;
+ rfcomm->volumes[id].hw_volume = hw_volume;
+ }
+
+ spa_log_debug(rfcomm->backend->log, "volume changed %d", hw_volume);
+
+ if (rfcomm->transport == NULL || !rfcomm->has_volume)
+ return;
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) {
+ t_volume = &rfcomm->transport->volumes[i];
+ t_volume->active = rfcomm->volumes[i].active;
+ t_volume->volume =
+ spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max);
+ }
+
+ spa_bt_transport_emit_volume_changed(rfcomm->transport);
+}
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+static bool rfcomm_hsp_ag(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int gain, dummy;
+
+ /* There are only three HSP AT commands:
+ * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain.
+ * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain.
+ * AT+CKPD=200: Sent by HS when headset button is pressed. */
+ if (sscanf(buf, "AT+VGS=%d", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ rfcomm_send_reply(rfcomm, "ERROR");
+ }
+ } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ if (!rfcomm->broken_mic_hw_volume)
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ rfcomm_send_reply(rfcomm, "ERROR");
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ }
+ } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) {
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id)
+{
+ struct spa_bt_transport_volume *t_volume;
+ const char *format;
+ int hw_volume;
+
+ if (!rfcomm_volume_enabled(rfcomm))
+ return false;
+
+ t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL;
+
+ if (!(t_volume && t_volume->active))
+ return false;
+
+ hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max);
+ rfcomm->volumes[id].hw_volume = hw_volume;
+
+ if (id == SPA_BT_VOLUME_ID_TX)
+ format = "AT+VGM";
+ else if (id == SPA_BT_VOLUME_ID_RX)
+ format = "AT+VGS";
+ else
+ spa_assert_not_reached();
+
+ rfcomm_send_cmd(rfcomm, "%s=%d", format, hw_volume);
+
+ return true;
+}
+
+static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int gain;
+
+ /* There are only three HSP AT result codes:
+ * +VGS=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGS command
+ * or when the gain is changed on the AG side.
+ * +VGM=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGM command
+ * or when the gain is changed on the AG side.
+ * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because
+ * it does not expect a reply. */
+ if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ }
+ } else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ }
+ } else if (spa_strstartswith(buf, "\r\nOK\r\n")) {
+ if (rfcomm->hs_state == hsp_hs_init2) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hs_state = hsp_hs_vgs;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ } else if (rfcomm->hs_state == hsp_hs_vgs) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX))
+ rfcomm->hs_state = hsp_hs_vgm;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ }
+ }
+
+ return true;
+}
+#endif
+
+#ifdef HAVE_LIBUSB
+static bool check_usb_altsetting_6(struct impl *backend, uint16_t vendor_id, uint16_t product_id)
+{
+ libusb_context *ctx = NULL;
+ struct libusb_config_descriptor *cfg = NULL;
+ libusb_device **devices = NULL;
+
+ ssize_t ndev, idev;
+ int res;
+ bool ok = false;
+
+ if ((res = libusb_init(&ctx)) < 0) {
+ ctx = NULL;
+ goto fail;
+ }
+
+ if ((ndev = libusb_get_device_list(ctx, &devices)) < 0) {
+ res = ndev;
+ devices = NULL;
+ goto fail;
+ }
+
+ for (idev = 0; idev < ndev; ++idev) {
+ libusb_device *dev = devices[idev];
+ struct libusb_device_descriptor desc;
+ int icfg;
+
+ libusb_get_device_descriptor(dev, &desc);
+ if (vendor_id != desc.idVendor || product_id != desc.idProduct)
+ continue;
+
+ /* Check the device has Bluetooth isoch. altsetting 6 interface */
+
+ for (icfg = 0; icfg < desc.bNumConfigurations; ++icfg) {
+ int iiface;
+
+ if ((res = libusb_get_config_descriptor(dev, icfg, &cfg)) != 0) {
+ cfg = NULL;
+ goto fail;
+ }
+
+ for (iiface = 0; iiface < cfg->bNumInterfaces; ++iiface) {
+ const struct libusb_interface *iface = &cfg->interface[iiface];
+ int ialt;
+
+ for (ialt = 0; ialt < iface->num_altsetting; ++ialt) {
+ const struct libusb_interface_descriptor *idesc = &iface->altsetting[ialt];
+ int iep;
+
+ if (idesc->bInterfaceClass != LIBUSB_CLASS_WIRELESS ||
+ idesc->bInterfaceSubClass != 1 /* RF */ ||
+ idesc->bInterfaceProtocol != 1 /* Bluetooth */ ||
+ idesc->bAlternateSetting != 6)
+ continue;
+
+ for (iep = 0; iep < idesc->bNumEndpoints; ++iep) {
+ const struct libusb_endpoint_descriptor *ep = &idesc->endpoint[iep];
+ if ((ep->bmAttributes & 0x3) == 0x1 /* isochronous */) {
+ ok = true;
+ goto done;
+ }
+ }
+ }
+ }
+
+ libusb_free_config_descriptor(cfg);
+ cfg = NULL;
+ }
+ }
+
+done:
+ if (cfg)
+ libusb_free_config_descriptor(cfg);
+ if (devices)
+ libusb_free_device_list(devices, 0);
+ if (ctx)
+ libusb_exit(ctx);
+ return ok;
+
+fail:
+ spa_log_info(backend->log, "failed to acquire USB device info: %d (%s)",
+ res, libusb_strerror(res));
+ ok = false;
+ goto done;
+}
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+
+static bool device_supports_required_mSBC_transport_modes(
+ struct impl *backend, struct spa_bt_device *device)
+{
+ int res;
+ bool msbc_ok, msbc_alt1_ok;
+ uint32_t bt_features;
+
+ if (device->adapter == NULL)
+ return false;
+
+ if (backend->quirks && spa_bt_quirks_get_features(backend->quirks, device->adapter, device, &bt_features) == 0) {
+ msbc_ok = bt_features & SPA_BT_FEATURE_MSBC;
+ msbc_alt1_ok = bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL);
+ } else {
+ msbc_ok = true;
+ msbc_alt1_ok = true;
+ }
+
+ spa_log_info(backend->log,
+ "bluez-monitor/hardware.conf: msbc:%d msbc-alt1:%d", (int)msbc_ok, (int)msbc_alt1_ok);
+
+ if (!msbc_ok && !msbc_alt1_ok)
+ return false;
+
+ res = spa_bt_adapter_has_msbc(device->adapter);
+ if (res < 0) {
+ spa_log_warn(backend->log,
+ "adapter %s: failed to determine msbc/esco capability (%d)",
+ device->adapter->path, res);
+ } else if (res == 0) {
+ spa_log_info(backend->log,
+ "adapter %s: no msbc/esco transport",
+ device->adapter->path);
+ return false;
+ } else {
+ spa_log_debug(backend->log,
+ "adapter %s: has msbc/esco transport",
+ device->adapter->path);
+ }
+
+ /* Check if USB ALT6 is really available on the device */
+ if (device->adapter->bus_type == BUS_TYPE_USB && !msbc_alt1_ok && msbc_ok) {
+#ifdef HAVE_LIBUSB
+ if (device->adapter->source_id == SOURCE_ID_USB) {
+ msbc_ok = check_usb_altsetting_6(backend, device->adapter->vendor_id,
+ device->adapter->product_id);
+ } else {
+ msbc_ok = false;
+ }
+ if (!msbc_ok)
+ spa_log_info(backend->log, "bluetooth host adapter does not support USB ALT6");
+#else
+ spa_log_info(backend->log,
+ "compiled without libusb; can't check if bluetooth adapter has USB ALT6");
+ msbc_ok = false;
+#endif
+ }
+ if (device->adapter->bus_type != BUS_TYPE_USB)
+ msbc_alt1_ok = false;
+
+ return msbc_ok || msbc_alt1_ok;
+}
+
+static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec);
+
+static void process_iphoneaccev_indicator(struct rfcomm *rfcomm, unsigned int key, unsigned int value)
+{
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "key:%u value:%u", key, value);
+
+ switch (key) {
+ case SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL: {
+ // Battery level is reported in range of 0-9, convert to 10-100%
+ uint8_t level = (SPA_CLAMP(value, 0u, 9u) + 1) * 10;
+ spa_log_debug(backend->log, "battery level: %d%%", (int) level);
+
+ // TODO: report without Battery Provider (using props)
+ spa_bt_device_report_battery_level(rfcomm->device, level);
+ break;
+ }
+ case SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE:
+ break;
+ default:
+ spa_log_warn(backend->log, "unknown AT+IPHONEACCEV key:%u value:%u", key, value);
+ break;
+ }
+}
+
+static void process_hfp_hf_indicator(struct rfcomm *rfcomm, unsigned int indicator, unsigned int value)
+{
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "indicator:%u value:%u", indicator, value);
+
+ switch (indicator) {
+ case SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY:
+ break;
+ case SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL:
+ // Battery level is reported in range 0-100
+ spa_log_debug(backend->log, "battery level: %u%%", value);
+
+ if (value <= 100) {
+ // TODO: report without Battery Provider (using props)
+ spa_bt_device_report_battery_level(rfcomm->device, value);
+ } else {
+ spa_log_warn(backend->log, "battery HF indicator %u outside of range [0, 100]: %u", indicator, value);
+ }
+ break;
+ default:
+ spa_log_warn(backend->log, "unknown HF indicator:%u value:%u", indicator, value);
+ break;
+ }
+}
+
+static void rfcomm_hfp_ag_set_cind(struct rfcomm *rfcomm, bool call_active)
+{
+ if (rfcomm->profile != SPA_BT_PROFILE_HFP_HF)
+ return;
+
+ if (call_active == rfcomm->cind_call_active)
+ return;
+
+ rfcomm->cind_call_active = call_active;
+
+ if (!rfcomm->cind_call_notify)
+ return;
+
+ rfcomm_send_reply(rfcomm, "+CIEV: 2,%d", rfcomm->cind_call_active);
+}
+
+static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int features;
+ unsigned int gain;
+ unsigned int count, r;
+ unsigned int selected_codec;
+ unsigned int indicator;
+ unsigned int indicator_value;
+ unsigned int value;
+ int xapl_vendor;
+ int xapl_product;
+ int xapl_features;
+
+ spa_debug_log_mem(backend->log, SPA_LOG_LEVEL_DEBUG, 2, buf, strlen(buf));
+
+ /* Some devices send initial \n: be permissive */
+ while (*buf == '\n')
+ ++buf;
+
+ if (sscanf(buf, "AT+BRSF=%u", &features) == 1) {
+ unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE;
+
+ /*
+ * Determine device volume control. Some headsets only support control of
+ * TX volume, but not RX, even if they have a microphone. Determine this
+ * separately based on whether we also get AT+VGS/AT+VGM, and quirks.
+ */
+ rfcomm->has_volume = (features & SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL);
+
+ /* Decide if we want to signal that the computer supports mSBC negotiation
+ This should be done when the computers bluetooth adapter supports the necessary transport mode */
+ if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) {
+
+ /* set the feature bit that indicates AG (=computer) supports codec negotiation */
+ ag_features |= SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION;
+
+ /* let's see if the headset supports codec negotiation */
+ if ((features & (SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION)) != 0) {
+ spa_log_debug(backend->log,
+ "RFCOMM features = %i, codec negotiation supported by headset",
+ features);
+ /* Prepare reply: Audio Gateway (=computer) supports codec negotiation */
+ rfcomm->codec_negotiation_supported = true;
+ rfcomm->msbc_supported_by_hfp = false;
+ } else {
+ /* Codec negotiation not supported */
+ spa_log_debug(backend->log,
+ "RFCOMM features = %i, codec negotiation NOT supported by headset",
+ features);
+
+ rfcomm->codec_negotiation_supported = false;
+ rfcomm->msbc_supported_by_hfp = false;
+ }
+ }
+
+ /* send reply to HF with the features supported by Audio Gateway (=computer) */
+ ag_features |= mm_supported_features();
+ ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS;
+ rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BAC=")) {
+ /* retrieve supported codecs */
+ /* response has the form AT+BAC=<codecID1>,<codecID2>,<codecIDx>
+ strategy: split the string into tokens */
+
+ char* token;
+ int cntr = 0;
+
+ while ((token = strsep(&buf, "=,"))) {
+ unsigned int codec_id;
+
+ /* skip token 0 i.e. the "AT+BAC=" part */
+ if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) {
+ spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id);
+ if (codec_id == HFP_AUDIO_CODEC_MSBC) {
+ rfcomm->msbc_supported_by_hfp = true;
+ spa_log_debug(backend->log, "RFCOMM headset supports mSBC codec");
+ }
+ }
+ cntr++;
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CIND=?")) {
+ rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CIND?")) {
+ rfcomm_send_reply(rfcomm, "+CIND: %d,%d,%d,0,%d,%d,%d", backend->modem.network_has_service,
+ backend->modem.active_call, backend->modem.call_setup, backend->modem.signal_strength,
+ backend->modem.network_is_roaming, backend->battery_level);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CMER")) {
+ int mode, keyp, disp, ind;
+
+ rfcomm->slc_configured = true;
+ rfcomm_send_reply(rfcomm, "OK");
+
+ rfcomm->cind_call_active = false;
+ if (sscanf(buf, "AT+CMER= %d , %d , %d , %d", &mode, &keyp, &disp, &ind) == 4)
+ rfcomm->cind_call_notify = ind ? true : false;
+ else
+ rfcomm->cind_call_notify = false;
+
+ /* switch codec to mSBC by sending unsolicited +BCS message */
+ if (rfcomm->codec_negotiation_supported && rfcomm->msbc_supported_by_hfp) {
+ spa_log_debug(backend->log, "RFCOMM initial codec setup");
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND;
+ rfcomm_send_reply(rfcomm, "+BCS: 2");
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC);
+ } else {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ } else {
+ rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID);
+ }
+ }
+ } else if (spa_streq(buf, "\r")) {
+ /* No commands, reply OK (ITU-T Rec. V.250 Sec. 5.2.1 & 5.6) */
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (!rfcomm->slc_configured) {
+ spa_log_warn(backend->log, "RFCOMM receive command before SLC completed: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection
+ * ***** */
+
+ } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) {
+ /* parse BCS(=Bluetooth Codec Selection) reply */
+ bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL);
+ rfcomm->hfp_ag_switching_codec = false;
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
+ codec_switch_stop_timer(rfcomm);
+ volume_sync_stop_timer(rfcomm);
+
+ if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
+ spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ if (was_switching_codec)
+ spa_bt_device_emit_codec_switched(rfcomm->device, -EIO);
+ return true;
+ }
+
+ rfcomm->codec = selected_codec;
+
+ spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec);
+
+ /* Recreate transport, since previous connection may now be invalid */
+ if (rfcomm->transport)
+ spa_bt_transport_free(rfcomm->transport);
+
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ if (was_switching_codec)
+ spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM);
+ return true;
+ }
+ rfcomm->transport->codec = selected_codec;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID);
+
+ rfcomm_send_reply(rfcomm, "OK");
+ if (was_switching_codec)
+ spa_bt_device_emit_codec_switched(rfcomm->device, 0);
+ } else if (spa_strstartswith(buf, "AT+BIA=")) {
+ /* retrieve indicators activation
+ * form: AT+BIA=[indrep1],[indrep2],[indrepx] */
+ char *str = buf + 7;
+ unsigned int ind = 1;
+
+ while (*str && ind < CIND_MAX && *str != '\r' && *str != '\n') {
+ if (*str == ',') {
+ ind++;
+ goto next_indicator;
+ }
+
+ /* Ignore updates to mandantory indicators which are always ON */
+ if (ind == CIND_CALL || ind == CIND_CALLSETUP || ind == CIND_CALLHELD)
+ goto next_indicator;
+
+ switch (*str) {
+ case '0':
+ rfcomm->cind_enabled_indicators &= ~(1 << ind);
+ break;
+ case '1':
+ rfcomm->cind_enabled_indicators |= (1 << ind);
+ break;
+ default:
+ spa_log_warn(backend->log, "Unsupported entry in %s: %c", buf, *str);
+ }
+next_indicator:
+ str++;
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CLCC")) {
+ struct spa_list *calls;
+ struct call *call;
+ unsigned int type;
+
+ if (backend->modemmanager) {
+ calls = mm_get_calls(backend->modemmanager);
+ spa_list_for_each(call, calls, link) {
+ if (!call->number) {
+ rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u", call->index, call->direction, call->state, call->multiparty);
+ } else {
+ if (spa_strstartswith(call->number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u,\"%s\",%d", call->index, call->direction, call->state,
+ call->multiparty, call->number, type);
+ }
+ }
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+CLIP=%u", &value) == 1) {
+ if (value > 1) {
+ spa_log_debug(backend->log, "Unsupported AT+CLIP value: %u", value);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ rfcomm->clip_notify = value;
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+CMEE=%u", &value) == 1) {
+ if (value > 1) {
+ spa_log_debug(backend->log, "Unsupported AT+CMEE value: %u", value);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ rfcomm->extended_error_reporting = value;
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CNUM")) {
+ if (backend->modem.own_number) {
+ unsigned int type;
+ if (spa_strstartswith(backend->modem.own_number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ rfcomm_send_reply(rfcomm, "+CNUM: ,\"%s\",%u", backend->modem.own_number, type);
+ }
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+COPS=")) {
+ unsigned int mode, val;
+
+ if (sscanf(buf, "AT+COPS=%u,%u", &mode, &val) != 2 ||
+ mode != 3 || val != 0) {
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ } else {
+ rfcomm_send_reply(rfcomm, "OK");
+ }
+ } else if (spa_strstartswith(buf, "AT+COPS?")) {
+ if (!backend->modem.network_has_service) {
+ rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+ } else {
+ if (backend->modem.operator_name)
+ rfcomm_send_reply(rfcomm, "+COPS: 0,0,\"%s\"", backend->modem.operator_name);
+ else
+ rfcomm_send_reply(rfcomm, "+COPS: 0,,");
+ rfcomm_send_reply(rfcomm, "OK");
+ }
+ } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ if (!rfcomm->broken_mic_hw_volume)
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
+ }
+ } else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
+ }
+ } else if (spa_strstartswith(buf, "AT+BIND=?")) {
+ rfcomm_send_reply(rfcomm, "+BIND: (2)");
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BIND?")) {
+ rfcomm_send_reply(rfcomm, "+BIND: 2,1");
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BIND=")) {
+ // BIND=... should return a comma separated list of indicators and
+ // 2 should be among the other numbers telling that battery charge
+ // is supported
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+BIEV=%u,%u", &indicator, &indicator_value) == 2) {
+ process_hfp_hf_indicator(rfcomm, indicator, indicator_value);
+ } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%*[^,],%u", &xapl_vendor, &xapl_product, &xapl_features) == 3) {
+ if (xapl_features & SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING) {
+ /* claim, that we support battery status reports */
+ rfcomm_send_reply(rfcomm, "+XAPL=iPhone,%u", SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING);
+ }
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+IPHONEACCEV=%u%n", &count, &r) == 1) {
+ if (count < 1 || count > 100)
+ return false;
+
+ buf += r;
+
+ for (unsigned int i = 0; i < count; i++) {
+ unsigned int key, value;
+
+ if (sscanf(buf, " , %u , %u%n", &key, &value, &r) != 2)
+ return false;
+
+ process_iphoneaccev_indicator(rfcomm, key, value);
+ buf += r;
+ }
+ } else if (spa_strstartswith(buf, "AT+APLSIRI?")) {
+ // This command is sent when we activate Apple extensions
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (!mm_is_available(backend->modemmanager)) {
+ spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection
+ * and acces to a modem
+ * ***** */
+
+ } else if (!backend->modem.network_has_service) {
+ spa_log_warn(backend->log, "RFCOMM receive command but network not available: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection,
+ * acces to a modem and to the network
+ * ***** */
+
+ } else if (spa_strstartswith(buf, "ATA")) {
+ enum cmee_error error;
+
+ if (!mm_answer_call(backend->modemmanager, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "ATD")) {
+ char number[31], sep;
+ enum cmee_error error;
+
+ if (sscanf(buf, "ATD%30[^;]%c", number, &sep) != 2 || sep != ';') {
+ spa_log_debug(backend->log, "Failed to parse ATD: \"%s\"", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ if (!mm_do_call(backend->modemmanager, number, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "AT+CHUP")) {
+ enum cmee_error error;
+
+ if (!mm_hangup_call(backend->modemmanager, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "AT+VTS=")) {
+ char *dtmf;
+ enum cmee_error error;
+
+ dtmf = calloc(1, 2);
+ if (sscanf(buf, "AT+VTS=%1s", dtmf) != 1) {
+ spa_log_debug(backend->log, "Failed to parse AT+VTS: \"%s\"", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ if (!mm_send_dtmf(backend->modemmanager, dtmf, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int features, gain, selected_codec, indicator, value;
+ char* token;
+
+ while ((token = strsep(&buf, "\r\n"))) {
+ if (sscanf(token, "+BRSF:%u", &features) == 1) {
+ if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) &&
+ rfcomm->msbc_supported_by_hfp)
+ rfcomm->codec_negotiation_supported = true;
+ } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) {
+ if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
+ spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec);
+
+ /* send codec selection to AG */
+ rfcomm_send_cmd(rfcomm, "AT+BCS=%u", selected_codec);
+
+ rfcomm->hf_state = hfp_hf_bcs;
+
+ if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) {
+ if (rfcomm->transport)
+ spa_bt_transport_free(rfcomm->transport);
+
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ } else {
+ rfcomm->transport->codec = selected_codec;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ }
+ } else if (sscanf(token, "+VGM%*1[:=]%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", token);
+ }
+ } else if (sscanf(token, "+VGS%*1[:=]%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", token);
+ }
+ } else if (spa_strstartswith(token, "+CIND: (")) {
+ uint8_t i = 1;
+ while (strstr(token, "\"")) {
+ token += strcspn(token, "\"") + 1;
+ token[strcspn(token, "\"")] = 0;
+ rfcomm->hf_indicators[i] = strdup(token);
+ token += strcspn(token, "\"") + 1;
+ i++;
+ if (i == MAX_HF_INDICATORS) {
+ break;
+ }
+ }
+ } else if (spa_strstartswith(token, "+CIND: ")) {
+ token[strcspn(token, "\r")] = 0;
+ token[strcspn(token, "\n")] = 0;
+ token += strlen("+CIND: ");
+ uint8_t i = 1;
+ while (strlen(token)) {
+ if (i >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[i]) {
+ break;
+ }
+ token[strcspn(token, ",")] = 0;
+ spa_log_info(backend->log, "AG indicator state: %s = %i", rfcomm->hf_indicators[i], atoi(token));
+
+ if (spa_streq(rfcomm->hf_indicators[i], "battchg")) {
+ spa_bt_device_report_battery_level(rfcomm->device, atoi(token) * 100 / 5);
+ }
+
+ token += strcspn(token, "\0") + 1;
+ i++;
+ }
+ } else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) {
+ if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) {
+ spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator);
+ } else {
+ spa_log_info(backend->log, "AG indicator update: %s = %u", rfcomm->hf_indicators[indicator], value);
+
+ if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) {
+ spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5);
+ }
+ }
+ } else if (spa_strstartswith(token, "OK")) {
+ switch(rfcomm->hf_state) {
+ case hfp_hf_brsf:
+ if (rfcomm->codec_negotiation_supported) {
+ rfcomm_send_cmd(rfcomm, "AT+BAC=1,2");
+ rfcomm->hf_state = hfp_hf_bac;
+ } else {
+ rfcomm_send_cmd(rfcomm, "AT+CIND=?");
+ rfcomm->hf_state = hfp_hf_cind1;
+ }
+ break;
+ case hfp_hf_bac:
+ rfcomm_send_cmd(rfcomm, "AT+CIND=?");
+ rfcomm->hf_state = hfp_hf_cind1;
+ break;
+ case hfp_hf_cind1:
+ rfcomm_send_cmd(rfcomm, "AT+CIND?");
+ rfcomm->hf_state = hfp_hf_cind2;
+ break;
+ case hfp_hf_cind2:
+ rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1");
+ rfcomm->hf_state = hfp_hf_cmer;
+ break;
+ case hfp_hf_cmer:
+ rfcomm->hf_state = hfp_hf_slc1;
+ rfcomm->slc_configured = true;
+ if (!rfcomm->codec_negotiation_supported) {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ } else {
+ rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ /* Report volume on SLC establishment */
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hf_state = hfp_hf_vgs;
+ break;
+ case hfp_hf_slc2:
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hf_state = hfp_hf_vgs;
+ break;
+ case hfp_hf_vgs:
+ rfcomm->hf_state = hfp_hf_slc1;
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX))
+ rfcomm->hf_state = hfp_hf_vgm;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+#endif
+
+static void rfcomm_event(struct spa_source *source)
+{
+ struct rfcomm *rfcomm = source->data;
+ struct impl *backend = rfcomm->backend;
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_info(backend->log, "lost RFCOMM connection.");
+ rfcomm_free(rfcomm);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ char buf[512];
+ ssize_t len;
+ bool res = false;
+
+ len = read(source->fd, buf, 511);
+ if (len < 0) {
+ spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno));
+ return;
+ }
+ buf[len] = 0;
+ spa_log_debug(backend->log, "RFCOMM << %s", buf);
+
+ switch (rfcomm->profile) {
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ case SPA_BT_PROFILE_HSP_HS:
+ res = rfcomm_hsp_ag(rfcomm, buf);
+ break;
+ case SPA_BT_PROFILE_HSP_AG:
+ res = rfcomm_hsp_hs(rfcomm, buf);
+ break;
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ case SPA_BT_PROFILE_HFP_HF:
+ res = rfcomm_hfp_ag(rfcomm, buf);
+ break;
+ case SPA_BT_PROFILE_HFP_AG:
+ res = rfcomm_hfp_hf(rfcomm, buf);
+ break;
+#endif
+ default:
+ break;
+ }
+
+ if (!res) {
+ spa_log_debug(backend->log, "RFCOMM received unsupported command: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED);
+ }
+ }
+}
+
+static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapter, bool msbc)
+{
+ struct sockaddr_sco addr;
+ socklen_t len;
+ bdaddr_t src;
+ int sock = -1;
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
+ if (sock < 0) {
+ spa_log_error(backend->log, "socket(SEQPACKET, SCO) %s", strerror(errno));
+ goto fail;
+ }
+
+ str2ba(adapter->address, &src);
+
+ len = sizeof(addr);
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &src);
+
+ if (bind(sock, (struct sockaddr *) &addr, len) < 0) {
+ spa_log_error(backend->log, "bind(): %s", strerror(errno));
+ goto fail;
+ }
+
+ spa_log_debug(backend->log, "msbc=%d", (int)msbc);
+ if (msbc) {
+ /* set correct socket options for mSBC */
+ struct bt_voice voice_config;
+ memset(&voice_config, 0, sizeof(voice_config));
+ voice_config.setting = BT_VOICE_TRANSPARENT;
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) {
+ spa_log_error(backend->log, "setsockopt(): %s", strerror(errno));
+ goto fail;
+ }
+ }
+
+ return sock;
+
+fail:
+ if (sock >= 0)
+ close(sock);
+ return -1;
+}
+
+static int sco_do_connect(struct spa_bt_transport *t)
+{
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+ struct spa_bt_device *d = t->device;
+ struct transport_data *td = t->user_data;
+ struct sockaddr_sco addr;
+ socklen_t len;
+ int err;
+ int sock;
+ bdaddr_t dst;
+ int retry = 2;
+
+ spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u",
+ t, t->codec);
+
+ if (d->adapter == NULL)
+ return -EIO;
+
+ str2ba(d->address, &dst);
+
+again:
+ sock = sco_create_socket(backend, d->adapter, (t->codec == HFP_AUDIO_CODEC_MSBC));
+ if (sock < 0)
+ return -1;
+
+ len = sizeof(addr);
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &dst);
+
+ spa_log_debug(backend->log, "transport %p: doing connect", t);
+ err = connect(sock, (struct sockaddr *) &addr, len);
+ if (err < 0 && errno == ECONNABORTED && retry-- > 0) {
+ spa_log_warn(backend->log, "connect(): %s. Remaining retry:%d",
+ strerror(errno), retry);
+ close(sock);
+ goto again;
+ } else if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) {
+ spa_log_error(backend->log, "connect(): %s", strerror(errno));
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (errno == EOPNOTSUPP && t->codec == HFP_AUDIO_CODEC_MSBC &&
+ td->rfcomm->msbc_supported_by_hfp) {
+ /* Adapter doesn't support msbc. Renegotiate. */
+ d->adapter->msbc_probed = true;
+ d->adapter->has_msbc = false;
+ td->rfcomm->msbc_supported_by_hfp = false;
+ if (t->profile == SPA_BT_PROFILE_HFP_HF) {
+ td->rfcomm->hfp_ag_switching_codec = true;
+ rfcomm_send_reply(td->rfcomm, "+BCS: 1");
+ } else if (t->profile == SPA_BT_PROFILE_HFP_AG) {
+ rfcomm_send_cmd(td->rfcomm, "AT+BAC=1");
+ }
+ }
+#endif
+ goto fail_close;
+ }
+
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later);
+
+static void wait_for_socket(int fd)
+{
+ struct pollfd fds[1];
+ const int timeout_ms = 500;
+
+ fds[0].fd = fd;
+ fds[0].events = POLLIN | POLLERR | POLLHUP;
+ poll(fds, 1, timeout_ms);
+}
+
+static int sco_acquire_cb(void *data, bool optional)
+{
+ struct spa_bt_transport *t = data;
+ struct transport_data *td = t->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+ int sock;
+ socklen_t len;
+
+ spa_log_debug(backend->log, "transport %p: enter sco_acquire_cb", t);
+
+ if (optional || t->fd > 0)
+ sock = t->fd;
+ else
+ sock = sco_do_connect(t);
+
+ if (sock < 0)
+ goto fail;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ rfcomm_hfp_ag_set_cind(td->rfcomm, true);
+#endif
+
+ /*
+ * Send RFCOMM volume after connection is ready, and also after
+ * a timeout.
+ *
+ * Some headsets adjust their HFP volume when in A2DP mode
+ * without reporting via RFCOMM to us, so the volume level can
+ * be out of sync, and we can't know what it is. Moreover, they may
+ * take the first +VGS command after connection only partially
+ * into account, and need a long enough timeout.
+ *
+ * E.g. with Sennheiser HD-250BT, the first +VGS changes the
+ * actual volume, but does not update the level in the hardware
+ * volume buttons, which is updated by an +VGS event only after
+ * sufficient time is elapsed from the connection.
+ */
+ wait_for_socket(sock);
+ rfcomm_ag_sync_volume(td->rfcomm, false);
+ rfcomm_ag_sync_volume(td->rfcomm, true);
+
+ t->fd = sock;
+
+ /* Fallback value */
+ t->read_mtu = 48;
+ t->write_mtu = 48;
+
+ if (true) {
+ struct sco_options sco_opt;
+
+ len = sizeof(sco_opt);
+ memset(&sco_opt, 0, len);
+
+ if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
+ spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults");
+ else {
+ spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu);
+ t->read_mtu = sco_opt.mtu;
+ t->write_mtu = sco_opt.mtu;
+ }
+ }
+ spa_log_debug(backend->log, "transport %p: read_mtu=%u, write_mtu=%u", t, t->read_mtu, t->write_mtu);
+
+ return 0;
+
+fail:
+ return -1;
+}
+
+static int sco_release_cb(void *data)
+{
+ struct spa_bt_transport *t = data;
+ struct transport_data *td = t->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ spa_log_info(backend->log, "Transport %s released", t->path);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ rfcomm_hfp_ag_set_cind(td->rfcomm, false);
+#endif
+
+ if (t->sco_io) {
+ spa_bt_sco_io_destroy(t->sco_io);
+ t->sco_io = NULL;
+ }
+
+ if (t->fd > 0) {
+ /* Shutdown and close the socket */
+ shutdown(t->fd, SHUT_RDWR);
+ close(t->fd);
+ t->fd = -1;
+ }
+
+ return 0;
+}
+
+static void sco_event(struct spa_source *source)
+{
+ struct spa_bt_transport *t = source->data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno));
+ if (t->fd >= 0) {
+ if (source->loop)
+ spa_loop_remove_source(source->loop, source);
+ shutdown(t->fd, SHUT_RDWR);
+ close (t->fd);
+ t->fd = -1;
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE);
+ }
+ }
+}
+
+static void sco_listen_event(struct spa_source *source)
+{
+ struct impl *backend = source->data;
+ struct sockaddr_sco addr;
+ socklen_t addrlen;
+ int sock = -1;
+ char local_address[18], remote_address[18];
+ struct rfcomm *rfcomm;
+ struct spa_bt_transport *t = NULL;
+ struct transport_data *td;
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_error(backend->log, "error listening SCO connection: %s", strerror(errno));
+ goto fail;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr);
+
+ spa_log_debug(backend->log, "doing accept");
+ sock = accept(source->fd, (struct sockaddr *) &addr, &addrlen);
+ if (sock < 0) {
+ if (errno != EAGAIN)
+ spa_log_error(backend->log, "SCO accept(): %s", strerror(errno));
+ goto fail;
+ }
+
+ ba2str(&addr.sco_bdaddr, remote_address);
+
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr);
+
+ if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) {
+ spa_log_error(backend->log, "SCO getsockname(): %s", strerror(errno));
+ goto fail;
+ }
+
+ ba2str(&addr.sco_bdaddr, local_address);
+
+ /* Find transport for local and remote address */
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->transport && spa_streq(rfcomm->transport->device->address, remote_address) &&
+ spa_streq(rfcomm->transport->device->adapter->address, local_address)) {
+ t = rfcomm->transport;
+ break;
+ }
+ }
+ if (!t) {
+ spa_log_debug(backend->log, "No transport for adapter %s and remote %s",
+ local_address, remote_address);
+ goto fail;
+ }
+
+ /* The Synchronous Connection shall always be established by the AG, i.e. the remote profile
+ should be a HSP AG or HFP AG profile */
+ if ((t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) == 0) {
+ spa_log_debug(backend->log, "transport %p: Rejecting incoming audio connection to an AG profile", t);
+ goto fail;
+ }
+
+ if (t->fd >= 0) {
+ spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t);
+ goto fail;
+ }
+
+ spa_log_debug(backend->log, "transport %p: codec=%u", t, t->codec);
+ if (backend->defer_setup_enabled) {
+ /* In BT_DEFER_SETUP mode, when a connection is accepted, the listening socket is unblocked but
+ * the effective connection setup happens only on first receive, allowing to configure the
+ * accepted socket. */
+ char buff;
+
+ if (t->codec == HFP_AUDIO_CODEC_MSBC) {
+ /* set correct socket options for mSBC */
+ struct bt_voice voice_config;
+ memset(&voice_config, 0, sizeof(voice_config));
+ voice_config.setting = BT_VOICE_TRANSPARENT;
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) {
+ spa_log_error(backend->log, "transport %p: setsockopt(): %s", t, strerror(errno));
+ goto fail;
+ }
+ }
+
+ /* First read from the accepted socket is non-blocking and returns a zero length buffer. */
+ if (read(sock, &buff, 1) == -1) {
+ spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(errno));
+ goto fail;
+ }
+ }
+
+ t->fd = sock;
+
+ td = t->user_data;
+ td->sco.func = sco_event;
+ td->sco.data = t;
+ td->sco.fd = sock;
+ td->sco.mask = SPA_IO_HUP | SPA_IO_ERR;
+ td->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &td->sco);
+
+ spa_log_debug(backend->log, "transport %p: audio connected", t);
+
+ /* Report initial volume to remote */
+ if (t->profile == SPA_BT_PROFILE_HSP_AG) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hs_state = hsp_hs_vgs;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ } else if (t->profile == SPA_BT_PROFILE_HFP_AG) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hf_state = hfp_hf_vgs;
+ else
+ rfcomm->hf_state = hfp_hf_slc1;
+ }
+
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING);
+ return;
+
+fail:
+ if (sock >= 0)
+ close(sock);
+ return;
+}
+
+static int sco_listen(struct impl *backend)
+{
+ struct sockaddr_sco addr;
+ int sock;
+ uint32_t defer = 1;
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO);
+ if (sock < 0) {
+ spa_log_error(backend->log, "socket(SEQPACKET, SCO) %m");
+ return -errno;
+ }
+
+ /* Bind to local address */
+ memset(&addr, 0, sizeof(addr));
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, BDADDR_ANY);
+
+ if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ spa_log_error(backend->log, "bind(): %m");
+ goto fail_close;
+ }
+
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, &defer, sizeof(defer)) < 0) {
+ spa_log_warn(backend->log, "Can't enable deferred setup: %s", strerror(errno));
+ backend->defer_setup_enabled = 0;
+ } else {
+ backend->defer_setup_enabled = 1;
+ }
+
+ spa_log_debug(backend->log, "doing listen");
+ if (listen(sock, 1) < 0) {
+ spa_log_error(backend->log, "listen(): %m");
+ goto fail_close;
+ }
+
+ backend->sco.func = sco_listen_event;
+ backend->sco.data = backend;
+ backend->sco.fd = sock;
+ backend->sco.mask = SPA_IO_IN;
+ backend->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &backend->sco);
+
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id)
+{
+ struct transport_data *td = t->user_data;
+ struct rfcomm *rfcomm = td->rfcomm;
+ const char *format;
+ int value;
+
+ if (!rfcomm_volume_enabled(rfcomm)
+ || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ || !(rfcomm->has_volume && rfcomm->volumes[id].active))
+ return -ENOTSUP;
+
+ value = rfcomm->volumes[id].hw_volume;
+
+ if (id == SPA_BT_VOLUME_ID_RX)
+ if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF)
+ format = "+VGM: %d";
+ else
+ format = "+VGM=%d";
+ else if (id == SPA_BT_VOLUME_ID_TX)
+ if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF)
+ format = "+VGS: %d";
+ else
+ format = "+VGS=%d";
+ else
+ spa_assert_not_reached();
+
+ if (rfcomm->transport)
+ rfcomm_send_reply(rfcomm, format, value);
+
+ return 0;
+}
+
+static int sco_set_volume_cb(void *data, int id, float volume)
+{
+ struct spa_bt_transport *t = data;
+ struct spa_bt_transport_volume *t_volume = &t->volumes[id];
+ struct transport_data *td = t->user_data;
+ struct rfcomm *rfcomm = td->rfcomm;
+ int value;
+
+ if (!rfcomm_volume_enabled(rfcomm)
+ || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ || !(rfcomm->has_volume && rfcomm->volumes[id].active))
+ return -ENOTSUP;
+
+ value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max);
+ t_volume->volume = volume;
+
+ if (rfcomm->volumes[id].hw_volume == value)
+ return 0;
+ rfcomm->volumes[id].hw_volume = value;
+
+ return rfcomm_ag_set_volume(t, id);
+}
+
+static const struct spa_bt_transport_implementation sco_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = sco_acquire_cb,
+ .release = sco_release_cb,
+ .set_volume = sco_set_volume_cb,
+};
+
+static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device)
+{
+ struct rfcomm *rfcomm;
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->device == device)
+ return rfcomm;
+ }
+ return NULL;
+}
+
+static int backend_native_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ struct impl *backend = data;
+ struct rfcomm *rfcomm;
+
+ rfcomm = device_find_rfcomm(backend, device);
+ if (rfcomm == NULL || rfcomm->profile != SPA_BT_PROFILE_HFP_HF)
+ return -ENOTSUP;
+
+ if (codec == HFP_AUDIO_CODEC_CVSD)
+ return 1;
+
+ return (codec == HFP_AUDIO_CODEC_MSBC &&
+ (rfcomm->profile == SPA_BT_PROFILE_HFP_AG ||
+ rfcomm->profile == SPA_BT_PROFILE_HFP_HF) &&
+ rfcomm->msbc_supported_by_hfp &&
+ rfcomm->codec_negotiation_supported) ? 1 : 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int codec_switch_stop_timer(struct rfcomm *rfcomm)
+{
+ struct impl *backend = rfcomm->backend;
+ struct itimerspec ts;
+
+ if (rfcomm->timer.data == NULL)
+ return 0;
+
+ spa_loop_remove_source(backend->main_loop, &rfcomm->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL);
+ spa_system_close(backend->main_system, rfcomm->timer.fd);
+ rfcomm->timer.data = NULL;
+ return 0;
+}
+
+static void volume_sync_stop_timer(struct rfcomm *rfcomm)
+{
+ if (rfcomm->volume_sync_timer)
+ spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+ NULL, NULL, false);
+}
+
+static void volume_sync_timer_event(void *data, uint64_t expirations)
+{
+ struct rfcomm *rfcomm = data;
+
+ volume_sync_stop_timer(rfcomm);
+
+ if (rfcomm->transport) {
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+ }
+}
+
+static int volume_sync_start_timer(struct rfcomm *rfcomm)
+{
+ struct timespec ts;
+ const uint64_t timeout = 1500 * SPA_NSEC_PER_MSEC;
+
+ if (rfcomm->volume_sync_timer == NULL)
+ rfcomm->volume_sync_timer = spa_loop_utils_add_timer(rfcomm->backend->loop_utils,
+ volume_sync_timer_event, rfcomm);
+
+ if (rfcomm->volume_sync_timer == NULL)
+ return -EIO;
+
+ ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+ &ts, NULL, false);
+
+ return 0;
+}
+
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later)
+{
+ if (rfcomm->transport == NULL)
+ return -ENOENT;
+
+ if (!later) {
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+ } else {
+ volume_sync_start_timer(rfcomm);
+ }
+
+ return 0;
+}
+
+static void codec_switch_timer_event(struct spa_source *source)
+{
+ struct rfcomm *rfcomm = source->data;
+ struct impl *backend = rfcomm->backend;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0)
+ spa_log_warn(backend->log, "error reading timerfd: %s", strerror(errno));
+
+ codec_switch_stop_timer(rfcomm);
+
+ spa_log_debug(backend->log, "rfcomm %p: codec switch timeout", rfcomm);
+
+ switch (rfcomm->hfp_ag_initial_codec_setup) {
+ case HFP_AG_INITIAL_CODEC_SETUP_SEND:
+ /* Retry codec selection */
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT;
+ rfcomm_send_reply(rfcomm, "+BCS: 2");
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC);
+ return;
+ case HFP_AG_INITIAL_CODEC_SETUP_WAIT:
+ /* Failure, try falling back to CVSD. */
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
+ if (rfcomm->transport == NULL) {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ } else {
+ rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ rfcomm_send_reply(rfcomm, "+BCS: 1");
+ return;
+ default:
+ break;
+ }
+
+ if (rfcomm->hfp_ag_switching_codec) {
+ rfcomm->hfp_ag_switching_codec = false;
+ if (rfcomm->device)
+ spa_bt_device_emit_codec_switched(rfcomm->device, -EIO);
+ }
+}
+
+static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec)
+{
+ struct impl *backend = rfcomm->backend;
+ struct itimerspec ts;
+
+ spa_log_debug(backend->log, "rfcomm %p: start timer", rfcomm);
+ if (rfcomm->timer.data == NULL) {
+ rfcomm->timer.data = rfcomm;
+ rfcomm->timer.func = codec_switch_timer_event;
+ rfcomm->timer.fd = spa_system_timerfd_create(backend->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ rfcomm->timer.mask = SPA_IO_IN;
+ rfcomm->timer.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &rfcomm->timer);
+ }
+ ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC;
+ ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int backend_native_ensure_codec(void *data, struct spa_bt_device *device, unsigned int codec)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ struct impl *backend = data;
+ struct rfcomm *rfcomm;
+ int res;
+
+ res = backend_native_supports_codec(data, device, codec);
+ if (res <= 0)
+ return -EINVAL;
+
+ rfcomm = device_find_rfcomm(backend, device);
+ if (rfcomm == NULL)
+ return -ENOTSUP;
+
+ if (!rfcomm->codec_negotiation_supported)
+ return -ENOTSUP;
+
+ if (rfcomm->codec == codec) {
+ spa_bt_device_emit_codec_switched(device, 0);
+ return 0;
+ }
+
+ if ((res = rfcomm_send_reply(rfcomm, "+BCS: %u", codec)) < 0)
+ return res;
+
+ rfcomm->hfp_ag_switching_codec = true;
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC);
+
+ return 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static void device_destroy(void *data)
+{
+ struct rfcomm *rfcomm = data;
+ rfcomm_free(rfcomm);
+}
+
+static const struct spa_bt_device_events device_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .destroy = device_destroy,
+};
+
+static enum spa_bt_profile path_to_profile(const char *path)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (spa_streq(path, PROFILE_HSP_AG))
+ return SPA_BT_PROFILE_HSP_HS;
+
+ if (spa_streq(path, PROFILE_HSP_HS))
+ return SPA_BT_PROFILE_HSP_AG;
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (spa_streq(path, PROFILE_HFP_AG))
+ return SPA_BT_PROFILE_HFP_HF;
+
+ if (spa_streq(path, PROFILE_HFP_HF))
+ return SPA_BT_PROFILE_HFP_AG;
+#endif
+
+ return SPA_BT_PROFILE_NULL;
+}
+
+static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+ DBusMessageIter it[5];
+ const char *handler, *path;
+ enum spa_bt_profile profile;
+ struct rfcomm *rfcomm;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t = NULL;
+ int fd;
+
+ if (!dbus_message_has_signature(m, "oha{sv}")) {
+ spa_log_warn(backend->log, "invalid NewConnection() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ handler = dbus_message_get_path(m);
+ profile = path_to_profile(handler);
+ if (profile == SPA_BT_PROFILE_NULL) {
+ spa_log_warn(backend->log, "invalid handler %s", handler);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &path);
+
+ d = spa_bt_device_find(backend->monitor, path);
+ if (d == NULL || d->adapter == NULL) {
+ spa_log_warn(backend->log, "unknown device for path %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_bt_device_add_profile(d, profile);
+
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_get_basic(&it[0], &fd);
+
+ spa_log_debug(backend->log, "NewConnection path=%s, fd=%d, profile %s", path, fd, handler);
+
+ rfcomm = calloc(1, sizeof(struct rfcomm));
+ if (rfcomm == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ rfcomm->backend = backend;
+ rfcomm->profile = profile;
+ rfcomm->device = d;
+ rfcomm->path = strdup(path);
+ rfcomm->source.func = rfcomm_event;
+ rfcomm->source.data = rfcomm;
+ rfcomm->source.fd = fd;
+ rfcomm->source.mask = SPA_IO_IN;
+ rfcomm->source.rmask = 0;
+ /* By default all indicators are enabled */
+ rfcomm->cind_enabled_indicators = 0xFFFFFFFF;
+ memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators);
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) {
+ if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)
+ rfcomm->volumes[i].active = true;
+ rfcomm->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID;
+ }
+
+ spa_bt_device_add_listener(d, &rfcomm->device_listener, &device_events, rfcomm);
+ spa_loop_add_source(backend->main_loop, &rfcomm->source);
+ spa_list_append(&backend->rfcomm_list, &rfcomm->link);
+
+ if (profile == SPA_BT_PROFILE_HSP_HS || profile == SPA_BT_PROFILE_HSP_AG) {
+ t = _transport_create(rfcomm);
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ goto fail_need_memory;
+ }
+ rfcomm->transport = t;
+ rfcomm->has_volume = rfcomm_volume_enabled(rfcomm);
+
+ if (profile == SPA_BT_PROFILE_HSP_AG) {
+ rfcomm->hs_state = hsp_hs_init1;
+ }
+
+ spa_bt_device_connect_profile(t->device, profile);
+
+ spa_log_debug(backend->log, "Transport %s available for profile %s", t->path, handler);
+ } else if (profile == SPA_BT_PROFILE_HFP_AG) {
+ /* Start SLC connection */
+ unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_NONE;
+
+ /* Decide if we want to signal that the HF supports mSBC negotiation
+ This should be done when the bluetooth adapter supports the necessary transport mode */
+ if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) {
+ /* set the feature bit that indicates HF supports codec negotiation */
+ hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION;
+ rfcomm->msbc_supported_by_hfp = true;
+ rfcomm->codec_negotiation_supported = false;
+ } else {
+ rfcomm->msbc_supported_by_hfp = false;
+ rfcomm->codec_negotiation_supported = false;
+ }
+
+ if (rfcomm_volume_enabled(rfcomm)) {
+ rfcomm->has_volume = true;
+ hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL;
+ }
+
+ /* send command to AG with the features supported by Hands-Free */
+ rfcomm_send_cmd(rfcomm, "AT+BRSF=%u", hf_features);
+
+ rfcomm->hf_state = hfp_hf_brsf;
+ }
+
+ if (rfcomm_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) {
+ uint32_t device_features;
+ if (spa_bt_quirks_get_features(backend->quirks, d->adapter, d, &device_features) == 0) {
+ rfcomm->broken_mic_hw_volume = !(device_features & SPA_BT_FEATURE_HW_VOLUME_MIC);
+ if (rfcomm->broken_mic_hw_volume)
+ spa_log_debug(backend->log, "microphone HW volume disabled by quirk");
+ }
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ goto fail_need_memory;
+ if (!dbus_connection_send(conn, r, NULL))
+ goto fail_need_memory;
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+fail_need_memory:
+ if (rfcomm)
+ rfcomm_free(rfcomm);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static DBusHandlerResult profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+ const char *handler, *path;
+ struct spa_bt_device *d;
+ enum spa_bt_profile profile = SPA_BT_PROFILE_NULL;
+ DBusMessageIter it[5];
+ struct rfcomm *rfcomm, *rfcomm_tmp;
+
+ if (!dbus_message_has_signature(m, "o")) {
+ spa_log_warn(backend->log, "invalid RequestDisconnection() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ handler = dbus_message_get_path(m);
+ profile = path_to_profile(handler);
+ if (profile == SPA_BT_PROFILE_NULL) {
+ spa_log_warn(backend->log, "invalid handler %s", handler);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &path);
+
+ d = spa_bt_device_find(backend->monitor, path);
+ if (d == NULL || d->adapter == NULL) {
+ spa_log_warn(backend->log, "unknown device for path %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ spa_list_for_each_safe(rfcomm, rfcomm_tmp, &backend->rfcomm_list, link) {
+ if (rfcomm->device == d && rfcomm->profile == profile) {
+ rfcomm_free(rfcomm);
+ }
+ }
+ spa_bt_device_check_profiles(d, false);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = PROFILE_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release"))
+ res = profile_release(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection"))
+ res = profile_request_disconnection(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection"))
+ res = profile_new_connection(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void register_profile_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) {
+ spa_log_warn(backend->log, "Register profile not supported");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(backend->log, "Error registering profile");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterProfile() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int register_profile(struct impl *backend, const char *profile, const char *uuid)
+{
+ DBusMessage *m;
+ DBusMessageIter it[4];
+ dbus_bool_t autoconnect;
+ dbus_uint16_t version, chan, features;
+ char *str;
+ DBusPendingCall *call;
+
+ if (!(backend->enabled_profiles & spa_bt_profile_from_uuid(uuid)))
+ return -ECANCELED;
+
+ spa_log_debug(backend->log, "Registering Profile %s %s", profile, uuid);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez",
+ BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_iter_init_append(m, &it[0]);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_OBJECT_PATH, &profile);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &uuid);
+ dbus_message_iter_open_container(&it[0], DBUS_TYPE_ARRAY, "{sv}", &it[1]);
+
+ if (spa_streq(uuid, SPA_BT_UUID_HSP_HS) ||
+ spa_streq(uuid, SPA_BT_UUID_HSP_HS_ALT)) {
+
+ /* In the headset role, the connection will only be initiated from the remote side */
+ str = "AutoConnect";
+ autoconnect = 0;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "b", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_BOOLEAN, &autoconnect);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ str = "Channel";
+ chan = HSP_HS_DEFAULT_CHANNEL;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &chan);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HSP version 1.2 */
+ str = "Version";
+ version = 0x0102;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ } else if (spa_streq(uuid, SPA_BT_UUID_HFP_AG)) {
+ str = "Features";
+
+ /* We announce wideband speech support anyway */
+ features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HFP version 1.7 */
+ str = "Version";
+ version = 0x0107;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ } else if (spa_streq(uuid, SPA_BT_UUID_HFP_HF)) {
+ str = "Features";
+
+ /* We announce wideband speech support anyway */
+ features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HFP version 1.7 */
+ str = "Version";
+ version = 0x0107;
+ dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
+ dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ }
+ dbus_message_iter_close_container(&it[0], &it[1]);
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, register_profile_reply, backend, NULL);
+ dbus_message_unref(m);
+ return 0;
+}
+
+static void unregister_profile(struct impl *backend, const char *profile)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+
+ spa_log_debug(backend->log, "Unregistering Profile %s", profile);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez",
+ BLUEZ_PROFILE_MANAGER_INTERFACE, "UnregisterProfile");
+ if (m == NULL)
+ return;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &profile, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(backend->log, "Unregistering Profile %s failed", profile);
+ dbus_error_free(&err);
+ return;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "UnregisterProfile() returned error: %s", dbus_message_get_error_name(r));
+ return;
+ }
+
+ dbus_message_unref(r);
+}
+
+static int backend_native_register_profiles(void *data)
+{
+ struct impl *backend = data;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ register_profile(backend, PROFILE_HSP_AG, SPA_BT_UUID_HSP_AG);
+ register_profile(backend, PROFILE_HSP_HS, SPA_BT_UUID_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ register_profile(backend, PROFILE_HFP_AG, SPA_BT_UUID_HFP_AG);
+ register_profile(backend, PROFILE_HFP_HF, SPA_BT_UUID_HFP_HF);
+#endif
+
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ sco_listen(backend);
+
+ return 0;
+}
+
+static void sco_close(struct impl *backend)
+{
+ if (backend->sco.fd >= 0) {
+ if (backend->sco.loop)
+ spa_loop_remove_source(backend->sco.loop, &backend->sco);
+ shutdown(backend->sco.fd, SHUT_RDWR);
+ close (backend->sco.fd);
+ backend->sco.fd = -1;
+ }
+}
+
+static int backend_native_unregister_profiles(void *data)
+{
+ struct impl *backend = data;
+
+ sco_close(backend);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_AG)
+ unregister_profile(backend, PROFILE_HSP_AG);
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_HS)
+ unregister_profile(backend, PROFILE_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_AG)
+ unregister_profile(backend, PROFILE_HFP_AG);
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_HF)
+ unregister_profile(backend, PROFILE_HFP_HF);
+#endif
+
+ return 0;
+}
+
+static void send_ciev_for_each_rfcomm(struct impl *backend, int indicator, int value)
+{
+ struct rfcomm *rfcomm;
+
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->slc_configured &&
+ ((indicator == CIND_CALL || indicator == CIND_CALLSETUP || indicator == CIND_CALLHELD) ||
+ (rfcomm->cind_call_notify && (rfcomm->cind_enabled_indicators & (1 << indicator)))))
+ rfcomm_send_reply(rfcomm, "+CIEV: %d,%d", indicator, value);
+ }
+}
+
+static void ring_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *backend = data;
+ const char *number;
+ unsigned int type;
+ struct timespec ts;
+ const uint64_t timeout = 1 * SPA_NSEC_PER_SEC;
+ struct rfcomm *rfcomm;
+
+ number = mm_get_incoming_call_number(backend->modemmanager);
+ if (number) {
+ if (spa_strstartswith(number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ }
+
+ ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, &ts, NULL, false);
+
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->slc_configured) {
+ rfcomm_send_reply(rfcomm, "RING");
+ if (rfcomm->clip_notify && number)
+ rfcomm_send_reply(rfcomm, "+CLIP: \"%s\",%u", number, type);
+ }
+ }
+}
+
+static void set_call_active(bool active, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.active_call != active) {
+ backend->modem.active_call = active;
+ send_ciev_for_each_rfcomm(backend, CIND_CALL, active);
+ }
+}
+
+static void set_call_setup(enum call_setup value, void *user_data)
+{
+ struct impl *backend = user_data;
+ enum call_setup old = backend->modem.call_setup;
+
+ if (backend->modem.call_setup != value) {
+ backend->modem.call_setup = value;
+ send_ciev_for_each_rfcomm(backend, CIND_CALLSETUP, value);
+ }
+
+ if (value == CIND_CALLSETUP_INCOMING) {
+ if (backend->ring_timer == NULL)
+ backend->ring_timer = spa_loop_utils_add_timer(backend->loop_utils, ring_timer_event, backend);
+
+ if (backend->ring_timer == NULL) {
+ spa_log_warn(backend->log, "Failed to create ring timer");
+ return;
+ }
+
+ ring_timer_event(backend, 0);
+ } else if (old == CIND_CALLSETUP_INCOMING) {
+ spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, NULL, NULL, false);
+ }
+}
+
+void set_battery_level(unsigned int level, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->battery_level != level) {
+ backend->battery_level = level;
+ send_ciev_for_each_rfcomm(backend, CIND_BATTERY_LEVEL, level);
+ }
+}
+
+static void set_modem_operator_name(const char *name, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.operator_name) {
+ free(backend->modem.operator_name);
+ backend->modem.operator_name = NULL;
+ }
+
+ if (name)
+ backend->modem.operator_name = strdup(name);
+}
+
+static void set_modem_roaming(bool is_roaming, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.network_is_roaming != is_roaming) {
+ backend->modem.network_is_roaming = is_roaming;
+ send_ciev_for_each_rfcomm(backend, CIND_ROAM, is_roaming);
+ }
+}
+
+static void set_modem_own_number(const char *number, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.own_number) {
+ free(backend->modem.own_number);
+ backend->modem.own_number = NULL;
+ }
+
+ if (number)
+ backend->modem.own_number = strdup(number);
+}
+
+static void set_modem_service(bool available, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.network_has_service != available) {
+ backend->modem.network_has_service = available;
+ send_ciev_for_each_rfcomm(backend, CIND_SERVICE, available);
+ }
+}
+
+static void set_modem_signal_strength(unsigned int strength, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.signal_strength != strength) {
+ backend->modem.signal_strength = strength;
+ send_ciev_for_each_rfcomm(backend, CIND_SIGNAL, strength);
+ }
+}
+
+static void send_cmd_result(bool success, enum cmee_error error, void *user_data)
+{
+ struct rfcomm *rfcomm = user_data;
+
+ if (success) {
+ rfcomm_send_reply(rfcomm, "OK");
+ return;
+ }
+
+ rfcomm_send_error(rfcomm, error);
+}
+
+static int backend_native_free(void *data)
+{
+ struct impl *backend = data;
+
+ struct rfcomm *rfcomm;
+
+ sco_close(backend);
+
+ if (backend->modemmanager) {
+ mm_unregister(backend);
+ backend->modemmanager = NULL;
+ }
+
+ if (backend->upower) {
+ upower_unregister(backend->upower);
+ backend->upower = NULL;
+ }
+
+ if (backend->ring_timer)
+ spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG);
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG);
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_HF);
+#endif
+
+ spa_list_consume(rfcomm, &backend->rfcomm_list, link)
+ rfcomm_free(rfcomm);
+
+ if (backend->modem.operator_name)
+ free(backend->modem.operator_name);
+ free(backend);
+
+ return 0;
+}
+
+static int parse_headset_roles(struct impl *backend, const struct spa_dict *info)
+{
+ const char *str;
+ int profiles = SPA_BT_PROFILE_NULL;
+
+ if (info == NULL ||
+ (str = spa_dict_lookup(info, PROP_KEY_HEADSET_ROLES)) == NULL)
+ goto fallback;
+
+ profiles = spa_bt_profiles_from_json_array(str);
+ if (profiles < 0)
+ goto fallback;
+
+ backend->enabled_profiles = profiles & SPA_BT_PROFILE_HEADSET_AUDIO;
+ return 0;
+fallback:
+ backend->enabled_profiles = DEFAULT_ENABLED_PROFILES;
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_native_free,
+ .register_profiles = backend_native_register_profiles,
+ .unregister_profiles = backend_native_unregister_profiles,
+ .ensure_codec = backend_native_ensure_codec,
+ .supports_codec = backend_native_supports_codec,
+};
+
+static const struct mm_ops mm_ops = {
+ .send_cmd_result = send_cmd_result,
+ .set_modem_service = set_modem_service,
+ .set_modem_signal_strength = set_modem_signal_strength,
+ .set_modem_operator_name = set_modem_operator_name,
+ .set_modem_own_number = set_modem_own_number,
+ .set_modem_roaming = set_modem_roaming,
+ .set_call_active = set_call_active,
+ .set_call_setup = set_call_setup,
+};
+
+struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+
+ static const DBusObjectPathVTable vtable_profile = {
+ .message_function = profile_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "native";
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
+ backend->conn = dbus_connection;
+ backend->sco.fd = -1;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ spa_list_init(&backend->rfcomm_list);
+
+ if (parse_headset_roles(backend, info) < 0)
+ goto fail;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HSP_AG,
+ &vtable_profile, backend)) {
+ goto fail;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HSP_HS,
+ &vtable_profile, backend)) {
+ goto fail1;
+ }
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HFP_AG,
+ &vtable_profile, backend)) {
+ goto fail2;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HFP_HF,
+ &vtable_profile, backend)) {
+ goto fail3;
+ }
+#endif
+
+ backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend);
+ backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend);
+
+ return &backend->this;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+fail3:
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG);
+fail2:
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS);
+fail1:
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG);
+#endif
+fail:
+ free(backend);
+ return NULL;
+}
diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c
new file mode 100644
index 0000000..3ba2b03
--- /dev/null
+++ b/spa/plugins/bluez5/backend-ofono.c
@@ -0,0 +1,947 @@
+/* Spa oFono backend
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <poll.h>
+#include <sys/socket.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sco.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+#define INITIAL_INTERVAL_NSEC (500 * SPA_NSEC_PER_MSEC)
+#define ACTION_INTERVAL_NSEC (3000 * SPA_NSEC_PER_MSEC)
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.ofono");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_dbus *dbus;
+ struct spa_loop_utils *loop_utils;
+ DBusConnection *conn;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_source *timer;
+
+ unsigned int filters_added:1;
+ unsigned int msbc_supported:1;
+};
+
+struct transport_data {
+ struct spa_source sco;
+ unsigned int broken:1;
+ unsigned int activated:1;
+};
+
+#define OFONO_HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager"
+#define OFONO_HF_AUDIO_CARD_INTERFACE OFONO_SERVICE ".HandsfreeAudioCard"
+#define OFONO_HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent"
+
+#define OFONO_AUDIO_CLIENT "/Profile/ofono"
+
+#define OFONO_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" OFONO_HF_AUDIO_AGENT_INTERFACE "\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg name=\"card\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
+ " <arg name=\"codec\" direction=\"in\" type=\"b\"/>" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define OFONO_ERROR_INVALID_ARGUMENTS "org.ofono.Error.InvalidArguments"
+#define OFONO_ERROR_NOT_IMPLEMENTED "org.ofono.Error.NotImplemented"
+#define OFONO_ERROR_IN_USE "org.ofono.Error.InUse"
+#define OFONO_ERROR_FAILED "org.ofono.Error.Failed"
+
+static void ofono_transport_get_mtu(struct impl *backend, struct spa_bt_transport *t)
+{
+ struct sco_options sco_opt;
+ socklen_t len;
+
+ /* Fallback values */
+ t->read_mtu = 48;
+ t->write_mtu = 48;
+
+ len = sizeof(sco_opt);
+ memset(&sco_opt, 0, len);
+
+ if (getsockopt(t->fd, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
+ spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults");
+ else {
+ spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu);
+ t->read_mtu = sco_opt.mtu;
+ t->write_mtu = sco_opt.mtu;
+ }
+}
+
+static struct spa_bt_transport *_transport_create(struct impl *backend,
+ const char *path,
+ struct spa_bt_device *device,
+ enum spa_bt_profile profile,
+ int codec,
+ struct spa_callbacks *impl)
+{
+ struct spa_bt_transport *t = NULL;
+ char *t_path = strdup(path);
+
+ t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct transport_data));
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ free(t_path);
+ goto finish;
+ }
+ spa_bt_transport_set_implementation(t, impl, t);
+
+ t->device = device;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->backend = &backend->this;
+ t->profile = profile;
+ t->codec = codec;
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+finish:
+ return t;
+}
+
+static int _audio_acquire(struct impl *backend, const char *path, uint8_t *codec)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ int ret = 0;
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, path,
+ OFONO_HF_AUDIO_CARD_INTERFACE,
+ "Acquire");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ /*
+ * XXX: We assume here oFono replies. It however can happen that the headset does
+ * XXX: not properly respond to the codec negotiation RFCOMM commands.
+ * XXX: oFono (1.34) fails to handle this condition, and will not send DBus reply
+ * XXX: in this case. The transport acquire API is synchronous, so we can't
+ * XXX: do better here right now.
+ */
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_error(backend->log, "Transport Acquire() failed for transport %s (%s)",
+ path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Acquire returned error: %s", dbus_message_get_error_name(r));
+ ret = -EIO;
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_UNIX_FD, &ret,
+ DBUS_TYPE_BYTE, codec,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(backend->log, "Failed to parse Acquire() reply: %s", err.message);
+ dbus_error_free(&err);
+ ret = -EIO;
+ goto finish;
+ }
+
+finish:
+ dbus_message_unref(r);
+ return ret;
+}
+
+static int ofono_audio_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct transport_data *td = transport->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ uint8_t codec;
+ int ret = 0;
+
+ if (transport->fd >= 0)
+ goto finish;
+ if (td->broken) {
+ ret = -EIO;
+ goto finish;
+ }
+
+ spa_bt_device_update_last_bluez_action_time(transport->device);
+
+ ret = _audio_acquire(backend, transport->path, &codec);
+ if (ret < 0)
+ goto finish;
+
+ transport->fd = ret;
+
+ if (transport->codec != codec) {
+ struct timespec ts;
+
+ spa_log_info(backend->log, "transport %p: acquired codec (%d) differs from transport one (%d)",
+ transport, codec, transport->codec);
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+
+ /* schedule immediate profile update, from main loop */
+ transport->codec = codec;
+ td->broken = true;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 1;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ return -EIO;
+ }
+
+ td->broken = false;
+
+ spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %d", transport,
+ transport->path, transport->fd, transport->codec);
+
+ ofono_transport_get_mtu(backend, transport);
+ ret = 0;
+
+finish:
+ return ret;
+}
+
+static int ofono_audio_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+
+ spa_log_debug(backend->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+
+ return 0;
+}
+
+static DBusHandlerResult ofono_audio_card_removed(struct impl *backend, const char *path)
+{
+ struct spa_bt_transport *transport;
+
+ spa_assert(backend);
+ spa_assert(path);
+
+ spa_log_debug(backend->log, "card removed: %s", path);
+
+ transport = spa_bt_transport_find(backend->monitor, path);
+
+ if (transport != NULL) {
+ struct spa_bt_device *device = transport->device;
+
+ spa_log_debug(backend->log, "transport %p: free %s",
+ transport, transport->path);
+
+ spa_bt_transport_free(transport);
+ if (device != NULL)
+ spa_bt_device_check_profiles(device, false);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static const struct spa_bt_transport_implementation ofono_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = ofono_audio_acquire,
+ .release = ofono_audio_release,
+};
+
+bool activate_transport(struct spa_bt_transport *t, const void *data)
+{
+ struct impl *backend = (void *)data;
+ struct transport_data *td = t->user_data;
+ struct timespec ts;
+ uint64_t now, threshold;
+
+ if (t->backend != &backend->this)
+ return false;
+
+ /* Check device-specific rate limit */
+ spa_system_clock_gettime(backend->main_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ threshold = t->device->last_bluez_action_time + ACTION_INTERVAL_NSEC;
+ if (now < threshold) {
+ ts.tv_sec = (threshold - now) / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = (threshold - now) % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ return false;
+ }
+
+ if (!td->activated) {
+ /* Connect profile */
+ spa_log_debug(backend->log, "Transport %s activated", t->path);
+ td->activated = true;
+ spa_bt_device_connect_profile(t->device, t->profile);
+ }
+
+ if (td->broken) {
+ /* Recreate the transport */
+ struct spa_bt_transport *t_copy;
+
+ t_copy = _transport_create(backend, t->path, t->device,
+ t->profile, t->codec, (struct spa_callbacks *)&ofono_transport_impl);
+ spa_bt_transport_free(t);
+
+ if (t_copy)
+ spa_bt_device_connect_profile(t_copy->device, t_copy->profile);
+
+ return true;
+ }
+
+ return false;
+}
+
+static void activate_transports(struct impl *backend)
+{
+ while (spa_bt_transport_find_full(backend->monitor, activate_transport, backend));
+}
+
+static void activate_timer_event(void *userdata, uint64_t expirations)
+{
+ struct impl *backend = userdata;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer, NULL, NULL, false);
+ activate_transports(backend);
+}
+
+static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path, DBusMessageIter *props_i)
+{
+ const char *remote_address = NULL;
+ const char *local_address = NULL;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t;
+ struct transport_data *td;
+ enum spa_bt_profile profile = SPA_BT_PROFILE_HFP_AG;
+ uint8_t codec = backend->msbc_supported ?
+ HFP_AUDIO_CODEC_MSBC : HFP_AUDIO_CODEC_CVSD;
+
+ spa_assert(backend);
+ spa_assert(path);
+ spa_assert(props_i);
+
+ spa_log_debug(backend->log, "new card: %s", path);
+
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key, *value;
+ char c;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) {
+ spa_log_error(backend->log, "Invalid properties for %s: expected 's', received '%c'", path, c);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_get_basic(&value_i, &value);
+
+ if (spa_streq(key, "RemoteAddress")) {
+ remote_address = value;
+ } else if (spa_streq(key, "LocalAddress")) {
+ local_address = value;
+ } else if (spa_streq(key, "Type")) {
+ if (spa_streq(value, "gateway"))
+ profile = SPA_BT_PROFILE_HFP_HF;
+ }
+
+ spa_log_debug(backend->log, "%s: %s", key, value);
+
+ dbus_message_iter_next(props_i);
+ }
+
+ if (!remote_address || !local_address) {
+ spa_log_error(backend->log, "Missing addresses for %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ d = spa_bt_device_find_by_address(backend->monitor, remote_address, local_address);
+ if (!d || !d->adapter) {
+ spa_log_error(backend->log, "Device doesn’t exist for %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_bt_device_add_profile(d, profile);
+
+ t = _transport_create(backend, path, d, profile, codec, (struct spa_callbacks *)&ofono_transport_impl);
+ if (t == NULL) {
+ spa_log_error(backend->log, "failed to create transport: %s", spa_strerror(-errno));
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ td = t->user_data;
+
+ /*
+ * For HF profile, delay profile connect, so that we likely don't do it at the
+ * same time as the device is busy with A2DP connect. This avoids some oFono
+ * misbehavior (see comment in _audio_acquire above).
+ *
+ * For AG mode, we delay the emission of the nodes, so it is not necessary
+ * to know the codec in advance.
+ */
+ if (profile == SPA_BT_PROFILE_HFP_HF) {
+ struct timespec ts;
+ ts.tv_sec = INITIAL_INTERVAL_NSEC / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = INITIAL_INTERVAL_NSEC % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ } else {
+ td->activated = true;
+ spa_bt_device_connect_profile(t->device, t->profile);
+ }
+
+ spa_log_debug(backend->log, "Transport %s available, codec %d", t->path, t->codec);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult ofono_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+
+ spa_log_warn(backend->log, "release");
+
+ r = dbus_message_new_error(m, OFONO_HF_AUDIO_AGENT_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void sco_event(struct spa_source *source)
+{
+ struct spa_bt_transport *t = source->data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno));
+ if (t->fd >= 0) {
+ if (source->loop)
+ spa_loop_remove_source(source->loop, source);
+ shutdown(t->fd, SHUT_RDWR);
+ close (t->fd);
+ t->fd = -1;
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE);
+ }
+ }
+}
+
+static int enable_sco_socket(int sock)
+{
+ char c;
+ struct pollfd pfd;
+
+ if (sock < 0)
+ return ENOTCONN;
+
+ memset(&pfd, 0, sizeof(pfd));
+ pfd.fd = sock;
+ pfd.events = POLLOUT;
+
+ if (poll(&pfd, 1, 0) < 0)
+ return errno;
+
+ /*
+ * If socket already writable then it is not in defer setup state,
+ * otherwise it needs to be read to authorize the connection.
+ */
+ if ((pfd.revents & POLLOUT))
+ return 0;
+
+ /* Enable socket by reading 1 byte */
+ if (read(sock, &c, 1) < 0)
+ return errno;
+
+ return 0;
+}
+
+static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path;
+ int fd;
+ uint8_t codec;
+ struct spa_bt_transport *t;
+ struct transport_data *td;
+ DBusMessage *r = NULL;
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_OBJECT_PATH, &path,
+ DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_BYTE, &codec,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, OFONO_ERROR_INVALID_ARGUMENTS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ t = spa_bt_transport_find(backend->monitor, path);
+ if (t && (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) {
+ int err;
+
+ err = enable_sco_socket(fd);
+ if (err) {
+ spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(err));
+ r = dbus_message_new_error(m, OFONO_ERROR_FAILED, "SCO authorization failed");
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ goto fail;
+ }
+
+ t->fd = fd;
+ t->codec = codec;
+
+ spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %d",
+ t, t->path, t->fd, t->codec);
+
+ td = t->user_data;
+ td->sco.func = sco_event;
+ td->sco.data = t;
+ td->sco.fd = fd;
+ td->sco.mask = SPA_IO_HUP | SPA_IO_ERR;
+ td->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &td->sco);
+
+ ofono_transport_get_mtu(backend, t);
+ spa_bt_transport_set_state (t, SPA_BT_TRANSPORT_STATE_PENDING);
+ }
+ else if (fd) {
+ spa_log_debug(backend->log, "ignoring NewConnection");
+ r = dbus_message_new_error(m, OFONO_ERROR_NOT_IMPLEMENTED, "Method not implemented");
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ }
+
+fail:
+ if (r) {
+ DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ res = DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+ return res;
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult ofono_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = OFONO_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "Release"))
+ res = ofono_release(c, m, userdata);
+ else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "NewConnection"))
+ res = ofono_new_audio_connection(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void ofono_getcards_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i, struct_i, props_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get a list of handsfree audio cards: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a(oa{sv})")) {
+ spa_log_error(backend->log, "Invalid arguments in GetCards() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ char *path;
+
+ dbus_message_iter_recurse(&array_i, &struct_i);
+ dbus_message_iter_get_basic(&struct_i, &path);
+ dbus_message_iter_next(&struct_i);
+
+ dbus_message_iter_recurse(&struct_i, &props_i);
+
+ ofono_audio_card_found(backend, path, &props_i);
+
+ dbus_message_iter_next(&array_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int backend_ofono_register(void *data)
+{
+ struct impl *backend = data;
+
+ DBusMessage *m, *r;
+ const char *path = OFONO_AUDIO_CLIENT;
+ uint8_t codecs[2];
+ const uint8_t *pcodecs = codecs;
+ int ncodecs = 0, res;
+ DBusPendingCall *call;
+ DBusError err;
+
+ spa_log_debug(backend->log, "Registering");
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ OFONO_HF_AUDIO_MANAGER_INTERFACE, "Register");
+ if (m == NULL)
+ return -ENOMEM;
+
+ codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD;
+ if (backend->msbc_supported)
+ codecs[ncodecs++] = HFP_AUDIO_CODEC_MSBC;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path,
+ DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs,
+ DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+ spa_log_info(backend->log, "oFono not available: %s",
+ err.message);
+ res = -ENOTSUP;
+ } else {
+ spa_log_warn(backend->log, "Registering Profile %s failed: %s (%s)",
+ path, err.message, err.name);
+ res = -EIO;
+ }
+ dbus_error_free(&err);
+ return res;
+ }
+
+ if (dbus_message_is_error(r, OFONO_ERROR_INVALID_ARGUMENTS)) {
+ spa_log_warn(backend->log, "invalid arguments");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, OFONO_ERROR_IN_USE)) {
+ spa_log_warn(backend->log, "already in use");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(backend->log, "Error registering profile");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_SERVICE_UNKNOWN)) {
+ spa_log_info(backend->log, "oFono not available, disabling");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Register() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+ dbus_message_unref(r);
+
+ spa_log_debug(backend->log, "registered");
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ OFONO_HF_AUDIO_MANAGER_INTERFACE, "GetCards");
+ if (m == NULL)
+ goto finish;
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, ofono_getcards_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+
+finish:
+ dbus_message_unref(r);
+ return -EIO;
+}
+
+static DBusHandlerResult ofono_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardAdded")) {
+ char *p;
+ DBusMessageIter arg_i, props_i;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sv}")) {
+ spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardAdded");
+ goto fail;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &p);
+
+ dbus_message_iter_next(&arg_i);
+ spa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&arg_i, &props_i);
+
+ return ofono_audio_card_found(backend, p, &props_i);
+ } else if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardRemoved")) {
+ const char *p;
+
+ if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) {
+ spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message);
+ goto fail;
+ }
+
+ return ofono_audio_card_removed(backend, p);
+ }
+
+fail:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *backend)
+{
+ DBusError err;
+
+ if (backend->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(backend->conn, ofono_filter_cb, backend, NULL)) {
+ spa_log_error(backend->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" OFONO_SERVICE "',"
+ "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" OFONO_SERVICE "',"
+ "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", &err);
+
+ backend->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static int backend_ofono_free(void *data)
+{
+ struct impl *backend = data;
+
+ if (backend->filters_added) {
+ dbus_connection_remove_filter(backend->conn, ofono_filter_cb, backend);
+ backend->filters_added = false;
+ }
+
+ if (backend->timer)
+ spa_loop_utils_destroy_source(backend->loop_utils, backend->timer);
+
+ dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
+
+ free(backend);
+
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_ofono_free,
+ .register_profiles = backend_ofono_register,
+};
+
+static bool is_available(struct impl *backend)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ DBUS_INTERFACE_INTROSPECTABLE, "Introspect");
+ if (m == NULL)
+ return false;
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
+ success = true;
+
+ if (r)
+ dbus_message_unref(r);
+ else
+ dbus_error_free(&err);
+
+ return success;
+}
+
+struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+ const char *str;
+ static const DBusObjectPathVTable vtable_profile = {
+ .message_function = ofono_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "ofono";
+ backend->this.exclusive = true;
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
+ backend->conn = dbus_connection;
+ if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc")))
+ backend->msbc_supported = spa_atob(str);
+ else
+ backend->msbc_supported = false;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ backend->timer = spa_loop_utils_add_timer(backend->loop_utils, activate_timer_event, backend);
+ if (backend->timer == NULL) {
+ free(backend);
+ return NULL;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ OFONO_AUDIO_CLIENT,
+ &vtable_profile, backend)) {
+ free(backend);
+ return NULL;
+ }
+
+ if (add_filters(backend) < 0) {
+ dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
+ free(backend);
+ return NULL;
+ }
+
+ backend->this.available = is_available(backend);
+
+ return &backend->this;
+}
diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h
new file mode 100644
index 0000000..7bfac35
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-caps.h
@@ -0,0 +1,142 @@
+/* Spa BAP codec API
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BLUEZ5_BAP_CODEC_CAPS_H_
+#define SPA_BLUEZ5_BAP_CODEC_CAPS_H_
+
+#define BAP_CODEC_LC3 0x06
+
+#define LC3_TYPE_FREQ 0x01
+#define LC3_FREQ_8KHZ (1 << 0)
+#define LC3_FREQ_11KHZ (1 << 1)
+#define LC3_FREQ_16KHZ (1 << 2)
+#define LC3_FREQ_22KHZ (1 << 3)
+#define LC3_FREQ_24KHZ (1 << 4)
+#define LC3_FREQ_32KHZ (1 << 5)
+#define LC3_FREQ_44KHZ (1 << 6)
+#define LC3_FREQ_48KHZ (1 << 7)
+#define LC3_FREQ_ANY (LC3_FREQ_8KHZ | \
+ LC3_FREQ_11KHZ | \
+ LC3_FREQ_16KHZ | \
+ LC3_FREQ_22KHZ | \
+ LC3_FREQ_24KHZ | \
+ LC3_FREQ_32KHZ | \
+ LC3_FREQ_44KHZ | \
+ LC3_FREQ_48KHZ)
+
+#define LC3_TYPE_DUR 0x02
+#define LC3_DUR_7_5 (1 << 0)
+#define LC3_DUR_10 (1 << 1)
+#define LC3_DUR_ANY (LC3_DUR_7_5 | \
+ LC3_DUR_10)
+
+#define LC3_TYPE_CHAN 0x03
+#define LC3_CHAN_1 (1 << 0)
+#define LC3_CHAN_2 (1 << 1)
+
+#define LC3_TYPE_FRAMELEN 0x04
+#define LC3_TYPE_BLKS 0x05
+
+/* LC3 config parameters */
+#define LC3_CONFIG_FREQ_8KHZ 0x01
+#define LC3_CONFIG_FREQ_11KHZ 0x02
+#define LC3_CONFIG_FREQ_16KHZ 0x03
+#define LC3_CONFIG_FREQ_22KHZ 0x04
+#define LC3_CONFIG_FREQ_24KHZ 0x05
+#define LC3_CONFIG_FREQ_32KHZ 0x06
+#define LC3_CONFIG_FREQ_44KHZ 0x07
+#define LC3_CONFIG_FREQ_48KHZ 0x08
+
+#define LC3_CONFIG_DURATION_7_5 0x00
+#define LC3_CONFIG_DURATION_10 0x01
+
+#define LC3_CONFIG_CHNL_NOT_ALLOWED 0x00000000
+#define LC3_CONFIG_CHNL_FL 0x00000001 /* front left */
+#define LC3_CONFIG_CHNL_FR 0x00000002 /* front right */
+#define LC3_CONFIG_CHNL_FC 0x00000004 /* front center */
+#define LC3_CONFIG_CHNL_LFE 0x00000008 /* LFE */
+#define LC3_CONFIG_CHNL_BL 0x00000010 /* back left */
+#define LC3_CONFIG_CHNL_BR 0x00000020 /* back right */
+#define LC3_CONFIG_CHNL_FLC 0x00000040 /* front left center */
+#define LC3_CONFIG_CHNL_FRC 0x00000080 /* front right center */
+#define LC3_CONFIG_CHNL_BC 0x00000100 /* back center */
+#define LC3_CONFIG_CHNL_LFE2 0x00000200 /* LFE 2 */
+#define LC3_CONFIG_CHNL_SL 0x00000400 /* side left */
+#define LC3_CONFIG_CHNL_SR 0x00000800 /* side right */
+#define LC3_CONFIG_CHNL_TFL 0x00001000 /* top front left */
+#define LC3_CONFIG_CHNL_TFR 0x00002000 /* top front right */
+#define LC3_CONFIG_CHNL_TFC 0x00004000 /* top front center */
+#define LC3_CONFIG_CHNL_TC 0x00008000 /* top center */
+#define LC3_CONFIG_CHNL_TBL 0x00010000 /* top back left */
+#define LC3_CONFIG_CHNL_TBR 0x00020000 /* top back right */
+#define LC3_CONFIG_CHNL_TSL 0x00040000 /* top side left */
+#define LC3_CONFIG_CHNL_TSR 0x00080000 /* top side right */
+#define LC3_CONFIG_CHNL_TBC 0x00100000 /* top back center */
+#define LC3_CONFIG_CHNL_BFC 0x00200000 /* bottom front center */
+#define LC3_CONFIG_CHNL_BFL 0x00400000 /* bottom front left */
+#define LC3_CONFIG_CHNL_BFR 0x00800000 /* bottom front right */
+#define LC3_CONFIG_CHNL_FLW 0x01000000 /* front left wide */
+#define LC3_CONFIG_CHNL_FRW 0x02000000 /* front right wide */
+#define LC3_CONFIG_CHNL_LS 0x04000000 /* left surround */
+#define LC3_CONFIG_CHNL_RS 0x08000000 /* right surround */
+
+#define LC3_MAX_CHANNELS 28
+
+typedef struct {
+ uint8_t rate;
+ uint8_t frame_duration;
+ uint32_t channels;
+ uint16_t framelen;
+ uint8_t n_blks;
+} __attribute__ ((packed)) bap_lc3_t;
+
+#define BT_ISO_QOS_CIG_UNSET 0xff
+#define BT_ISO_QOS_CIS_UNSET 0xff
+
+#define BT_ISO_QOS_TARGET_LATENCY_LOW 0x01
+#define BT_ISO_QOS_TARGET_LATENCY_BALANCED 0x02
+#define BT_ISO_QOS_TARGET_LATENCY_RELIABILITY 0x03
+
+struct bap_endpoint_qos {
+ uint8_t framing;
+ uint8_t phy;
+ uint8_t retransmission;
+ uint16_t latency;
+ uint32_t delay_min;
+ uint32_t delay_max;
+ uint32_t preferred_delay_min;
+ uint32_t preferred_delay_max;
+};
+
+struct bap_codec_qos {
+ uint32_t interval;
+ uint8_t framing;
+ uint8_t phy;
+ uint16_t sdu;
+ uint8_t retransmission;
+ uint16_t latency;
+ uint32_t delay;
+ uint8_t target_latency;
+};
+
+#endif
diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c
new file mode 100644
index 0000000..fe81168
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-lc3.c
@@ -0,0 +1,859 @@
+/* Spa BAP LC3 codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#include <bluetooth/bluetooth.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <lc3.h>
+
+#include "media-codecs.h"
+#include "bap-codec-caps.h"
+
+#define MAX_PACS 64
+
+struct impl {
+ lc3_encoder_t enc[LC3_MAX_CHANNELS];
+ lc3_decoder_t dec[LC3_MAX_CHANNELS];
+
+ int mtu;
+ int samplerate;
+ int channels;
+ int frame_dus;
+ int framelen;
+ int samples;
+ unsigned int codesize;
+};
+
+struct __attribute__((packed)) ltv {
+ uint8_t len;
+ uint8_t type;
+ uint8_t value[];
+};
+
+struct pac_data {
+ const uint8_t *data;
+ size_t size;
+};
+
+static int write_ltv(uint8_t *dest, uint8_t type, void* value, size_t len)
+{
+ struct ltv *ltv = (struct ltv *)dest;
+
+ ltv->len = len + 1;
+ ltv->type = type;
+ memcpy(ltv->value, value, len);
+
+ return len + 2;
+}
+
+static int write_ltv_uint8(uint8_t *dest, uint8_t type, uint8_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint16(uint8_t *dest, uint8_t type, uint16_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint32(uint8_t *dest, uint8_t type, uint32_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ uint8_t *data = caps;
+ uint16_t framelen[2] = {htobs(LC3_MIN_FRAME_BYTES), htobs(LC3_MAX_FRAME_BYTES)};
+
+ data += write_ltv_uint16(data, LC3_TYPE_FREQ,
+ htobs(LC3_FREQ_48KHZ | LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ));
+ data += write_ltv_uint8(data, LC3_TYPE_DUR, LC3_DUR_ANY);
+ data += write_ltv_uint8(data, LC3_TYPE_CHAN, LC3_CHAN_1 | LC3_CHAN_2);
+ data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen));
+ data += write_ltv_uint8(data, LC3_TYPE_BLKS, 2);
+
+ return data - caps;
+}
+
+static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS])
+{
+ /*
+ * BlueZ capabilites for the same codec may contain multiple
+ * PACs separated by zero-length LTV (see BlueZ b907befc2d80)
+ */
+ int pac = 0;
+
+ pacs[pac] = (struct pac_data){ data, 0 };
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len == 0) {
+ /* delimiter */
+ if (pac + 1 >= MAX_PACS)
+ break;
+
+ ++pac;
+ pacs[pac] = (struct pac_data){ data + 1, 0 };
+ } else if (ltv->len >= data_size) {
+ return -EINVAL;
+ } else {
+ pacs[pac].size += ltv->len + 1;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ return pac + 1;
+}
+
+static bool parse_capabilities(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+ uint16_t framelen_min = 0, framelen_max = 0;
+
+ if (!data_size)
+ return false;
+ memset(conf, 0, sizeof(*conf));
+
+ conf->frame_duration = 0xFF;
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size)
+ return false;
+
+ switch (ltv->type) {
+ case LC3_TYPE_FREQ:
+ spa_return_val_if_fail(ltv->len == 3, false);
+ {
+ uint16_t rate = ltv->value[0] + (ltv->value[1] << 8);
+ if (rate & LC3_FREQ_48KHZ)
+ conf->rate = LC3_CONFIG_FREQ_48KHZ;
+ else if (rate & LC3_FREQ_24KHZ)
+ conf->rate = LC3_CONFIG_FREQ_24KHZ;
+ else if (rate & LC3_FREQ_16KHZ)
+ conf->rate = LC3_CONFIG_FREQ_16KHZ;
+ else if (rate & LC3_FREQ_8KHZ)
+ conf->rate = LC3_CONFIG_FREQ_8KHZ;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_DUR:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ {
+ uint8_t duration = ltv->value[0];
+ if (duration & LC3_DUR_10)
+ conf->frame_duration = LC3_CONFIG_DURATION_10;
+ else if (duration & LC3_DUR_7_5)
+ conf->frame_duration = LC3_CONFIG_DURATION_7_5;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_CHAN:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ {
+ uint8_t channels = ltv->value[0];
+ /* Only mono or stereo streams are currently supported,
+ * in both case Audio location is defined as both Front Left
+ * and Front Right, difference is done by the n_blks parameter.
+ */
+ if ((channels & LC3_CHAN_2) || (channels & LC3_CHAN_1))
+ conf->channels = LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_FRAMELEN:
+ spa_return_val_if_fail(ltv->len == 5, false);
+ framelen_min = ltv->value[0] + (ltv->value[1] << 8);
+ framelen_max = ltv->value[2] + (ltv->value[3] << 8);
+ break;
+ case LC3_TYPE_BLKS:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->n_blks = ltv->value[0];
+ if (!conf->n_blks)
+ return false;
+ break;
+ default:
+ return false;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES)
+ return false;
+ if (conf->frame_duration == 0xFF || !conf->rate)
+ return false;
+ if (!conf->channels)
+ conf->channels = LC3_CONFIG_CHNL_FL;
+
+ switch (conf->rate) {
+ case LC3_CONFIG_FREQ_48KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 117;
+ else
+ conf->framelen = 120;
+ break;
+ case LC3_CONFIG_FREQ_24KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 45;
+ else
+ conf->framelen = 60;
+ break;
+ case LC3_CONFIG_FREQ_16KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 30;
+ else
+ conf->framelen = 40;
+ break;
+ case LC3_CONFIG_FREQ_8KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 26;
+ else
+ conf->framelen = 30;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+static bool parse_conf(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+ if (!data_size)
+ return false;
+ memset(conf, 0, sizeof(*conf));
+
+ conf->frame_duration = 0xFF;
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size)
+ return false;
+
+ switch (ltv->type) {
+ case LC3_TYPE_FREQ:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->rate = ltv->value[0];
+ break;
+ case LC3_TYPE_DUR:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->frame_duration = ltv->value[0];
+ break;
+ case LC3_TYPE_CHAN:
+ spa_return_val_if_fail(ltv->len == 5, false);
+ conf->channels = ltv->value[0] + (ltv->value[1] << 8) + (ltv->value[2] << 16) + (ltv->value[3] << 24);
+ break;
+ case LC3_TYPE_FRAMELEN:
+ spa_return_val_if_fail(ltv->len == 3, false);
+ conf->framelen = ltv->value[0] + (ltv->value[1] << 8);
+ break;
+ case LC3_TYPE_BLKS:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->n_blks = ltv->value[0];
+ if (!conf->n_blks)
+ return false;
+ break;
+ default:
+ return false;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ if (conf->frame_duration == 0xFF || !conf->rate)
+ return false;
+
+ return true;
+}
+
+static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, int res2)
+{
+ const bap_lc3_t *conf;
+ int a, b;
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = conf1; \
+ a = (expr); \
+ conf = conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(bap_lc3_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(bap_lc3_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->channels & LC3_CHAN_2);
+ PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_48KHZ | LC3_CONFIG_FREQ_24KHZ | LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_8KHZ));
+ PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_48KHZ);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static int pac_cmp(const void *p1, const void *p2)
+{
+ const struct pac_data *pac1 = p1;
+ const struct pac_data *pac2 = p2;
+ bap_lc3_t conf1, conf2;
+ int res1, res2;
+
+ res1 = parse_capabilities(&conf1, pac1->data, pac1->size) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+ res2 = parse_capabilities(&conf2, pac2->data, pac2->size) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+
+ return conf_cmp(&conf1, res1, &conf2, res2);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ struct pac_data pacs[MAX_PACS];
+ int npacs;
+ bap_lc3_t conf;
+ uint8_t *data = config;
+
+ if (caps == NULL)
+ return -EINVAL;
+
+ /* Select best conf from those possible */
+ npacs = parse_bluez_pacs(caps, caps_size, pacs);
+ if (npacs < 0)
+ return npacs;
+ else if (npacs == 0)
+ return -EINVAL;
+
+ qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp);
+
+ if (!parse_capabilities(&conf, pacs[0].data, pacs[0].size))
+ return -ENOTSUP;
+
+ data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate);
+ data += write_ltv_uint8(data, LC3_TYPE_DUR, conf.frame_duration);
+ data += write_ltv_uint32(data, LC3_TYPE_CHAN, htobl(conf.channels));
+ data += write_ltv_uint16(data, LC3_TYPE_FRAMELEN, htobs(conf.framelen));
+ data += write_ltv_uint8(data, LC3_TYPE_BLKS, conf.n_blks);
+
+ return data - config;
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ bap_lc3_t conf1, conf2;
+ int res1, res2;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+ return conf_cmp(&conf1, res1, &conf2, res2);
+}
+
+static uint8_t channels_to_positions(uint32_t channels, uint8_t n_channels, uint32_t *position)
+{
+ uint8_t n_positions = 0;
+
+ spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS);
+
+ /* First check if stream is configure for Mono, i.e. 1 block for both Front
+ * Left anf Front Right,
+ * else map LE Audio locations to PipeWire locations in the ascending order
+ * which will be used as block order in stream.
+ */
+ if ((channels & (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL)) == (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL) &&
+ n_channels == 1) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ n_positions = 1;
+ } else {
+#define CHANNEL_2_SPACHANNEL(channel,spa_channel) if (channels & channel) position[n_positions++] = spa_channel;
+
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FL, SPA_AUDIO_CHANNEL_FL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FR, SPA_AUDIO_CHANNEL_FR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FC, SPA_AUDIO_CHANNEL_FC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE, SPA_AUDIO_CHANNEL_LFE);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BL, SPA_AUDIO_CHANNEL_RL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BR, SPA_AUDIO_CHANNEL_RR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLC, SPA_AUDIO_CHANNEL_FLC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRC, SPA_AUDIO_CHANNEL_FRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BC, SPA_AUDIO_CHANNEL_BC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE2, SPA_AUDIO_CHANNEL_LFE2);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SL, SPA_AUDIO_CHANNEL_SL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SR, SPA_AUDIO_CHANNEL_SR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFL, SPA_AUDIO_CHANNEL_TFL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFR, SPA_AUDIO_CHANNEL_TFR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFC, SPA_AUDIO_CHANNEL_TFC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TC, SPA_AUDIO_CHANNEL_TC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBL, SPA_AUDIO_CHANNEL_TRL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBR, SPA_AUDIO_CHANNEL_TRR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSL, SPA_AUDIO_CHANNEL_TSL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSR, SPA_AUDIO_CHANNEL_TSR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBC, SPA_AUDIO_CHANNEL_TRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFC, SPA_AUDIO_CHANNEL_BC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFL, SPA_AUDIO_CHANNEL_BLC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFR, SPA_AUDIO_CHANNEL_BRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLW, SPA_AUDIO_CHANNEL_FLW);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRW, SPA_AUDIO_CHANNEL_FRW);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LS, SPA_AUDIO_CHANNEL_LLFE); /* is it the right mapping? */
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_RS, SPA_AUDIO_CHANNEL_RLFE); /* is it the right mapping? */
+
+#undef CHANNEL_2_SPACHANNEL
+ }
+
+ return n_positions;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ bap_lc3_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+ uint8_t res;
+
+ if (!parse_conf(&conf, caps, caps_size))
+ return -EINVAL;
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.rate & LC3_CONFIG_FREQ_48KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_24KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 24000);
+ spa_pod_builder_int(b, 24000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_16KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_8KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 8000);
+ spa_pod_builder_int(b, 8000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ res = channels_to_positions(conf.channels, conf.n_blks, position);
+ if (res == 0)
+ return -EINVAL;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(res),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, res, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ bap_lc3_t conf;
+ uint8_t res;
+
+ if (caps == NULL)
+ return -EINVAL;
+
+ if (!parse_conf(&conf, caps, caps_size))
+ return -ENOTSUP;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S24_32;
+
+ switch (conf.rate) {
+ case LC3_CONFIG_FREQ_48KHZ:
+ info->info.raw.rate = 48000U;
+ break;
+ case LC3_CONFIG_FREQ_24KHZ:
+ info->info.raw.rate = 24000U;
+ break;
+ case LC3_CONFIG_FREQ_16KHZ:
+ info->info.raw.rate = 16000U;
+ break;
+ case LC3_CONFIG_FREQ_8KHZ:
+ info->info.raw.rate = 8000U;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ res = channels_to_positions(conf.channels, conf.n_blks, info->info.raw.position);
+ if (res == 0)
+ return -EINVAL;
+ info->info.raw.channels = res;
+
+ switch (conf.frame_duration) {
+ case LC3_CONFIG_DURATION_10:
+ case LC3_CONFIG_DURATION_7_5:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int codec_get_qos(const struct media_codec *codec,
+ const void *config, size_t config_size,
+ const struct bap_endpoint_qos *endpoint_qos,
+ struct bap_codec_qos *qos)
+{
+ bap_lc3_t conf;
+
+ spa_zero(*qos);
+
+ if (!parse_conf(&conf, config, config_size))
+ return -EINVAL;
+
+ qos->framing = false;
+ if (endpoint_qos->phy & 0x2)
+ qos->phy = 0x2;
+ else if (endpoint_qos->phy & 0x1)
+ qos->phy = 0x1;
+ else
+ qos->phy = 0x2;
+ qos->retransmission = 2; /* default */
+ qos->sdu = conf.framelen * conf.n_blks;
+ qos->latency = 20; /* default */
+ qos->delay = 40000U;
+ qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000);
+ qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED;
+
+ switch (conf.rate) {
+ case LC3_CONFIG_FREQ_8KHZ:
+ case LC3_CONFIG_FREQ_16KHZ:
+ case LC3_CONFIG_FREQ_24KHZ:
+ case LC3_CONFIG_FREQ_32KHZ:
+ qos->retransmission = 2;
+ qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 8 : 10);
+ break;
+ case LC3_CONFIG_FREQ_48KHZ:
+ qos->retransmission = 5;
+ qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 15 : 20);
+ break;
+ }
+
+ /* Clamp to ASE values */
+ if (endpoint_qos->latency >= 0x0005 && endpoint_qos->latency <= 0x0FA0)
+ /* Values outside the range are RFU */
+ qos->latency = SPA_MAX(qos->latency, endpoint_qos->latency);
+
+ if (endpoint_qos->delay_min)
+ qos->delay = SPA_MAX(qos->delay, endpoint_qos->delay_min);
+ if (endpoint_qos->delay_max)
+ qos->delay = SPA_MIN(qos->delay, endpoint_qos->delay_max);
+
+ return 0;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ bap_lc3_t conf;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ int res, ich;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+
+ if (!parse_conf(&conf, config, config_len)) {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ this->mtu = mtu;
+ this->samplerate = config_info.info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->framelen = conf.framelen;
+
+ switch (conf.frame_duration) {
+ case LC3_CONFIG_DURATION_10:
+ this->frame_dus = 10000;
+ break;
+ case LC3_CONFIG_DURATION_7_5:
+ this->frame_dus = 7500;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+ if (this->samples < 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->codesize = this->samples * this->channels * sizeof(int32_t);
+
+ if (!(flags & MEDIA_CODEC_FLAG_SINK)) {
+ for (ich = 0; ich < this->channels; ich++) {
+ this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_encoder_size(this->frame_dus, this->samplerate)));
+ if (this->enc[ich] == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+ }
+ } else {
+ for (ich = 0; ich < this->channels; ich++) {
+ this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_decoder_size(this->frame_dus, this->samplerate)));
+ if (this->dec[ich] == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+ }
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this) {
+ for (ich = 0; ich < this->channels; ich++) {
+ if (this->enc[ich])
+ free(this->enc[ich]);
+ if (this->dec[ich])
+ free(this->dec[ich]);
+ }
+ }
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ int ich;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ if (this->enc[ich])
+ free(this->enc[ich]);
+ if (this->dec[ich])
+ free(this->dec[ich]);
+ }
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return 0;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int frame_bytes;
+ int ich, res;
+ int size, processed;
+
+ frame_bytes = lc3_frame_bytes(this->frame_dus, this->samplerate);
+ processed = 0;
+ size = 0;
+
+ if (src_size < (size_t)this->codesize)
+ goto done;
+ if (dst_size < (size_t)frame_bytes)
+ goto done;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ uint8_t *in = (uint8_t *)src + (ich * 4);
+ uint8_t *out = (uint8_t *)dst + ich * this->framelen;
+ res = lc3_encode(this->enc[ich], LC3_PCM_FORMAT_S24, in, this->channels, this->framelen, out);
+ size += this->framelen;
+ if (SPA_UNLIKELY(res != 0))
+ return -EINVAL;
+ }
+ *dst_out = size;
+
+ processed += this->codesize;
+
+done:
+ spa_assert(size <= this->mtu);
+ *need_flush = NEED_FLUSH_ALL;
+
+ return processed;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int ich, res;
+ int consumed;
+ int samples;
+
+ spa_return_val_if_fail((size_t)(this->framelen * this->channels) == src_size, -EINVAL);
+ consumed = 0;
+
+ samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+ if (samples == -1)
+ return -EINVAL;
+ if (dst_size < this->codesize)
+ return -EINVAL;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ uint8_t *in = (uint8_t *)src + ich * this->framelen;
+ uint8_t *out = (uint8_t *)dst + (ich * 4);
+ res = lc3_decode(this->dec[ich], in, this->framelen, LC3_PCM_FORMAT_S24, out, this->channels);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ consumed += this->framelen;
+ }
+
+ *dst_out = this->codesize;
+
+ return consumed;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+const struct media_codec bap_codec_lc3 = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3,
+ .name = "lc3",
+ .codec_id = BAP_CODEC_LC3,
+ .bap = true,
+ .description = "LC3",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .get_qos = codec_get_qos,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "lc3",
+ &bap_codec_lc3
+);
diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf
new file mode 100644
index 0000000..0247f75
--- /dev/null
+++ b/spa/plugins/bluez5/bluez-hardware.conf
@@ -0,0 +1,103 @@
+# List of hardware/kernel features, which cannot be detected generically.
+#
+# The `feature` is enabled only if all three of adapter, device, and
+# kernel have it.
+#
+# For each of the adapter/device/kernel, the match rules are processed
+# one at a time, and the first one that matches is used.
+#
+# Features and tags:
+# msbc "standard" mSBC (60 byte tx packet)
+# msbc-alt1 USB adapters with mSBC in ALT1 setting (24 byte tx packet)
+# msbc-alt1-rtl Realtek USB adapters with mSBC in ALT1 setting (24 byte tx packet)
+# hw-volume AVRCP and HSP/HFP hardware volume support
+# hw-volume-mic Functional HSP/HFP microphone volume support
+# sbc-xq "nonstandard" SBC codec setting with better sound quality
+# faststream FastStream codec support
+# a2dp-duplex A2DP duplex codec support
+#
+# Features are disabled with the key "no-features" whose value is an
+# array of strings in the match rule.
+
+bluez5.features.device = [
+ # properties:
+ # - name
+ # - address ("ff:ff:ff:ff:ff:ff")
+ # - vendor-id ("bluetooth:ffff", "usb:ffff")
+ # - product-id
+ # - version-id
+
+ { name = "Air 1 Plus", no-features = [ hw-volume-mic ] },
+ { name = "AirPods", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "AirPods Pro", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "AXLOIE Goin", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "BAA 100", no-features = [ hw-volume ] }, # Buxton BAA 100, doesn't remember volume, #pipewire-1449
+ { name = "D50s", address = "~^00:13:ef:", no-features = [ hw-volume ] }, # volume has no effect, #pipewire-1562
+ { name = "FiiO BTR3", address = "~^40:ed:98:", no-features = [ faststream ] }, # #pipewire-1658
+ { name = "JBL Endurance RUN BT", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "JBL LIVE650BTNC" },
+ { name = "Motorola DC800", no-features = [ sbc-xq ] }, # #pipewire-1590
+ { name = "Motorola S305", no-features = [ sbc-xq ] }, # #pipewire-1590
+ { name = "Soundcore Life P2-L", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "Soundcore Motion B", no-features = [ hw-volume ] },
+ { name = "SoundCore mini", no-features = [ hw-volume ] }, # #pipewire-1686
+ { name = "SoundCore 2", no-features = [ sbc-xq ] }, # #pipewire-2291
+ { name = "Tribit MAXSound Plus", no-features = [ hw-volume ] }, # #pipewire-1592
+ { name = "Urbanista Stockholm Plus", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+
+ { address = "~^44:5e:cd:", no-features = [ faststream, a2dp-duplex ]}, # #pipewire-1756
+
+ { address = "~^94:16:25:", no-features = [ hw-volume ]}, # AirPods 2
+ { address = "~^9c:64:8b:", no-features = [ hw-volume ]}, # AirPods 2
+ { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # Ausdom M05
+ { address = "~^0c:a6:94:", no-features = [ hw-volume ]}, # deepblue2
+ { address = "~^00:14:02:", no-features = [ hw-volume ]}, # iKross IKBT83B HS
+ { address = "~^44:5e:f3:", no-features = [ hw-volume ]}, # JayBird BlueBuds X
+ { address = "~^d4:9c:28:", no-features = [ hw-volume ]}, # JayBird BlueBuds X
+ { address = "~^00:18:6b:", no-features = [ hw-volume ]}, # LG Tone HBS-730
+ { address = "~^b8:ad:3e:", no-features = [ hw-volume ]}, # LG Tone HBS-730
+ { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # LG Tone HV-800
+ { address = "~^00:24:1c:", no-features = [ hw-volume ]}, # Motorola Roadster
+ { address = "~^00:11:b1:", no-features = [ hw-volume ]}, # Mpow Cheetah
+ { address = "~^a4:15:66:", no-features = [ hw-volume ]}, # SOL REPUBLIC Tracks Air
+ { address = "~^00:14:f1:", no-features = [ hw-volume ]}, # Swage Rokitboost HS
+ { address = "~^00:26:7e:", no-features = [ hw-volume ]}, # VW Car Kit
+ { address = "~^90:03:b7:", no-features = [ hw-volume ]}, # VW Car Kit
+
+ # All features are enabled by default; it's simpler to block non-working devices one by one.
+]
+
+bluez5.features.adapter = [
+ # properties:
+ # - address ("ff:ff:ff:ff:ff:ff")
+ # - bus-type ("usb", "other")
+ # - vendor-id ("usb:ffff")
+ # - product-id ("ffff")
+
+ # Realtek Semiconductor Corp.
+ { bus-type = "usb", vendor-id = "usb:0bda" },
+
+ # Generic USB adapters
+ { bus-type = "usb", no-features = [ msbc-alt1-rtl ] },
+
+ # Other adapters
+ { no-features = [ msbc-alt1-rtl ] },
+]
+
+bluez5.features.kernel = [
+ # properties (as in uname):
+ # - sysname
+ # - release
+ # - version
+
+ # See https://lore.kernel.org/linux-bluetooth/20201210012003.133000-1-tpiepho@gmail.com/
+ # https://lore.kernel.org/linux-bluetooth/b86543908684cc6cd9afaf4de10fac7af1a49665.camel@iki.fi/
+ { sysname = "Linux", release = "~^[0-4]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { sysname = "Linux", release = "~^5\\.[1-7]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { sysname = "Linux", release = "~^5\\.(8|9)\\.", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.10\\.(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|51|52|53|54|55|56|57|58|59|60|61)($|[^0-9])", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.12\\.(18|19)($|[^0-9])", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.13\\.(3|4|5|6|7|8|9|10|11|12|13)($|[^0-9])", no-features = [ msbc-alt1 ] },
+
+ { no-features = [] },
+]
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
new file mode 100644
index 0000000..4034f99
--- /dev/null
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -0,0 +1,5182 @@
+/* Spa V4l2 dbus
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+
+#include <bluetooth/bluetooth.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include "config.h"
+#include "codec-loader.h"
+#include "player.h"
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+enum backend_selection {
+ BACKEND_NONE = -2,
+ BACKEND_ANY = -1,
+ BACKEND_HSPHFPD = 0,
+ BACKEND_OFONO = 1,
+ BACKEND_NATIVE = 2,
+ BACKEND_NUM,
+};
+
+/*
+ * Rate limit for BlueZ SetConfiguration calls.
+ *
+ * Too rapid calls to BlueZ API may cause A2DP profile to disappear, as the
+ * internal BlueZ/connection state gets confused. Use some reasonable minimum
+ * interval.
+ *
+ * AVDTP v1.3 Sec. 6.13 mentions 3 seconds as a reasonable timeout in one case
+ * (ACP connection reset timeout, if no INT response). The case here is
+ * different, but we assume a similar value is fine here.
+ */
+#define BLUEZ_ACTION_RATE_MSEC 3000
+
+#define CODEC_SWITCH_RETRIES 1
+
+
+struct spa_bt_monitor {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_plugin_loader *plugin_loader;
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *dbus_connection;
+ DBusConnection *conn;
+
+ struct spa_hook_list hooks;
+
+ uint32_t id;
+
+ const struct media_codec * const * media_codecs;
+
+ /*
+ * Lists of BlueZ objects, kept up-to-date by following DBus events
+ * initiated by BlueZ. Object lifetime is also determined by that.
+ */
+ struct spa_list adapter_list;
+ struct spa_list device_list;
+ struct spa_list remote_endpoint_list;
+ struct spa_list transport_list;
+
+ unsigned int filters_added:1;
+ unsigned int objects_listed:1;
+ DBusPendingCall *get_managed_objects_call;
+
+ struct spa_bt_backend *backend;
+ struct spa_bt_backend *backends[BACKEND_NUM];
+ enum backend_selection backend_selection;
+
+ struct spa_dict enabled_codecs;
+
+ unsigned int connection_info_supported:1;
+ unsigned int dummy_avrcp_player:1;
+
+ struct spa_bt_quirks *quirks;
+
+#define MAX_SETTINGS 128
+ struct spa_dict_item global_setting_items[MAX_SETTINGS];
+ struct spa_dict global_settings;
+
+ /* A reference audio info for A2DP codec configuration. */
+ struct media_codec_audio_info default_audio_info;
+
+ bool le_audio_supported;
+};
+
+/* Stream endpoints owned by BlueZ for each device */
+struct spa_bt_remote_endpoint {
+ struct spa_list link;
+ struct spa_list device_link;
+ struct spa_bt_monitor *monitor;
+ char *path;
+
+ char *uuid;
+ unsigned int codec;
+ struct spa_bt_device *device;
+ uint8_t *capabilities;
+ int capabilities_len;
+ bool delay_reporting;
+ bool acceptor;
+};
+
+/*
+ * Codec switching tries various codec/remote endpoint combinations
+ * in order, until an acceptable one is found. This triggers BlueZ
+ * to initiate DBus calls that result to the creation of a transport
+ * with the desired capabilities.
+ * The codec switch struct tracks candidates still to be tried.
+ */
+struct spa_bt_media_codec_switch {
+ struct spa_bt_device *device;
+ struct spa_list device_link;
+
+ /*
+ * Codec switch may be waiting for either DBus reply from BlueZ
+ * or a timeout (but not both).
+ */
+ struct spa_source timer;
+ DBusPendingCall *pending;
+
+ uint32_t profile;
+
+ /*
+ * Called asynchronously, so endpoint paths instead of pointers (which may be
+ * invalidated in the meantime).
+ */
+ const struct media_codec **codecs;
+ char **paths;
+
+ const struct media_codec **codec_iter; /**< outer iterator over codecs */
+ char **path_iter; /**< inner iterator over endpoint paths */
+
+ uint16_t retries;
+ size_t num_paths;
+};
+
+#define DEFAULT_RECONNECT_PROFILES SPA_BT_PROFILE_NULL
+#define DEFAULT_HW_VOLUME_PROFILES (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY | SPA_BT_PROFILE_HEADSET_HEAD_UNIT | \
+ SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK)
+
+#define BT_DEVICE_DISCONNECTED 0
+#define BT_DEVICE_CONNECTED 1
+#define BT_DEVICE_INIT -1
+
+/*
+ * SCO socket connect may fail with ECONNABORTED if it is done too soon after
+ * previous close. To avoid this in cases where nodes are toggled between
+ * stopped/started rapidly, postpone release until the transport has remained
+ * unused for a time. Since this appears common to multiple SCO backends, we do
+ * it for all SCO backends here.
+ */
+#define SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC 1000
+#define SPA_BT_TRANSPORT_IS_SCO(transport) (transport->backend != NULL)
+
+#define TRANSPORT_VOLUME_TIMEOUT_MSEC 200
+
+static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport);
+
+static int device_start_timer(struct spa_bt_device *device);
+static int device_stop_timer(struct spa_bt_device *device);
+
+// Working with BlueZ Battery Provider.
+// Developed using https://github.com/dgreid/adhd/commit/655b58f as an example of DBus calls.
+
+// Name of battery, formatted as /org/freedesktop/pipewire/battery/org/bluez/hciX/dev_XX_XX_XX_XX_XX_XX
+static char *battery_get_name(const char *device_path)
+{
+ char *path = malloc(strlen(PIPEWIRE_BATTERY_PROVIDER) + strlen(device_path) + 1);
+ sprintf(path, PIPEWIRE_BATTERY_PROVIDER "%s", device_path);
+ return path;
+}
+
+// Unregister virtual battery of device
+static void battery_remove(struct spa_bt_device *device) {
+ DBusMessageIter i, entry;
+ DBusMessage *m;
+ const char *interface;
+
+ if (device->battery_pending_call) {
+ spa_log_debug(device->monitor->log, "Cancelling and freeing pending battery provider register call");
+ dbus_pending_call_cancel(device->battery_pending_call);
+ dbus_pending_call_unref(device->battery_pending_call);
+ device->battery_pending_call = NULL;
+ }
+
+ if (!device->adapter || !device->adapter->has_battery_provider || !device->has_battery)
+ return;
+
+ spa_log_debug(device->monitor->log, "Removing virtual battery: %s", device->battery_path);
+
+ m = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER,
+ DBUS_INTERFACE_OBJECT_MANAGER,
+ DBUS_SIGNAL_INTERFACES_REMOVED);
+
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH,
+ &device->battery_path);
+ dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_STRING_AS_STRING, &entry);
+ interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
+ &interface);
+ dbus_message_iter_close_container(&i, &entry);
+
+ if (!dbus_connection_send(device->monitor->conn, m, NULL)) {
+ spa_log_error(device->monitor->log, "sending " DBUS_SIGNAL_INTERFACES_REMOVED " failed");
+ }
+
+ dbus_message_unref(m);
+
+ device->has_battery = false;
+}
+
+// Create properties for Battery Provider request
+static void battery_write_properties(DBusMessageIter *iter, struct spa_bt_device *device)
+{
+ DBusMessageIter dict, entry, variant;
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL,
+ &entry);
+ const char *prop_percentage = "Percentage";
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_percentage);
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+ DBUS_TYPE_BYTE_AS_STRING, &variant);
+ dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &device->battery);
+ dbus_message_iter_close_container(&entry, &variant);
+ dbus_message_iter_close_container(&dict, &entry);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ const char *prop_device = "Device";
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_device);
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+ DBUS_TYPE_OBJECT_PATH_AS_STRING,
+ &variant);
+ dbus_message_iter_append_basic(&variant, DBUS_TYPE_OBJECT_PATH, &device->path);
+ dbus_message_iter_close_container(&entry, &variant);
+ dbus_message_iter_close_container(&dict, &entry);
+
+ dbus_message_iter_close_container(iter, &dict);
+}
+
+// Send current percentage to BlueZ
+static void battery_update(struct spa_bt_device *device)
+{
+ spa_log_debug(device->monitor->log, "updating battery: %s", device->battery_path);
+
+ DBusMessage *msg;
+ DBusMessageIter iter;
+
+ msg = dbus_message_new_signal(device->battery_path,
+ DBUS_INTERFACE_PROPERTIES,
+ DBUS_SIGNAL_PROPERTIES_CHANGED);
+
+ dbus_message_iter_init_append(msg, &iter);
+ const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING,
+ &interface);
+
+ battery_write_properties(&iter, device);
+
+ if (!dbus_connection_send(device->monitor->conn, msg, NULL))
+ spa_log_error(device->monitor->log, "Error updating battery");
+
+ dbus_message_unref(msg);
+}
+
+// Create new virtual battery with value stored in current device object
+static void battery_create(struct spa_bt_device *device) {
+ DBusMessage *msg;
+ DBusMessageIter iter, entry, dict;
+ msg = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER,
+ DBUS_INTERFACE_OBJECT_MANAGER,
+ DBUS_SIGNAL_INTERFACES_ADDED);
+
+ dbus_message_iter_init_append(msg, &iter);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
+ &device->battery_path);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict);
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
+ &interface);
+
+ battery_write_properties(&entry, device);
+
+ dbus_message_iter_close_container(&dict, &entry);
+ dbus_message_iter_close_container(&iter, &dict);
+
+ if (!dbus_connection_send(device->monitor->conn, msg, NULL)) {
+ spa_log_error(device->monitor->log, "Failed to create virtual battery for %s", device->address);
+ return;
+ }
+
+ dbus_message_unref(msg);
+
+ spa_log_debug(device->monitor->log, "Created virtual battery for %s", device->address);
+ device->has_battery = true;
+}
+
+static void on_battery_provider_registered(DBusPendingCall *pending_call,
+ void *data)
+{
+ DBusMessage *reply;
+ struct spa_bt_device *device = data;
+
+ reply = dbus_pending_call_steal_reply(pending_call);
+ dbus_pending_call_unref(pending_call);
+
+ device->battery_pending_call = NULL;
+
+ if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider. Error: %s", dbus_message_get_error_name(reply));
+ spa_log_error(device->monitor->log, "BlueZ Battery Provider is not available, won't retry to register it. Make sure you are running BlueZ 5.56+ with experimental features to use Battery Provider.");
+ device->adapter->battery_provider_unavailable = true;
+ dbus_message_unref(reply);
+ return;
+ }
+
+ spa_log_debug(device->monitor->log, "Registered Battery Provider");
+
+ device->adapter->has_battery_provider = true;
+
+ if (!device->has_battery)
+ battery_create(device);
+
+ dbus_message_unref(reply);
+}
+
+// Register Battery Provider for adapter and then create virtual battery for device
+static void register_battery_provider(struct spa_bt_device *device)
+{
+ DBusMessage *method_call;
+ DBusMessageIter message_iter;
+
+ if (device->battery_pending_call) {
+ spa_log_debug(device->monitor->log, "Already registering battery provider");
+ return;
+ }
+
+ method_call = dbus_message_new_method_call(
+ BLUEZ_SERVICE, device->adapter_path,
+ BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER,
+ "RegisterBatteryProvider");
+
+ if (!method_call) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ dbus_message_iter_init_append(method_call, &message_iter);
+ const char *object_path = PIPEWIRE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_OBJECT_PATH,
+ &object_path);
+
+ if (!dbus_connection_send_with_reply(device->monitor->conn, method_call, &device->battery_pending_call,
+ DBUS_TIMEOUT_USE_DEFAULT)) {
+ dbus_message_unref(method_call);
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ dbus_message_unref(method_call);
+
+ if (!device->battery_pending_call) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ if (!dbus_pending_call_set_notify(
+ device->battery_pending_call, on_battery_provider_registered,
+ device, NULL)) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ dbus_pending_call_cancel(device->battery_pending_call);
+ dbus_pending_call_unref(device->battery_pending_call);
+ device->battery_pending_call = NULL;
+ }
+}
+
+static int media_codec_to_endpoint(const struct media_codec *codec,
+ enum spa_bt_media_direction direction,
+ char** object_path)
+{
+ const char * endpoint;
+
+ if (direction == SPA_BT_MEDIA_SOURCE)
+ endpoint = codec->bap ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT;
+ else
+ endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT;
+
+ *object_path = spa_aprintf("%s/%s", endpoint,
+ codec->endpoint_name ? codec->endpoint_name : codec->name);
+ if (*object_path == NULL)
+ return -errno;
+ return 0;
+}
+
+static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, const struct media_codec *preferred)
+{
+ const char *ep_name;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const struct media_codec *found = NULL;
+ int i;
+
+ if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/");
+ *sink = true;
+ } else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/");
+ *sink = false;
+ } else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(BAP_SOURCE_ENDPOINT "/");
+ *sink = false;
+ } else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(BAP_SINK_ENDPOINT "/");
+ *sink = true;
+ } else {
+ *sink = true;
+ return NULL;
+ }
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+ const char *codec_ep_name =
+ codec->endpoint_name ? codec->endpoint_name : codec->name;
+
+ if (!spa_streq(ep_name, codec_ep_name))
+ continue;
+ if ((*sink && !codec->decode) || (!*sink && !codec->encode))
+ continue;
+
+ /* Same endpoint may be shared with multiple codec objects,
+ * which may e.g. correspond to different encoder settings.
+ * Look up which one we selected.
+ */
+ if ((preferred && codec == preferred) || found == NULL)
+ found = codec;
+ }
+ return found;
+}
+
+static int media_endpoint_to_profile(const char *endpoint)
+{
+
+ if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/"))
+ return SPA_BT_PROFILE_A2DP_SOURCE;
+ else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/"))
+ return SPA_BT_PROFILE_A2DP_SINK;
+ else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/"))
+ return SPA_BT_PROFILE_BAP_SOURCE;
+ else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/"))
+ return SPA_BT_PROFILE_BAP_SINK;
+ else
+ return SPA_BT_PROFILE_NULL;
+}
+
+static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec)
+{
+ return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL;
+}
+
+static bool codec_has_direction(const struct media_codec *codec, enum spa_bt_media_direction direction)
+{
+ switch (direction) {
+ case SPA_BT_MEDIA_SOURCE:
+ return codec->encode;
+ case SPA_BT_MEDIA_SINK:
+ return codec->decode;
+ default:
+ spa_assert_not_reached();
+ }
+}
+
+static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ /* Codecs with fill_caps == NULL share endpoint with another codec,
+ * and don't have their own endpoint
+ */
+ return is_media_codec_enabled(monitor, codec) &&
+ codec_has_direction(codec, direction) &&
+ codec->fill_caps;
+}
+
+static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path;
+ uint8_t *cap, config[A2DP_MAX_CAPS_SIZE];
+ uint8_t *pconf = (uint8_t *) config;
+ DBusMessage *r;
+ DBusError err;
+ int size, res;
+ const struct media_codec *codec;
+ bool sink;
+
+ dbus_error_init(&err);
+
+ path = dbus_message_get_path(m);
+
+ if (!dbus_message_get_args(m, &err, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_BYTE, &cap, &size, DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Endpoint SelectConfiguration(): %s", err.message);
+ dbus_error_free(&err);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_log_info(monitor->log, "%p: %s select conf %d", monitor, path, size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, cap, (size_t)size);
+
+ /* For codecs sharing the same endpoint, BlueZ-initiated connections
+ * always pick the default one. The session manager will
+ * switch the codec to a saved value after connection, so this generally
+ * does not matter.
+ */
+ codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
+ if (codec != NULL)
+ /* FIXME: We can't determine which device the SelectConfiguration()
+ * call is associated with, therefore device settings are not passed.
+ * This causes inconsistency with SelectConfiguration() triggered
+ * by codec switching.
+ */
+ res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info,
+ &monitor->global_settings, config);
+ else
+ res = -ENOTSUP;
+
+ if (res < 0 || res != size) {
+ spa_log_error(monitor->log, "can't select config: %d (%s)",
+ res, spa_strerror(res));
+ if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments",
+ "Unable to select configuration")) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ goto exit_send;
+ }
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, pconf, (size_t)size);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_BYTE, &pconf, size, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+exit_send:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant);
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
+static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path);
+
+static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path;
+ DBusMessageIter args, props, iter;
+ DBusMessage *r = NULL;
+ int res;
+ const struct media_codec *codec;
+ bool sink;
+ const char *err_msg = "Unknown error";
+
+ const char *endpoint_path = NULL;
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ int caps_size = 0;
+ int conf_size;
+ DBusMessageIter dict;
+ struct bap_endpoint_qos endpoint_qos;
+
+ spa_zero(endpoint_qos);
+
+ if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "a{sv}")) {
+ spa_log_error(monitor->log, "Invalid signature for method SelectProperties()");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_recurse(&args, &props);
+ if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY)
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ path = dbus_message_get_path(m);
+
+ /* TODO: for codecs with shared endpoint, this currently always picks the default
+ * one. However, currently we don't have BAP codecs with shared endpoint, so
+ * this does not matter, but in case they are needed later we should pick the
+ * right one here.
+ */
+ codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+ if (!codec) {
+ spa_log_error(monitor->log, "Unsupported codec");
+ err_msg = "Unsupported codec";
+ goto error;
+ }
+
+ /* Parse transport properties */
+ while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) {
+ const char *key;
+ DBusMessageIter value, entry;
+ int type;
+
+ dbus_message_iter_recurse(&props, &entry);
+ dbus_message_iter_get_basic(&entry, &key);
+
+ dbus_message_iter_next(&entry);
+ dbus_message_iter_recurse(&entry, &value);
+
+ type = dbus_message_iter_get_arg_type(&value);
+
+ if (spa_streq(key, "Capabilities")) {
+ DBusMessageIter array;
+ uint8_t *buf;
+
+ if (type != DBUS_TYPE_ARRAY) {
+ spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_recurse(&value, &array);
+ type = dbus_message_iter_get_arg_type(&array);
+ if (type != DBUS_TYPE_BYTE) {
+ spa_log_error(monitor->log, "%s is an array of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_get_fixed_array(&array, &buf, &caps_size);
+ if (caps_size > (int)sizeof(caps)) {
+ spa_log_error(monitor->log, "%s size:%d too large", key, (int)caps_size);
+ goto error_invalid;
+ }
+ memcpy(caps, buf, caps_size);
+
+ spa_log_info(monitor->log, "%p: %s %s size:%d", monitor, path, key, caps_size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', caps, (size_t)caps_size);
+ } else if (spa_streq(key, "Endpoint")) {
+ if (type != DBUS_TYPE_OBJECT_PATH) {
+ spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_get_basic(&value, &endpoint_path);
+
+ spa_log_info(monitor->log, "%p: %s %s %s", monitor, path, key, endpoint_path);
+ } else if (type == DBUS_TYPE_BYTE) {
+ uint8_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "Framing"))
+ endpoint_qos.framing = v;
+ else if (spa_streq(key, "PHY"))
+ endpoint_qos.phy = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else if (type == DBUS_TYPE_UINT16) {
+ dbus_uint16_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "Latency"))
+ endpoint_qos.latency = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else if (type == DBUS_TYPE_UINT32) {
+ dbus_uint32_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "MinimumDelay"))
+ endpoint_qos.delay_min = v;
+ else if (spa_streq(key, "MaximumDelay"))
+ endpoint_qos.delay_max = v;
+ else if (spa_streq(key, "PreferredMinimumDelay"))
+ endpoint_qos.preferred_delay_min = v;
+ else if (spa_streq(key, "PreferredMaximumDelay"))
+ endpoint_qos.preferred_delay_max = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else {
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ }
+
+ dbus_message_iter_next(&props);
+ }
+
+ if (codec->bap) {
+ struct spa_bt_remote_endpoint *ep;
+
+ ep = remote_endpoint_find(monitor, endpoint_path);
+ if (!ep) {
+ spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path);
+ goto error_invalid;
+ }
+
+ /* Call of SelectProperties means that local device acts as an initiator
+ * and therefor remote endpoint is an acceptor
+ */
+ ep->acceptor = true;
+ }
+
+ /* TODO: determine which device the SelectConfiguration() call is associated
+ * with; it's known here based on the remote endpoint.
+ */
+ conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, NULL, config);
+ if (conf_size < 0) {
+ spa_log_error(monitor->log, "can't select config: %d (%s)",
+ conf_size, spa_strerror(conf_size));
+ goto error_invalid;
+ }
+ spa_log_info(monitor->log, "%p: selected conf %d", monitor, conf_size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', (uint8_t *)config, (size_t)conf_size);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_iter_init_append(r, &iter);
+
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+ DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+ DBUS_TYPE_STRING_AS_STRING
+ DBUS_TYPE_VARIANT_AS_STRING
+ DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
+ &dict);
+ append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, &config, conf_size);
+
+ if (codec->get_qos) {
+ struct bap_codec_qos qos;
+ dbus_bool_t framing;
+ const char *phy_str;
+
+ spa_zero(qos);
+
+ res = codec->get_qos(codec, config, conf_size, &endpoint_qos, &qos);
+ if (res < 0) {
+ spa_log_error(monitor->log, "can't select QOS config: %d (%s)",
+ res, spa_strerror(res));
+ goto error_invalid;
+ }
+
+ append_basic_variant_dict_entry(&dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval);
+ framing = (qos.framing ? TRUE : FALSE);
+ append_basic_variant_dict_entry(&dict, "Framing", DBUS_TYPE_BOOLEAN, "b", &framing);
+ if (qos.phy == 0x1)
+ phy_str = "1M";
+ else if (qos.phy == 0x2)
+ phy_str = "2M";
+ else
+ spa_assert_not_reached();
+ append_basic_variant_dict_entry(&dict, "PHY", DBUS_TYPE_STRING, "s", &phy_str);
+ append_basic_variant_dict_entry(&dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu);
+ append_basic_variant_dict_entry(&dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission);
+ append_basic_variant_dict_entry(&dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency);
+ append_basic_variant_dict_entry(&dict, "Delay", DBUS_TYPE_UINT32, "u", &qos.delay);
+ append_basic_variant_dict_entry(&dict, "TargetLatency", DBUS_TYPE_BYTE, "y", &qos.target_latency);
+ }
+
+ dbus_message_iter_close_container(&iter, &dict);
+
+ if (r) {
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+error_invalid:
+ err_msg = "Invalid property";
+ goto error;
+
+error:
+ if (r)
+ dbus_message_unref(r);
+ if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", err_msg)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL)) {
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_adapter *d;
+ spa_list_for_each(d, &monitor->adapter_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+static bool check_iter_signature(DBusMessageIter *it, const char *sig)
+{
+ char *v;
+ bool res;
+ v = dbus_message_iter_get_signature(it);
+ res = spa_streq(v, sig);
+ dbus_free(v);
+ return res;
+}
+
+static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vendor,
+ uint16_t *product, uint16_t *version)
+{
+ char *pos;
+ unsigned int src, i, j, k;
+
+ if (spa_strstartswith(modalias, "bluetooth:"))
+ src = SOURCE_ID_BLUETOOTH;
+ else if (spa_strstartswith(modalias, "usb:"))
+ src = SOURCE_ID_USB;
+ else
+ return -EINVAL;
+
+ pos = strchr(modalias, ':');
+ if (pos == NULL)
+ return -EINVAL;
+
+ if (sscanf(pos + 1, "v%04Xp%04Xd%04X", &i, &j, &k) != 3)
+ return -EINVAL;
+
+ /* Ignore BlueZ placeholder value */
+ if (src == SOURCE_ID_USB && i == 0x1d6b && j == 0x0246)
+ return -ENXIO;
+
+ *source = src;
+ *vendor = i;
+ *product = j;
+ *version = k;
+
+ return 0;
+}
+
+static int adapter_update_props(struct spa_bt_adapter *adapter,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%s", adapter, key, value);
+
+ if (spa_streq(key, "Alias")) {
+ free(adapter->alias);
+ adapter->alias = strdup(value);
+ }
+ else if (spa_streq(key, "Name")) {
+ free(adapter->name);
+ adapter->name = strdup(value);
+ }
+ else if (spa_streq(key, "Address")) {
+ free(adapter->address);
+ adapter->address = strdup(value);
+ }
+ else if (spa_streq(key, "Modalias")) {
+ int ret;
+ ret = parse_modalias(value, &adapter->source_id, &adapter->vendor_id,
+ &adapter->product_id, &adapter->version_id);
+ if (ret < 0)
+ spa_log_debug(monitor->log, "adapter %p: %s=%s ignored: %s",
+ adapter, key, value, spa_strerror(ret));
+ }
+ }
+ else if (type == DBUS_TYPE_UINT32) {
+ uint32_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value);
+
+ if (spa_streq(key, "Class"))
+ adapter->bluetooth_class = value;
+
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value);
+
+ if (spa_streq(key, "Powered")) {
+ adapter->powered = value;
+ }
+ }
+ else if (spa_streq(key, "UUIDs")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+ enum spa_bt_profile profile;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ profile = spa_bt_profile_from_uuid(uuid);
+
+ if (profile && (adapter->profiles & profile) == 0) {
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, uuid);
+ adapter->profiles |= profile;
+ } else if (strcasecmp(uuid, SPA_BT_UUID_PACS) == 0 &&
+ (adapter->profiles & SPA_BT_PROFILE_BAP_SINK) == 0) {
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SINK);
+ adapter->profiles |= SPA_BT_PROFILE_BAP_SINK;
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SOURCE);
+ adapter->profiles |= SPA_BT_PROFILE_BAP_SOURCE;
+ }
+ dbus_message_iter_next(&iter);
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "adapter %p: unhandled key %s", adapter, key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static void adapter_update_devices(struct spa_bt_adapter *adapter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ struct spa_bt_device *device;
+
+ /*
+ * Update devices when new adapter appears.
+ * Devices may appear on DBus before or after the adapter does.
+ */
+
+ spa_list_for_each(device, &monitor->device_list, link) {
+ if (device->adapter == NULL && spa_streq(device->adapter_path, adapter->path))
+ device->adapter = adapter;
+ }
+}
+
+static void adapter_register_player(struct spa_bt_adapter *adapter)
+{
+ if (adapter->player_registered || !adapter->monitor->dummy_avrcp_player)
+ return;
+
+ if (spa_bt_player_register(adapter->dummy_player, adapter->path) == 0)
+ adapter->player_registered = true;
+}
+
+static int adapter_init_bus_type(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d)
+{
+ char path[1024], buf[1024];
+ const char *str;
+ ssize_t res = -EINVAL;
+
+ d->bus_type = BUS_TYPE_OTHER;
+
+ str = strrchr(d->path, '/'); /* hciXX */
+ if (str == NULL)
+ return -ENOENT;
+
+ snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/subsystem", str);
+ if ((res = readlink(path, buf, sizeof(buf)-1)) < 0)
+ return -errno;
+ buf[res] = '\0';
+
+ str = strrchr(buf, '/');
+ if (str && spa_streq(str, "/usb"))
+ d->bus_type = BUS_TYPE_USB;
+ return 0;
+}
+
+static int adapter_init_modalias(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d)
+{
+ char path[1024];
+ FILE *f = NULL;
+ int vendor_id, product_id;
+ const char *str;
+ int res = -EINVAL;
+
+ /* Lookup vendor/product id for the device, if present */
+ str = strrchr(d->path, '/'); /* hciXX */
+ if (str == NULL)
+ goto fail;
+ snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/modalias", str);
+ if ((f = fopen(path, "rbe")) == NULL) {
+ res = -errno;
+ goto fail;
+ }
+ if (fscanf(f, "usb:v%04Xp%04X", &vendor_id, &product_id) != 2)
+ goto fail;
+ d->source_id = SOURCE_ID_USB;
+ d->vendor_id = vendor_id;
+ d->product_id = product_id;
+ fclose(f);
+
+ spa_log_debug(monitor->log, "adapter %p: usb vendor:%04x product:%04x",
+ d, vendor_id, product_id);
+ return 0;
+
+fail:
+ if (f)
+ fclose(f);
+ return res;
+}
+
+static struct spa_bt_adapter *adapter_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_adapter *d;
+
+ d = calloc(1, sizeof(struct spa_bt_adapter));
+ if (d == NULL)
+ return NULL;
+
+ d->dummy_player = spa_bt_player_new(monitor->conn, monitor->log);
+ if (d->dummy_player == NULL) {
+ free(d);
+ return NULL;
+ }
+
+ d->monitor = monitor;
+ d->path = strdup(path);
+
+ spa_list_prepend(&monitor->adapter_list, &d->link);
+
+ adapter_init_bus_type(monitor, d);
+ adapter_init_modalias(monitor, d);
+
+ return d;
+}
+
+static void device_free(struct spa_bt_device *device);
+
+static void adapter_free(struct spa_bt_adapter *adapter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ struct spa_bt_device *d, *td;
+
+ spa_log_debug(monitor->log, "%p", adapter);
+
+ /* Devices should be destroyed before their assigned adapter */
+ spa_list_for_each_safe(d, td, &monitor->device_list, link)
+ if (d->adapter == adapter)
+ device_free(d);
+
+ spa_bt_player_destroy(adapter->dummy_player);
+
+ spa_list_remove(&adapter->link);
+ free(adapter->alias);
+ free(adapter->name);
+ free(adapter->address);
+ free(adapter->path);
+ free(adapter);
+}
+
+static uint32_t adapter_connectable_profiles(struct spa_bt_adapter *adapter)
+{
+ const uint32_t profiles = adapter->profiles;
+ uint32_t mask = 0;
+
+ if (profiles & SPA_BT_PROFILE_A2DP_SINK)
+ mask |= SPA_BT_PROFILE_A2DP_SOURCE;
+ if (profiles & SPA_BT_PROFILE_A2DP_SOURCE)
+ mask |= SPA_BT_PROFILE_A2DP_SINK;
+
+ if (profiles & SPA_BT_PROFILE_BAP_SINK)
+ mask |= SPA_BT_PROFILE_BAP_SOURCE;
+ if (profiles & SPA_BT_PROFILE_BAP_SOURCE)
+ mask |= SPA_BT_PROFILE_BAP_SINK;
+
+ if (profiles & SPA_BT_PROFILE_HSP_AG)
+ mask |= SPA_BT_PROFILE_HSP_HS;
+ if (profiles & SPA_BT_PROFILE_HSP_HS)
+ mask |= SPA_BT_PROFILE_HSP_AG;
+
+ if (profiles & SPA_BT_PROFILE_HFP_AG)
+ mask |= SPA_BT_PROFILE_HFP_HF;
+ if (profiles & SPA_BT_PROFILE_HFP_HF)
+ mask |= SPA_BT_PROFILE_HFP_AG;
+
+ return mask;
+}
+
+struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_device *d;
+ spa_list_for_each(d, &monitor->device_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address)
+{
+ struct spa_bt_device *d;
+ spa_list_for_each(d, &monitor->device_list, link)
+ if (spa_streq(d->address, remote_address) && spa_streq(d->adapter->address, local_address))
+ return d;
+ return NULL;
+}
+
+void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device)
+{
+ struct timespec ts;
+ spa_system_clock_gettime(device->monitor->main_system, CLOCK_MONOTONIC, &ts);
+ device->last_bluez_action_time = SPA_TIMESPEC_TO_NSEC(&ts);
+}
+
+static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_device *d;
+
+ d = calloc(1, sizeof(struct spa_bt_device));
+ if (d == NULL)
+ return NULL;
+
+ d->id = monitor->id++;
+ d->monitor = monitor;
+ d->path = strdup(path);
+ d->battery_path = battery_get_name(d->path);
+ d->reconnect_profiles = DEFAULT_RECONNECT_PROFILES;
+ d->hw_volume_profiles = DEFAULT_HW_VOLUME_PROFILES;
+
+ spa_list_init(&d->remote_endpoint_list);
+ spa_list_init(&d->transport_list);
+ spa_list_init(&d->codec_switch_list);
+
+ spa_hook_list_init(&d->listener_list);
+
+ spa_list_prepend(&monitor->device_list, &d->link);
+
+ spa_bt_device_update_last_bluez_action_time(d);
+
+ return d;
+}
+
+static int device_stop_timer(struct spa_bt_device *device);
+
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw);
+
+static void device_clear_sub(struct spa_bt_device *device)
+{
+ battery_remove(device);
+ spa_bt_device_release_transports(device);
+}
+
+static void device_free(struct spa_bt_device *device)
+{
+ struct spa_bt_remote_endpoint *ep, *tep;
+ struct spa_bt_media_codec_switch *sw;
+ struct spa_bt_transport *t, *tt;
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ spa_log_debug(monitor->log, "%p", device);
+
+ spa_bt_device_emit_destroy(device);
+
+ device_clear_sub(device);
+ device_stop_timer(device);
+
+ if (device->added) {
+ spa_device_emit_object_info(&monitor->hooks, device->id, NULL);
+ }
+
+ spa_list_for_each_safe(ep, tep, &device->remote_endpoint_list, device_link) {
+ if (ep->device == device) {
+ spa_list_remove(&ep->device_link);
+ ep->device = NULL;
+ }
+ }
+
+ spa_list_for_each_safe(t, tt, &device->transport_list, device_link) {
+ if (t->device == device) {
+ spa_list_remove(&t->device_link);
+ t->device = NULL;
+ }
+ }
+
+ spa_list_consume(sw, &device->codec_switch_list, device_link)
+ media_codec_switch_free(sw);
+
+ spa_list_remove(&device->link);
+ free(device->path);
+ free(device->alias);
+ free(device->address);
+ free(device->adapter_path);
+ free(device->battery_path);
+ free(device->name);
+ free(device->icon);
+ free(device);
+}
+
+int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id, uint16_t product_id,
+ char *vendor_str, int vendor_str_size, char *product_str, int product_str_size)
+{
+ char *source_str;
+
+ switch (source_id) {
+ case SOURCE_ID_USB:
+ source_str = "usb";
+ break;
+ case SOURCE_ID_BLUETOOTH:
+ source_str = "bluetooth";
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ spa_scnprintf(vendor_str, vendor_str_size, "%s:%04x", source_str, (unsigned int)vendor_id);
+ spa_scnprintf(product_str, product_str_size, "%04x", (unsigned int)product_id);
+ return 0;
+}
+
+static void emit_device_info(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, bool with_connection)
+{
+ struct spa_device_object_info info;
+ char dev[32], name[128], class[16], vendor_id[64], product_id[64], product_id_tot[67];
+ struct spa_dict_item items[23];
+ uint32_t n_items = 0;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = SPA_NAME_API_BLUEZ5_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ snprintf(name, sizeof(name), "bluez_card.%s", device->address);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, name);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, device->alias);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ALIAS, device->name);
+ if (spa_bt_format_vendor_product_id(
+ device->source_id, device->vendor_id, device->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ snprintf(product_id_tot, sizeof(product_id_tot), "0x%s", product_id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, vendor_id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, product_id_tot);
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR,
+ spa_bt_form_factor_name(
+ spa_bt_form_factor_from_class(device->bluetooth_class)));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_STRING, device->address);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, device->icon);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, device->path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
+ snprintf(dev, sizeof(dev), "pointer:%p", device);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_DEVICE, dev);
+ snprintf(class, sizeof(class), "0x%06x", device->bluetooth_class);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
+
+ if (with_connection) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CONNECTION,
+ device->connected ? "connected": "disconnected");
+ }
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&monitor->hooks, device->id, &info);
+}
+
+static int device_connected_old(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, int connected)
+{
+
+ if (connected == BT_DEVICE_INIT)
+ return 0;
+
+ device->connected = connected;
+
+ if (device->connected) {
+ emit_device_info(monitor, device, false);
+ device->added = true;
+ } else {
+ if (!device->added)
+ return 0;
+
+ device_clear_sub(device);
+ spa_device_emit_object_info(&monitor->hooks, device->id, NULL);
+ device->added = false;
+ }
+
+ return 0;
+}
+
+enum {
+ BT_DEVICE_RECONNECT_INIT = 0,
+ BT_DEVICE_RECONNECT_PROFILE,
+ BT_DEVICE_RECONNECT_STOP
+};
+
+static int device_connected(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, int status)
+{
+ bool connected, init = (status == BT_DEVICE_INIT);
+
+ connected = init ? 0 : status;
+
+ if (!init) {
+ device->reconnect_state =
+ connected ? BT_DEVICE_RECONNECT_STOP
+ : BT_DEVICE_RECONNECT_PROFILE;
+ }
+
+ if ((device->connected_profiles != 0) ^ connected) {
+ spa_log_error(monitor->log,
+ "device %p: unexpected call, connected_profiles:%08x connected:%d",
+ device, device->connected_profiles, device->connected);
+ return -EINVAL;
+ }
+
+ if (!monitor->connection_info_supported)
+ return device_connected_old(monitor, device, status);
+
+ if (init) {
+ device->connected = connected;
+ } else {
+ if (!device->added || !(connected ^ device->connected))
+ return 0;
+
+ device->connected = connected;
+ spa_bt_device_emit_connected(device, device->connected);
+
+ if (!device->connected)
+ device_clear_sub(device);
+ }
+
+ emit_device_info(monitor, device, true);
+ device->added = true;
+
+ return 0;
+}
+
+/*
+ * Add profile to device based on bluez actions
+ * (update property UUIDs, trigger profile handlers),
+ * in case UUIDs is empty on signal InterfaceAdded for
+ * org.bluez.Device1. And emit device info if there is
+ * at least 1 profile on device. This should be called
+ * before any device setting accessing.
+ */
+int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ if (profile && (device->profiles & profile) == 0) {
+ spa_log_info(monitor->log, "device %p: add new profile %08x", device, profile);
+ device->profiles |= profile;
+ }
+
+ if (!device->added && device->profiles) {
+ device_connected(monitor, device, BT_DEVICE_INIT);
+ if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT)
+ device_start_timer(device);
+ }
+
+ return 0;
+}
+
+
+static int device_try_connect_profile(struct spa_bt_device *device,
+ const char *profile_uuid)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ DBusMessage *m;
+
+ spa_log_info(monitor->log, "device %p %s: profile %s not connected; try ConnectProfile()",
+ device, device->path, profile_uuid);
+
+ /* Call org.bluez.Device1.ConnectProfile() on device, ignoring result */
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ device->path,
+ BLUEZ_DEVICE_INTERFACE,
+ "ConnectProfile");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &profile_uuid, DBUS_TYPE_INVALID);
+ if (!dbus_connection_send(monitor->conn, m, NULL)) {
+ dbus_message_unref(m);
+ return -EIO;
+ }
+ dbus_message_unref(m);
+
+ return 0;
+}
+
+static int reconnect_device_profiles(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct spa_bt_device *d;
+ uint32_t reconnect = device->profiles
+ & device->reconnect_profiles
+ & (device->connected_profiles ^ device->profiles);
+
+ /* Don't try to connect to same device via multiple adapters */
+ spa_list_for_each(d, &monitor->device_list, link) {
+ if (d != device && spa_streq(d->address, device->address)) {
+ if (d->paired && d->trusted && !d->blocked &&
+ d->reconnect_state == BT_DEVICE_RECONNECT_STOP)
+ reconnect &= ~d->reconnect_profiles;
+ if (d->connected_profiles)
+ reconnect = 0;
+ }
+ }
+
+ /* Connect only profiles the adapter has a counterpart for */
+ if (device->adapter)
+ reconnect &= adapter_connectable_profiles(device->adapter);
+
+ if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) {
+ if (reconnect & SPA_BT_PROFILE_HFP_HF) {
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_HS);
+ } else if (reconnect & SPA_BT_PROFILE_HSP_HS) {
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_HF);
+ }
+ } else
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
+
+ if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) {
+ if (reconnect & SPA_BT_PROFILE_HFP_AG)
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_AG);
+ else if (reconnect & SPA_BT_PROFILE_HSP_AG)
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_AG);
+ } else
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+
+ if (reconnect & SPA_BT_PROFILE_HFP_HF)
+ device_try_connect_profile(device, SPA_BT_UUID_HFP_HF);
+ if (reconnect & SPA_BT_PROFILE_HSP_HS)
+ device_try_connect_profile(device, SPA_BT_UUID_HSP_HS);
+ if (reconnect & SPA_BT_PROFILE_HFP_AG)
+ device_try_connect_profile(device, SPA_BT_UUID_HFP_AG);
+ if (reconnect & SPA_BT_PROFILE_HSP_AG)
+ device_try_connect_profile(device, SPA_BT_UUID_HSP_AG);
+ if (reconnect & SPA_BT_PROFILE_A2DP_SINK)
+ device_try_connect_profile(device, SPA_BT_UUID_A2DP_SINK);
+ if (reconnect & SPA_BT_PROFILE_A2DP_SOURCE)
+ device_try_connect_profile(device, SPA_BT_UUID_A2DP_SOURCE);
+ if (reconnect & SPA_BT_PROFILE_BAP_SINK)
+ device_try_connect_profile(device, SPA_BT_UUID_BAP_SINK);
+ if (reconnect & SPA_BT_PROFILE_BAP_SOURCE)
+ device_try_connect_profile(device, SPA_BT_UUID_BAP_SOURCE);
+
+ return reconnect;
+}
+
+#define DEVICE_RECONNECT_TIMEOUT_SEC 2
+#define DEVICE_PROFILE_TIMEOUT_SEC 6
+
+static void device_timer_event(struct spa_source *source)
+{
+ struct spa_bt_device *device = source->data;
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_log_debug(monitor->log, "device %p: timeout %08x %08x",
+ device, device->profiles, device->connected_profiles);
+ device_stop_timer(device);
+ if (BT_DEVICE_RECONNECT_STOP != device->reconnect_state) {
+ device->reconnect_state = BT_DEVICE_RECONNECT_STOP;
+ if (device->paired
+ && device->trusted
+ && !device->blocked
+ && device->reconnect_profiles != 0
+ && reconnect_device_profiles(device))
+ {
+ device_start_timer(device);
+ return;
+ }
+ }
+ if (device->connected_profiles)
+ device_connected(device->monitor, device, BT_DEVICE_CONNECTED);
+}
+
+static int device_start_timer(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct itimerspec ts;
+
+ spa_log_debug(monitor->log, "device %p: start timer", device);
+ if (device->timer.data == NULL) {
+ device->timer.data = device;
+ device->timer.func = device_timer_event;
+ device->timer.fd = spa_system_timerfd_create(monitor->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ device->timer.mask = SPA_IO_IN;
+ device->timer.rmask = 0;
+ spa_loop_add_source(monitor->main_loop, &device->timer);
+ }
+ ts.it_value.tv_sec = device->reconnect_state == BT_DEVICE_RECONNECT_STOP
+ ? DEVICE_PROFILE_TIMEOUT_SEC
+ : DEVICE_RECONNECT_TIMEOUT_SEC;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int device_stop_timer(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct itimerspec ts;
+
+ if (device->timer.data == NULL)
+ return 0;
+
+ spa_log_debug(monitor->log, "device %p: stop timer", device);
+ spa_loop_remove_source(monitor->main_loop, &device->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, device->timer.fd);
+ device->timer.data = NULL;
+ return 0;
+}
+
+int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint32_t connected_profiles = device->connected_profiles;
+ uint32_t connectable_profiles =
+ device->adapter ? adapter_connectable_profiles(device->adapter) : 0;
+ uint32_t direction_masks[3] = {
+ SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT,
+ SPA_BT_PROFILE_MEDIA_SOURCE,
+ SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY,
+ };
+ bool direction_connected = false;
+ bool all_connected;
+ size_t i;
+
+ if (connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ connected_profiles |= SPA_BT_PROFILE_HEADSET_HEAD_UNIT;
+ if (connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)
+ connected_profiles |= SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ for (i = 0; i < SPA_N_ELEMENTS(direction_masks); ++i) {
+ uint32_t mask = direction_masks[i] & device->profiles & connectable_profiles;
+ if (mask && (connected_profiles & mask) == mask)
+ direction_connected = true;
+ }
+
+ all_connected = (device->profiles & connected_profiles) == device->profiles;
+
+ spa_log_debug(monitor->log, "device %p: profiles %08x %08x connectable:%08x added:%d all:%d dir:%d",
+ device, device->profiles, connected_profiles, connectable_profiles,
+ device->added, all_connected, direction_connected);
+
+ if (connected_profiles == 0 && spa_list_is_empty(&device->codec_switch_list)) {
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_DISCONNECTED);
+ } else if (force || direction_connected || all_connected) {
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_CONNECTED);
+ } else {
+ /* The initial reconnect event has not been triggered,
+ * the connecting is triggered by bluez. */
+ if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT)
+ device->reconnect_state = BT_DEVICE_RECONNECT_PROFILE;
+ device_start_timer(device);
+ }
+ return 0;
+}
+
+static void device_set_connected(struct spa_bt_device *device, int connected)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ if (device->connected && !connected)
+ device->connected_profiles = 0;
+
+ if (connected)
+ spa_bt_device_check_profiles(device, false);
+ else {
+ /* Stop codec switch on disconnect */
+ struct spa_bt_media_codec_switch *sw;
+ spa_list_consume(sw, &device->codec_switch_list, device_link)
+ media_codec_switch_free(sw);
+
+ if (device->reconnect_state != BT_DEVICE_RECONNECT_INIT)
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_DISCONNECTED);
+ }
+}
+
+int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile)
+{
+ uint32_t prev_connected = device->connected_profiles;
+ device->connected_profiles |= profile;
+ spa_bt_device_check_profiles(device, false);
+ if (device->connected_profiles != prev_connected)
+ spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+ return 0;
+}
+
+static void device_update_hw_volume_profiles(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint32_t bt_features = 0;
+
+ if (!monitor->quirks)
+ return;
+
+ if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) != 0)
+ return;
+
+ if (!(bt_features & SPA_BT_FEATURE_HW_VOLUME))
+ device->hw_volume_profiles = 0;
+
+ spa_log_debug(monitor->log, "hw-volume-profiles:%08x", (int)device->hw_volume_profiles);
+}
+
+static int device_update_props(struct spa_bt_device *device,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%s", device, key, value);
+
+ if (spa_streq(key, "Alias")) {
+ free(device->alias);
+ device->alias = strdup(value);
+ }
+ else if (spa_streq(key, "Name")) {
+ free(device->name);
+ device->name = strdup(value);
+ }
+ else if (spa_streq(key, "Address")) {
+ free(device->address);
+ device->address = strdup(value);
+ }
+ else if (spa_streq(key, "Adapter")) {
+ free(device->adapter_path);
+ device->adapter_path = strdup(value);
+
+ device->adapter = adapter_find(monitor, value);
+ if (device->adapter == NULL) {
+ spa_log_info(monitor->log, "unknown adapter %s", value);
+ }
+ }
+ else if (spa_streq(key, "Icon")) {
+ free(device->icon);
+ device->icon = strdup(value);
+ }
+ else if (spa_streq(key, "Modalias")) {
+ int ret;
+ ret = parse_modalias(value, &device->source_id, &device->vendor_id,
+ &device->product_id, &device->version_id);
+ if (ret < 0)
+ spa_log_debug(monitor->log, "device %p: %s=%s ignored: %s",
+ device, key, value, spa_strerror(ret));
+ }
+ }
+ else if (type == DBUS_TYPE_UINT32) {
+ uint32_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%08x", device, key, value);
+
+ if (spa_streq(key, "Class"))
+ device->bluetooth_class = value;
+ }
+ else if (type == DBUS_TYPE_UINT16) {
+ uint16_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "Appearance"))
+ device->appearance = value;
+ }
+ else if (type == DBUS_TYPE_INT16) {
+ int16_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "RSSI"))
+ device->RSSI = value;
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "Paired")) {
+ device->paired = value;
+ }
+ else if (spa_streq(key, "Trusted")) {
+ device->trusted = value;
+ }
+ else if (spa_streq(key, "Connected")) {
+ device_set_connected(device, value);
+ }
+ else if (spa_streq(key, "Blocked")) {
+ device->blocked = value;
+ }
+ else if (spa_streq(key, "ServicesResolved")) {
+ if (value)
+ spa_bt_device_check_profiles(device, false);
+ }
+ }
+ else if (spa_streq(key, "UUIDs")) {
+ DBusMessageIter iter;
+ uint32_t prev_profiles = device->profiles;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+ enum spa_bt_profile profile;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ profile = spa_bt_profile_from_uuid(uuid);
+
+ /* Only add A2DP/BAP profiles if HSP/HFP backed is none.
+ * This allows BT device to connect instantly instead of waiting for
+ * profile timeout, because all available profiles are connected.
+ */
+ if (monitor->backend_selection != BACKEND_NONE || (monitor->backend_selection == BACKEND_NONE &&
+ profile & (SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE))) {
+ if (profile && (device->profiles & profile) == 0) {
+ spa_log_debug(monitor->log, "device %p: add UUID=%s", device, uuid);
+ device->profiles |= profile;
+ }
+ }
+ dbus_message_iter_next(&iter);
+ }
+
+ if (device->profiles != prev_profiles)
+ spa_bt_device_emit_profiles_changed(
+ device, prev_profiles, device->connected_profiles);
+ }
+ else
+ spa_log_debug(monitor->log, "device %p: unhandled key %s type %d", device, key, type);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static bool device_props_ready(struct spa_bt_device *device)
+{
+ /*
+ * In some cases, BlueZ device props may be missing part of
+ * the information required when the interface first appears.
+ */
+ return device->adapter && device->address;
+}
+
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct spa_bt_remote_endpoint *ep;
+ const struct { enum spa_bluetooth_audio_codec codec; uint32_t mask; } quirks[] = {
+ { SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, SPA_BT_FEATURE_SBC_XQ },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_BT_FEATURE_FASTSTREAM },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_FASTSTREAM },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
+ };
+ size_t i;
+
+ if (!is_media_codec_enabled(device->monitor, codec))
+ return false;
+
+ if (!device->adapter->application_registered) {
+ /* Codec switching not supported: only plain SBC allowed */
+ return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc"));
+ }
+
+ /* Check codec quirks */
+ for (i = 0; i < SPA_N_ELEMENTS(quirks); ++i) {
+ uint32_t bt_features;
+
+ if (codec->id != quirks[i].codec)
+ continue;
+ if (monitor->quirks == NULL)
+ break;
+ if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) < 0)
+ break;
+ if (!(bt_features & quirks[i].mask))
+ return false;
+ }
+
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
+ const enum spa_bt_profile profile = spa_bt_profile_from_uuid(ep->uuid);
+ enum spa_bt_profile expected;
+
+ if (codec->bap)
+ expected = sink ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_BAP_SOURCE;
+ else
+ expected = sink ? SPA_BT_PROFILE_A2DP_SINK : SPA_BT_PROFILE_A2DP_SOURCE;
+
+ if (profile != expected)
+ continue;
+
+ if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len,
+ &ep->monitor->default_audio_info, &monitor->global_settings))
+ return true;
+ }
+
+ return false;
+}
+
+const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const struct media_codec **supported_codecs;
+ size_t i, j, size;
+
+ *count = 0;
+
+ size = 8;
+ supported_codecs = malloc(size * sizeof(const struct media_codec *));
+ if (supported_codecs == NULL)
+ return NULL;
+
+ j = 0;
+ for (i = 0; media_codecs[i] != NULL; ++i) {
+ if (spa_bt_device_supports_media_codec(device, media_codecs[i], sink)) {
+ supported_codecs[j] = media_codecs[i];
+ ++j;
+ }
+
+ if (j >= size) {
+ const struct media_codec **p;
+ size = size * 2;
+#ifdef HAVE_REALLOCARRRAY
+ p = reallocarray(supported_codecs, size, sizeof(const struct media_codec *));
+#else
+ p = realloc(supported_codecs, size * sizeof(const struct media_codec *));
+#endif
+ if (p == NULL) {
+ free(supported_codecs);
+ return NULL;
+ }
+ supported_codecs = p;
+ }
+ }
+
+ supported_codecs[j] = NULL;
+ *count = j;
+
+ return supported_codecs;
+}
+
+static struct spa_bt_remote_endpoint *device_remote_endpoint_find(struct spa_bt_device *device, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link)
+ if (spa_streq(ep->path, path))
+ return ep;
+ return NULL;
+}
+
+static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+ spa_list_for_each(ep, &monitor->remote_endpoint_list, link)
+ if (spa_streq(ep->path, path))
+ return ep;
+ return NULL;
+}
+
+static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_endpoint,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = remote_endpoint->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%s", remote_endpoint, key, value);
+
+ if (spa_streq(key, "UUID")) {
+ free(remote_endpoint->uuid);
+ remote_endpoint->uuid = strdup(value);
+ }
+ else if (spa_streq(key, "Device")) {
+ struct spa_bt_device *device;
+ device = spa_bt_device_find(monitor, value);
+ if (device == NULL)
+ goto next;
+ spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device);
+
+ if (remote_endpoint->device != device) {
+ if (remote_endpoint->device != NULL)
+ spa_list_remove(&remote_endpoint->device_link);
+ remote_endpoint->device = device;
+ if (device != NULL)
+ spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link);
+ }
+ }
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, value);
+
+ if (spa_streq(key, "DelayReporting")) {
+ remote_endpoint->delay_reporting = value;
+ }
+ }
+ else if (type == DBUS_TYPE_BYTE) {
+ uint8_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value);
+
+ if (spa_streq(key, "Codec")) {
+ remote_endpoint->codec = value;
+ }
+ }
+ else if (spa_streq(key, "Capabilities")) {
+ DBusMessageIter iter;
+ uint8_t *value;
+ int len;
+
+ if (!check_iter_signature(&it[1], "ay"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+ dbus_message_iter_get_fixed_array(&iter, &value, &len);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len);
+
+ free(remote_endpoint->capabilities);
+ remote_endpoint->capabilities_len = 0;
+
+ remote_endpoint->capabilities = malloc(len);
+ if (remote_endpoint->capabilities) {
+ memcpy(remote_endpoint->capabilities, value, len);
+ remote_endpoint->capabilities_len = len;
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static struct spa_bt_remote_endpoint *remote_endpoint_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+
+ ep = calloc(1, sizeof(struct spa_bt_remote_endpoint));
+ if (ep == NULL)
+ return NULL;
+
+ ep->monitor = monitor;
+ ep->path = strdup(path);
+
+ spa_list_prepend(&monitor->remote_endpoint_list, &ep->link);
+
+ return ep;
+}
+
+static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint)
+{
+ struct spa_bt_monitor *monitor = remote_endpoint->monitor;
+
+ spa_log_debug(monitor->log, "remote endpoint %p: free %s",
+ remote_endpoint, remote_endpoint->path);
+
+ if (remote_endpoint->device)
+ spa_list_remove(&remote_endpoint->device_link);
+
+ spa_list_remove(&remote_endpoint->link);
+ free(remote_endpoint->path);
+ free(remote_endpoint->uuid);
+ free(remote_endpoint->capabilities);
+ free(remote_endpoint);
+}
+
+struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_transport *t;
+ spa_list_for_each(t, &monitor->transport_list, link)
+ if (spa_streq(t->path, path))
+ return t;
+ return NULL;
+}
+
+struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor,
+ bool (*callback) (struct spa_bt_transport *t, const void *data),
+ const void *data)
+{
+ struct spa_bt_transport *t;
+
+ spa_list_for_each(t, &monitor->transport_list, link)
+ if (callback(t, data) == true)
+ return t;
+ return NULL;
+}
+
+
+struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra)
+{
+ struct spa_bt_transport *t;
+
+ t = calloc(1, sizeof(struct spa_bt_transport) + extra);
+ if (t == NULL)
+ return NULL;
+
+ t->acquire_refcount = 0;
+ t->monitor = monitor;
+ t->path = path;
+ t->fd = -1;
+ t->sco_io = NULL;
+ t->delay = SPA_BT_UNKNOWN_DELAY;
+ t->user_data = SPA_PTROFF(t, sizeof(struct spa_bt_transport), void);
+ spa_hook_list_init(&t->listener_list);
+ spa_list_init(&t->bap_transport_linked);
+
+ spa_list_append(&monitor->transport_list, &t->link);
+
+ return t;
+}
+
+bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport)
+{
+ return transport->device != NULL
+ && (transport->device->hw_volume_profiles & transport->profile);
+}
+
+static void transport_sync_volume(struct spa_bt_transport *transport)
+{
+ if (!spa_bt_transport_volume_enabled(transport))
+ return;
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i)
+ spa_bt_transport_set_volume(transport, i, transport->volumes[i].volume);
+ spa_bt_transport_emit_volume_changed(transport);
+}
+
+void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ enum spa_bt_transport_state old = transport->state;
+
+ if (old != state) {
+ transport->state = state;
+ spa_log_debug(monitor->log, "transport %p: %s state changed %d -> %d",
+ transport, transport->path, old, state);
+ spa_bt_transport_emit_state_changed(transport, old, state);
+ if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING)
+ transport_sync_volume(transport);
+ }
+}
+
+void spa_bt_transport_free(struct spa_bt_transport *transport)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ struct spa_bt_device *device = transport->device;
+ uint32_t prev_connected = 0;
+
+ spa_log_debug(monitor->log, "transport %p: free %s", transport, transport->path);
+
+ spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE);
+
+ spa_bt_transport_keepalive(transport, false);
+
+ spa_bt_transport_emit_destroy(transport);
+
+ spa_bt_transport_stop_volume_timer(transport);
+ spa_bt_transport_stop_release_timer(transport);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ spa_bt_transport_destroy(transport);
+
+ if (transport->fd >= 0) {
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
+
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+ }
+
+ spa_list_remove(&transport->link);
+ if (transport->device) {
+ prev_connected = transport->device->connected_profiles;
+ transport->device->connected_profiles &= ~transport->profile;
+ spa_list_remove(&transport->device_link);
+ }
+
+ if (device && device->connected_profiles != prev_connected)
+ spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+
+ spa_list_remove(&transport->bap_transport_linked);
+
+ free(transport->endpoint_path);
+ free(transport->path);
+ free(transport);
+}
+
+int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive)
+{
+ if (keepalive) {
+ t->keepalive = true;
+ return 0;
+ }
+
+ t->keepalive = false;
+
+ if (t->acquire_refcount == 0 && t->acquired) {
+ t->acquire_refcount = 1;
+ return spa_bt_transport_release(t);
+ }
+
+ return 0;
+}
+
+int spa_bt_transport_acquire(struct spa_bt_transport *transport, bool optional)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ int res;
+
+ if (transport->acquire_refcount > 0) {
+ spa_log_debug(monitor->log, "transport %p: incref %s", transport, transport->path);
+ transport->acquire_refcount += 1;
+ return 0;
+ }
+ spa_assert(transport->acquire_refcount == 0);
+
+ if (!transport->acquired)
+ res = spa_bt_transport_impl(transport, acquire, 0, optional);
+ else
+ res = 0;
+
+ if (res >= 0) {
+ transport->acquire_refcount = 1;
+ transport->acquired = true;
+ }
+
+ return res;
+}
+
+int spa_bt_transport_release(struct spa_bt_transport *transport)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ int res;
+
+ if (transport->acquire_refcount > 1) {
+ spa_log_debug(monitor->log, "transport %p: decref %s", transport, transport->path);
+ transport->acquire_refcount -= 1;
+ return 0;
+ }
+ else if (transport->acquire_refcount == 0) {
+ spa_log_info(monitor->log, "transport %s already released", transport->path);
+ return 0;
+ }
+ spa_assert(transport->acquire_refcount == 1);
+ spa_assert(transport->acquired);
+
+ if (SPA_BT_TRANSPORT_IS_SCO(transport)) {
+ /* Postpone SCO transport releases, since we might need it again soon */
+ res = spa_bt_transport_start_release_timer(transport);
+ } else if (transport->keepalive) {
+ res = 0;
+ transport->acquire_refcount = 0;
+ spa_log_debug(monitor->log, "transport %p: keepalive %s on release",
+ transport, transport->path);
+ } else {
+ res = spa_bt_transport_impl(transport, release, 0);
+ if (res >= 0) {
+ transport->acquire_refcount = 0;
+ transport->acquired = false;
+ }
+ }
+
+ return res;
+}
+
+static int spa_bt_transport_release_now(struct spa_bt_transport *transport)
+{
+ int res;
+
+ if (!transport->acquired)
+ return 0;
+
+ spa_bt_transport_stop_release_timer(transport);
+ res = spa_bt_transport_impl(transport, release, 0);
+ if (res >= 0) {
+ transport->acquire_refcount = 0;
+ transport->acquired = false;
+ }
+
+ return res;
+}
+
+int spa_bt_device_release_transports(struct spa_bt_device *device)
+{
+ struct spa_bt_transport *t;
+ spa_list_for_each(t, &device->transport_list, device_link)
+ spa_bt_transport_release_now(t);
+ return 0;
+}
+
+static int start_timeout_timer(struct spa_bt_monitor *monitor,
+ struct spa_source *timer, spa_source_func_t timer_event,
+ time_t timeout_msec, void *data)
+{
+ struct itimerspec ts;
+ if (timer->data == NULL) {
+ timer->data = data;
+ timer->func = timer_event;
+ timer->fd = spa_system_timerfd_create(
+ monitor->main_system, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ timer->mask = SPA_IO_IN;
+ timer->rmask = 0;
+ spa_loop_add_source(monitor->main_loop, timer);
+ }
+ ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC;
+ ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int stop_timeout_timer(struct spa_bt_monitor *monitor, struct spa_source *timer)
+{
+ struct itimerspec ts;
+
+ if (timer->data == NULL)
+ return 0;
+
+ spa_loop_remove_source(monitor->main_loop, timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, timer->fd);
+ timer->data = NULL;
+ return 0;
+}
+
+static void spa_bt_transport_release_timer_event(struct spa_source *source)
+{
+ struct spa_bt_transport *transport = source->data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+
+ spa_assert(transport->acquire_refcount >= 1);
+ spa_assert(transport->acquired);
+
+ spa_bt_transport_stop_release_timer(transport);
+
+ if (transport->acquire_refcount == 1) {
+ if (!transport->keepalive) {
+ spa_bt_transport_impl(transport, release, 0);
+ transport->acquired = false;
+ } else {
+ spa_log_debug(monitor->log, "transport %p: keepalive %s on release",
+ transport, transport->path);
+ }
+ } else {
+ spa_log_debug(monitor->log, "transport %p: delayed decref %s", transport, transport->path);
+ }
+ transport->acquire_refcount -= 1;
+}
+
+static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport)
+{
+ return start_timeout_timer(transport->monitor,
+ &transport->release_timer,
+ spa_bt_transport_release_timer_event,
+ SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC, transport);
+}
+
+static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport)
+{
+ return stop_timeout_timer(transport->monitor, &transport->release_timer);
+}
+
+static void spa_bt_transport_volume_changed(struct spa_bt_transport *transport)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ struct spa_bt_transport_volume * t_volume;
+ int volume_id;
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ volume_id = SPA_BT_VOLUME_ID_TX;
+ else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE)
+ volume_id = SPA_BT_VOLUME_ID_RX;
+ else
+ return;
+
+ t_volume = &transport->volumes[volume_id];
+
+ if (t_volume->hw_volume != t_volume->new_hw_volume) {
+ t_volume->hw_volume = t_volume->new_hw_volume;
+ t_volume->volume = spa_bt_volume_hw_to_linear(t_volume->hw_volume,
+ t_volume->hw_volume_max);
+ spa_log_debug(monitor->log, "transport %p: volume changed %d(%f) ",
+ transport, t_volume->new_hw_volume, t_volume->volume);
+ if (spa_bt_transport_volume_enabled(transport)) {
+ transport->device->a2dp_volume_active[volume_id] = true;
+ spa_bt_transport_emit_volume_changed(transport);
+ }
+ }
+}
+
+static void spa_bt_transport_volume_timer_event(struct spa_source *source)
+{
+ struct spa_bt_transport *transport = source->data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_bt_transport_volume_changed(transport);
+}
+
+static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport)
+{
+ return start_timeout_timer(transport->monitor,
+ &transport->volume_timer,
+ spa_bt_transport_volume_timer_event,
+ TRANSPORT_VOLUME_TIMEOUT_MSEC, transport);
+}
+
+static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport)
+{
+ return stop_timeout_timer(transport->monitor, &transport->volume_timer);
+}
+
+
+int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop)
+{
+ if (t->sco_io == NULL) {
+ t->sco_io = spa_bt_sco_io_create(data_loop,
+ t->fd,
+ t->read_mtu,
+ t->write_mtu);
+ if (t->sco_io == NULL)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t)
+{
+ if (t->delay != SPA_BT_UNKNOWN_DELAY)
+ return (int64_t)t->delay * 100 * SPA_NSEC_PER_USEC;
+
+ /* Fallback values when device does not provide information */
+
+ if (t->media_codec == NULL)
+ return 30 * SPA_NSEC_PER_MSEC;
+
+ switch (t->media_codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_SBC:
+ case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ:
+ return 200 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_MPEG:
+ case SPA_BLUETOOTH_AUDIO_CODEC_AAC:
+ return 200 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD:
+ return 150 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_LDAC:
+ return 175 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL:
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM:
+ case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_LC3:
+ return 40 * SPA_NSEC_PER_MSEC;
+ default:
+ break;
+ };
+ return 150 * SPA_NSEC_PER_MSEC;
+}
+
+static int transport_update_props(struct spa_bt_transport *transport,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%s", transport, key, value);
+
+ if (spa_streq(key, "UUID")) {
+ switch (spa_bt_profile_from_uuid(value)) {
+ case SPA_BT_PROFILE_A2DP_SOURCE:
+ transport->profile = SPA_BT_PROFILE_A2DP_SINK;
+ break;
+ case SPA_BT_PROFILE_A2DP_SINK:
+ transport->profile = SPA_BT_PROFILE_A2DP_SOURCE;
+ break;
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ transport->profile = SPA_BT_PROFILE_BAP_SINK;
+ break;
+ case SPA_BT_PROFILE_BAP_SINK:
+ transport->profile = SPA_BT_PROFILE_BAP_SOURCE;
+ break;
+ default:
+ spa_log_warn(monitor->log, "unknown profile %s", value);
+ break;
+ }
+ }
+ else if (spa_streq(key, "State")) {
+ spa_bt_transport_set_state(transport, spa_bt_transport_state_from_string(value));
+ }
+ else if (spa_streq(key, "Device")) {
+ struct spa_bt_device *device = spa_bt_device_find(monitor, value);
+ if (transport->device != device) {
+ if (transport->device != NULL)
+ spa_list_remove(&transport->device_link);
+ transport->device = device;
+ if (device != NULL)
+ spa_list_append(&device->transport_list, &transport->device_link);
+ else
+ spa_log_warn(monitor->log, "could not find device %s", value);
+ }
+ }
+ else if (spa_streq(key, "Endpoint")) {
+ struct spa_bt_remote_endpoint *ep = remote_endpoint_find(monitor, value);
+ if (!ep) {
+ spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", value);
+ goto next;
+ }
+
+ // If the remote endpoint is an acceptor this transport is an initiator
+ transport->bap_initiator = ep->acceptor;
+ }
+ }
+ else if (spa_streq(key, "Codec")) {
+ uint8_t value;
+
+ if (type != DBUS_TYPE_BYTE)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->codec = value;
+ }
+ else if (spa_streq(key, "Configuration")) {
+ DBusMessageIter iter;
+ uint8_t *value;
+ int len;
+
+ if (!check_iter_signature(&it[1], "ay"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+ dbus_message_iter_get_fixed_array(&iter, &value, &len);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%d", transport, key, len);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len);
+
+ free(transport->configuration);
+ transport->configuration_len = 0;
+
+ transport->configuration = malloc(len);
+ if (transport->configuration) {
+ memcpy(transport->configuration, value, len);
+ transport->configuration_len = len;
+ }
+ }
+ else if (spa_streq(key, "Volume")) {
+ uint16_t value;
+ struct spa_bt_transport_volume * t_volume;
+
+ if (type != DBUS_TYPE_UINT16)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%d", transport, key, value);
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX];
+ else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE)
+ t_volume = &transport->volumes[SPA_BT_VOLUME_ID_RX];
+ else
+ goto next;
+
+ t_volume->active = true;
+ t_volume->new_hw_volume = value;
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ spa_bt_transport_start_volume_timer(transport);
+ else
+ spa_bt_transport_volume_changed(transport);
+ }
+ else if (spa_streq(key, "Delay")) {
+ uint16_t value;
+
+ if (type != DBUS_TYPE_UINT16)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->delay = value;
+ spa_bt_transport_emit_delay_changed(transport);
+ }
+ else if (spa_streq(key, "PresentationDelay")) {
+ uint32_t value;
+
+ if (type != DBUS_TYPE_UINT32)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->delay = value / 100;
+ spa_bt_transport_emit_delay_changed(transport);
+ }
+ else if (spa_streq(key, "Links")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "ao"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *transport_path;
+ struct spa_bt_transport *t;
+
+ dbus_message_iter_get_basic(&iter, &transport_path);
+
+ spa_log_debug(monitor->log, "transport %p: Linked with=%s", transport, transport_path);
+ t = spa_bt_transport_find(monitor, transport_path);
+ if (!t) {
+ spa_log_warn(monitor->log, "Unable to find linked transport");
+ dbus_message_iter_next(&iter);
+ continue;
+ }
+
+ if (spa_list_is_empty(&t->bap_transport_linked))
+ spa_list_append(&transport->bap_transport_linked, &t->bap_transport_linked);
+ else if (spa_list_is_empty(&transport->bap_transport_linked))
+ spa_list_append(&t->bap_transport_linked, &transport->bap_transport_linked);
+
+ dbus_message_iter_next(&iter);
+ }
+ }
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static int transport_set_property_volume(struct spa_bt_transport *transport, uint16_t value)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r;
+ DBusMessageIter it[2];
+ DBusError err;
+ const char *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE;
+ const char *name = "Volume";
+ int res = 0;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ transport->path,
+ DBUS_INTERFACE_PROPERTIES,
+ "Set");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_iter_init_append(m, &it[0]);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &interface);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &name);
+ dbus_message_iter_open_container(&it[0], DBUS_TYPE_VARIANT,
+ DBUS_TYPE_UINT16_AS_STRING, &it[1]);
+ dbus_message_iter_append_basic(&it[1], DBUS_TYPE_UINT16, &value);
+ dbus_message_iter_close_container(&it[0], &it[1]);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(monitor->log, "set volume %u failed for transport %s (%s)",
+ value, transport->path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR)
+ res = -EIO;
+
+ dbus_message_unref(r);
+
+ spa_log_debug(monitor->log, "transport %p: set volume to %d", transport, value);
+
+ return res;
+}
+
+static int transport_set_volume(void *data, int id, float volume)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_transport_volume *t_volume = &transport->volumes[id];
+ uint16_t value;
+
+ if (!t_volume->active || !spa_bt_transport_volume_enabled(transport))
+ return -ENOTSUP;
+
+ value = spa_bt_volume_linear_to_hw(volume, 127);
+ t_volume->volume = volume;
+
+ /* AVRCP volume would not applied on remote sink device
+ * if transport is not acquired (idle). */
+ if (transport->fd < 0 && (transport->profile & SPA_BT_PROFILE_A2DP_SINK)) {
+ t_volume->hw_volume = SPA_BT_VOLUME_INVALID;
+ return 0;
+ } else if (t_volume->hw_volume != value) {
+ t_volume->hw_volume = value;
+ spa_bt_transport_stop_volume_timer(transport);
+ transport_set_property_volume(transport, value);
+ }
+ return 0;
+}
+
+static int transport_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r = NULL;
+ DBusError err;
+ int ret = 0;
+ const char *method = optional ? "TryAcquire" : "Acquire";
+ struct spa_bt_transport *t_linked;
+
+ /* For LE Audio, multiple transport from the same device may share the same
+ * stream (CIS) and group (CIG) but for different direction, e.g. a speaker and
+ * a microphone. In this case they are linked.
+ * If one of them has already been acquired this function should not call Acquire
+ * or TryAcquire but re-use values from the previously acquired transport.
+ */
+ spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+ if (t_linked->acquired && t_linked->device == transport->device) {
+ transport->fd = t_linked->fd;
+ transport->read_mtu = t_linked->read_mtu;
+ transport->write_mtu = t_linked->write_mtu;
+ spa_log_debug(monitor->log, "transport %p: linked transport %s", transport, t_linked->path);
+ goto done;
+ }
+ }
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ transport->path,
+ BLUEZ_MEDIA_TRANSPORT_INTERFACE,
+ method);
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ if (optional && spa_streq(err.name, "org.bluez.Error.NotAvailable")) {
+ spa_log_info(monitor->log, "Failed optional acquire of unavailable transport %s",
+ transport->path);
+ }
+ else {
+ spa_log_error(monitor->log, "Transport %s() failed for transport %s (%s)",
+ method, transport->path, err.message);
+ }
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "%s returned error: %s", method, dbus_message_get_error_name(r));
+ ret = -EIO;
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_UNIX_FD, &transport->fd,
+ DBUS_TYPE_UINT16, &transport->read_mtu,
+ DBUS_TYPE_UINT16, &transport->write_mtu,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Failed to parse %s() reply: %s", method, err.message);
+ dbus_error_free(&err);
+ ret = -EIO;
+ goto finish;
+ }
+done:
+ spa_log_debug(monitor->log, "transport %p: %s %s, fd %d MTU %d:%d", transport, method,
+ transport->path, transport->fd, transport->read_mtu, transport->write_mtu);
+
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_PLAYING);
+
+ transport_sync_volume(transport);
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+ return ret;
+}
+
+static int transport_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r;
+ DBusError err;
+ bool is_idle = (transport->state == SPA_BT_TRANSPORT_STATE_IDLE);
+ struct spa_bt_transport *t_linked;
+ bool linked = false;
+
+ spa_log_debug(monitor->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
+
+ /* For LE Audio, multiple transport stream (CIS) can be linked together (CIG).
+ * If they are part of the same device they re-use the same fd, and call to
+ * release should be done for the last one only.
+ */
+ spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+ if (t_linked->acquired && t_linked->device == transport->device) {
+ linked = true;
+ break;
+ }
+ }
+ if (linked) {
+ spa_log_info(monitor->log, "Linked transport %s released", transport->path);
+ transport->fd = -1;
+ return 0;
+ }
+
+ close(transport->fd);
+ transport->fd = -1;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ transport->path,
+ BLUEZ_MEDIA_TRANSPORT_INTERFACE,
+ "Release");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r != NULL)
+ dbus_message_unref(r);
+
+ if (dbus_error_is_set(&err)) {
+ if (is_idle) {
+ /* XXX: The fd always needs to be closed. However, Release()
+ * XXX: apparently doesn't need to be called on idle transports
+ * XXX: and fails. We call it just to be sure (e.g. in case
+ * XXX: there's a race with updating the property), but tone down the error.
+ */
+ spa_log_debug(monitor->log, "Failed to release idle transport %s: %s",
+ transport->path, err.message);
+ } else {
+ spa_log_error(monitor->log, "Failed to release transport %s: %s",
+ transport->path, err.message);
+ }
+ dbus_error_free(&err);
+ }
+ else
+ spa_log_info(monitor->log, "Transport %s released", transport->path);
+
+ return 0;
+}
+
+static const struct spa_bt_transport_implementation transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = transport_acquire,
+ .release = transport_release,
+ .set_volume = transport_set_volume,
+};
+
+static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata);
+
+static int media_codec_switch_cmp(const void *a, const void *b);
+
+static struct spa_bt_media_codec_switch *media_codec_switch_cmp_sw; /* global for qsort */
+
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout);
+
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw);
+
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw)
+{
+ char **p;
+
+ media_codec_switch_stop_timer(sw);
+
+ if (sw->pending != NULL) {
+ dbus_pending_call_cancel(sw->pending);
+ dbus_pending_call_unref(sw->pending);
+ }
+
+ if (sw->device != NULL)
+ spa_list_remove(&sw->device_link);
+
+ if (sw->paths != NULL)
+ for (p = sw->paths; *p != NULL; ++p)
+ free(*p);
+
+ free(sw->paths);
+ free(sw->codecs);
+ free(sw);
+}
+
+static void media_codec_switch_next(struct spa_bt_media_codec_switch *sw)
+{
+ spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL);
+
+ ++sw->path_iter;
+ if (*sw->path_iter == NULL) {
+ ++sw->codec_iter;
+ sw->path_iter = sw->paths;
+ }
+
+ sw->retries = CODEC_SWITCH_RETRIES;
+}
+
+static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct media_codec *codec;
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ enum spa_bt_media_direction direction;
+ char *local_endpoint = NULL;
+ int res, config_size;
+ dbus_bool_t dbus_ret;
+ DBusMessage *m;
+ DBusMessageIter iter, d;
+ int i;
+ bool sink;
+
+ /* Try setting configuration for current codec on current endpoint in list */
+
+ codec = *sw->codec_iter;
+
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: consider codec %s for remote endpoint %s",
+ sw, (*sw->codec_iter)->name, *sw->path_iter);
+
+ ep = device_remote_endpoint_find(sw->device, *sw->path_iter);
+
+ if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s not valid, try next",
+ sw, *sw->path_iter);
+ goto next;
+ }
+
+ /* Setup and check compatible configuration */
+ if (ep->codec != codec->codec_id) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: different codec, try next", sw);
+ goto next;
+ }
+
+ if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: wrong uuid (%s) for profile, try next",
+ sw, ep->uuid);
+ goto next;
+ }
+
+ if ((sw->profile & SPA_BT_PROFILE_A2DP_SINK) || (sw->profile & SPA_BT_PROFILE_BAP_SINK) ) {
+ direction = SPA_BT_MEDIA_SOURCE;
+ sink = false;
+ } else if ((sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) || (sw->profile & SPA_BT_PROFILE_BAP_SOURCE) ) {
+ direction = SPA_BT_MEDIA_SINK;
+ sink = true;
+ } else {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: bad profile (%d), try next",
+ sw, sw->profile);
+ goto next;
+ }
+
+ if (media_codec_to_endpoint(codec, direction, &local_endpoint) < 0) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: no endpoint for codec %s, try next",
+ sw, codec->name);
+ goto next;
+ }
+
+ /* Each endpoint can be used by only one device at a time (on each adapter) */
+ spa_list_for_each(t, &sw->device->monitor->transport_list, link) {
+ if (t->device == sw->device)
+ continue;
+ if (t->device->adapter != sw->device->adapter)
+ continue;
+ if (spa_streq(t->endpoint_path, local_endpoint)) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s in use, try next",
+ sw, local_endpoint);
+ goto next;
+ }
+ }
+
+ res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len,
+ &sw->device->monitor->default_audio_info,
+ &sw->device->monitor->global_settings, config);
+ if (res < 0) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: incompatible capabilities (%d), try next",
+ sw, res);
+ goto next;
+ }
+ config_size = res;
+
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: configuration %d", sw, config_size);
+ for (i = 0; i < config_size; i++)
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: %d: %02x", sw, i, config[i]);
+
+ /* Codecs may share the same endpoint, so indicate which one we are using */
+ sw->device->preferred_codec = codec;
+
+ /* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration");
+ if (m == NULL) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: dbus allocation failure, try next", sw);
+ goto next;
+ }
+
+ spa_bt_device_update_last_bluez_action_time(sw->device);
+
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: trying codec %s for endpoint %s, local endpoint %s",
+ sw, codec->name, ep->path, local_endpoint);
+
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d);
+ append_basic_array_variant_dict_entry(&d, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, config, config_size);
+ dbus_message_iter_close_container(&iter, &d);
+
+ spa_assert(sw->pending == NULL);
+ dbus_ret = dbus_connection_send_with_reply(sw->device->monitor->conn, m, &sw->pending, -1);
+
+ if (!dbus_ret || sw->pending == NULL) {
+ spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw);
+ dbus_message_unref(m);
+ goto next;
+ }
+
+ dbus_ret = dbus_pending_call_set_notify(sw->pending, media_codec_switch_reply, sw, NULL);
+ dbus_message_unref(m);
+
+ if (!dbus_ret) {
+ spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus set notify failure", sw);
+ goto next;
+ }
+
+ free(local_endpoint);
+ return true;
+
+next:
+ free(local_endpoint);
+ return false;
+}
+
+static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw)
+{
+ while (*sw->codec_iter != NULL && *sw->path_iter != NULL) {
+ struct timespec ts;
+ uint64_t now, threshold;
+
+ /* Rate limit BlueZ calls */
+ spa_system_clock_gettime(sw->device->monitor->main_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ threshold = sw->device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC;
+ if (now < threshold) {
+ /* Wait for timeout */
+ media_codec_switch_start_timer(sw, threshold - now);
+ return;
+ }
+
+ if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) {
+ /* Sort endpoints according to codec preference, when at a new codec. */
+ media_codec_switch_cmp_sw = sw;
+ qsort(sw->paths, sw->num_paths, sizeof(char *), media_codec_switch_cmp);
+ }
+
+ if (media_codec_switch_process_current(sw)) {
+ /* Wait for dbus reply */
+ return;
+ }
+
+ media_codec_switch_next(sw);
+ };
+
+ /* Didn't find any suitable endpoint. Report failure. */
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: failed to get an endpoint", sw);
+ spa_bt_device_emit_codec_switched(sw->device, -ENODEV);
+ spa_bt_device_check_profiles(sw->device, false);
+ media_codec_switch_free(sw);
+}
+
+static bool media_codec_switch_goto_active(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_device *device = sw->device;
+ struct spa_bt_media_codec_switch *active_sw;
+
+ active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_media_codec_switch, device_link);
+
+ if (active_sw != sw) {
+ struct spa_bt_media_codec_switch *t;
+
+ /* This codec switch has been canceled. Switch to the newest one. */
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: canceled, go to new switch", sw);
+
+ spa_list_for_each_safe(sw, t, &device->codec_switch_list, device_link) {
+ if (sw != active_sw)
+ media_codec_switch_free(sw);
+ }
+
+ media_codec_switch_process(active_sw);
+ return false;
+ }
+
+ return true;
+}
+
+static void media_codec_switch_timer_event(struct spa_source *source)
+{
+ struct spa_bt_media_codec_switch *sw = source->data;
+ struct spa_bt_device *device = sw->device;
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_log_debug(monitor->log, "media codec switch %p: rate limit timer event", sw);
+
+ media_codec_switch_stop_timer(sw);
+
+ if (!media_codec_switch_goto_active(sw))
+ return;
+
+ media_codec_switch_process(sw);
+}
+
+static void media_codec_switch_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_media_codec_switch *sw = user_data;
+ struct spa_bt_device *device = sw->device;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+
+ spa_assert(sw->pending == pending);
+ dbus_pending_call_unref(pending);
+ sw->pending = NULL;
+
+ spa_bt_device_update_last_bluez_action_time(device);
+
+ if (!media_codec_switch_goto_active(sw)) {
+ if (r != NULL)
+ dbus_message_unref(r);
+ return;
+ }
+
+ if (r == NULL) {
+ spa_log_error(sw->device->monitor->log,
+ "media codec switch %p: empty reply from dbus, trying next",
+ sw);
+ goto next;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: failed (%s), trying next",
+ sw, dbus_message_get_error_name(r));
+ dbus_message_unref(r);
+ goto next;
+ }
+
+ dbus_message_unref(r);
+
+ /* Success */
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw);
+ spa_bt_device_emit_codec_switched(sw->device, 0);
+ spa_bt_device_check_profiles(sw->device, false);
+ media_codec_switch_free(sw);
+ return;
+
+next:
+ if (sw->retries > 0)
+ --sw->retries;
+ else
+ media_codec_switch_next(sw);
+
+ media_codec_switch_process(sw);
+ return;
+}
+
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout)
+{
+ struct spa_bt_monitor *monitor = sw->device->monitor;
+ struct itimerspec ts;
+
+ spa_assert(sw->timer.data == NULL);
+
+ spa_log_debug(monitor->log, "media codec switch %p: starting rate limit timer", sw);
+
+ if (sw->timer.data == NULL) {
+ sw->timer.data = sw;
+ sw->timer.func = media_codec_switch_timer_event;
+ sw->timer.fd = spa_system_timerfd_create(monitor->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ sw->timer.mask = SPA_IO_IN;
+ sw->timer.rmask = 0;
+ spa_loop_add_source(monitor->main_loop, &sw->timer);
+ }
+ ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_monitor *monitor = sw->device->monitor;
+ struct itimerspec ts;
+
+ if (sw->timer.data == NULL)
+ return 0;
+
+ spa_log_debug(monitor->log, "media codec switch %p: stopping rate limit timer", sw);
+
+ spa_loop_remove_source(monitor->main_loop, &sw->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, sw->timer.fd);
+ sw->timer.data = NULL;
+ return 0;
+}
+
+static int media_codec_switch_cmp(const void *a, const void *b)
+{
+ struct spa_bt_media_codec_switch *sw = media_codec_switch_cmp_sw;
+ const struct media_codec *codec = *sw->codec_iter;
+ const char *path1 = *(char **)a, *path2 = *(char **)b;
+ struct spa_bt_remote_endpoint *ep1, *ep2;
+ uint32_t flags;
+
+ ep1 = device_remote_endpoint_find(sw->device, path1);
+ ep2 = device_remote_endpoint_find(sw->device, path2);
+
+ if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id || ep1->capabilities == NULL))
+ ep1 = NULL;
+ if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL))
+ ep2 = NULL;
+ if (ep1 && ep2 && !spa_streq(ep1->uuid, ep2->uuid)) {
+ ep1 = NULL;
+ ep2 = NULL;
+ }
+
+ if (ep1 == NULL && ep2 == NULL)
+ return 0;
+ else if (ep1 == NULL)
+ return 1;
+ else if (ep2 == NULL)
+ return -1;
+
+ if (codec->bap)
+ flags = spa_streq(ep1->uuid, SPA_BT_UUID_BAP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0;
+ else
+ flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0;
+
+ return codec->caps_preference_cmp(codec, flags, ep1->capabilities, ep1->capabilities_len,
+ ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info,
+ &sw->device->monitor->global_settings);
+}
+
+/* Ensure there's a transport for at least one of the listed codecs */
+int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs)
+{
+ struct spa_bt_media_codec_switch *sw;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct media_codec *preferred_codec = NULL;
+ size_t i, j, num_codecs, num_eps;
+
+ if (!device->adapter->application_registered) {
+ /* Codec switching not supported */
+ return -ENOTSUP;
+ }
+
+ for (i = 0; codecs[i] != NULL; ++i) {
+ if (spa_bt_device_supports_media_codec(device, codecs[i], true)) {
+ preferred_codec = codecs[i];
+ break;
+ }
+ }
+
+ /* Check if we already have an enabled transport for the most preferred codec.
+ * However, if there already was a codec switch running, these transports may
+ * disappear soon. In that case, we have to do the full thing.
+ */
+ if (spa_list_is_empty(&device->codec_switch_list) && preferred_codec != NULL) {
+ spa_list_for_each(t, &device->transport_list, device_link) {
+ if (t->media_codec != preferred_codec)
+ continue;
+
+ if ((device->connected_profiles & t->profile) != t->profile)
+ continue;
+
+ spa_bt_device_emit_codec_switched(device, 0);
+ return 0;
+ }
+ }
+
+ /* Setup and start iteration */
+
+ sw = calloc(1, sizeof(struct spa_bt_media_codec_switch));
+ if (sw == NULL)
+ return -ENOMEM;
+
+ num_eps = 0;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link)
+ ++num_eps;
+
+ num_codecs = 0;
+ while (codecs[num_codecs] != NULL)
+ ++num_codecs;
+
+ sw->codecs = calloc(num_codecs + 1, sizeof(const struct media_codec *));
+ sw->paths = calloc(num_eps + 1, sizeof(char *));
+ sw->num_paths = num_eps;
+
+ if (sw->codecs == NULL || sw->paths == NULL) {
+ media_codec_switch_free(sw);
+ return -ENOMEM;
+ }
+
+ for (i = 0, j = 0; i < num_codecs; ++i) {
+ if (is_media_codec_enabled(device->monitor, codecs[i])) {
+ sw->codecs[j] = codecs[i];
+ ++j;
+ }
+ }
+ sw->codecs[j] = NULL;
+
+ i = 0;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
+ sw->paths[i] = strdup(ep->path);
+ if (sw->paths[i] == NULL) {
+ media_codec_switch_free(sw);
+ return -ENOMEM;
+ }
+ ++i;
+ }
+ sw->paths[i] = NULL;
+
+ sw->codec_iter = sw->codecs;
+ sw->path_iter = sw->paths;
+ sw->retries = CODEC_SWITCH_RETRIES;
+
+ sw->profile = device->connected_profiles;
+
+ sw->device = device;
+
+ if (!spa_list_is_empty(&device->codec_switch_list)) {
+ /*
+ * There's a codec switch already running, either waiting for timeout or
+ * BlueZ reply.
+ *
+ * BlueZ does not appear to allow calling dbus_pending_call_cancel on an
+ * active request, so we have to wait for the reply to arrive first, and
+ * only then start processing this request. The timeout we would also have
+ * to wait to pass in any case, so we don't cancel it either.
+ */
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: already in progress, canceling previous",
+ sw);
+
+ spa_list_prepend(&device->codec_switch_list, &sw->device_link);
+ } else {
+ spa_list_prepend(&device->codec_switch_list, &sw->device_link);
+ media_codec_switch_process(sw);
+ }
+
+ return 0;
+}
+
+int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ return spa_bt_backend_ensure_codec(monitor->backend, device, codec);
+}
+
+int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ return spa_bt_backend_supports_codec(monitor->backend, device, codec);
+}
+
+static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
+ const char *path, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *transport_path, *endpoint;
+ DBusMessageIter it[2];
+ DBusMessage *r;
+ struct spa_bt_transport *transport;
+ const struct media_codec *codec;
+ int profile;
+ bool sink;
+
+ if (!dbus_message_has_signature(m, "oa{sv}")) {
+ spa_log_warn(monitor->log, "invalid SetConfiguration() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ endpoint = dbus_message_get_path(m);
+
+ profile = media_endpoint_to_profile(endpoint);
+ codec = media_endpoint_to_codec(monitor, endpoint, &sink, NULL);
+ if (codec == NULL) {
+ spa_log_warn(monitor->log, "unknown SetConfiguration() codec");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &transport_path);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ transport = spa_bt_transport_find(monitor, transport_path);
+
+ if (transport == NULL) {
+ char *tpath = strdup(transport_path);
+
+ transport = spa_bt_transport_create(monitor, tpath, 0);
+ if (transport == NULL) {
+ free(tpath);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+
+ spa_bt_transport_set_implementation(transport, &transport_impl, transport);
+
+ if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
+ transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME;
+ transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME;
+ } else {
+ transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME;
+ transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME;
+ }
+ }
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) {
+ transport->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID;
+ transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_A2DP_MAX;
+ }
+
+ free(transport->endpoint_path);
+ transport->endpoint_path = strdup(endpoint);
+ transport->profile = profile;
+ transport->media_codec = codec;
+ transport_update_props(transport, &it[1], NULL);
+
+ if (transport->device == NULL || transport->device->adapter == NULL) {
+ spa_log_warn(monitor->log, "no device found for transport");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ /* If multiple codecs share the endpoint, pick the one we wanted */
+ transport->media_codec = codec = media_endpoint_to_codec(monitor, endpoint, &sink,
+ transport->device->preferred_codec);
+ spa_assert(codec != NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
+ spa_bt_device_update_last_bluez_action_time(transport->device);
+
+ if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
+ /* PW is the rendering device so it's responsible for reporting hardware volume. */
+ transport->volumes[SPA_BT_VOLUME_ID_RX].active = true;
+ } else if (profile & SPA_BT_PROFILE_A2DP_SINK) {
+ transport->volumes[SPA_BT_VOLUME_ID_TX].active
+ |= transport->device->a2dp_volume_active[SPA_BT_VOLUME_ID_TX];
+ }
+
+ if (codec->validate_config) {
+ struct spa_audio_info info;
+ if (codec->validate_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0,
+ transport->configuration, transport->configuration_len,
+ &info) < 0) {
+ spa_log_error(monitor->log, "invalid transport configuration");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ transport->n_channels = info.info.raw.channels;
+ memcpy(transport->channels, info.info.raw.position,
+ transport->n_channels * sizeof(uint32_t));
+ } else {
+ transport->n_channels = 2;
+ transport->channels[0] = SPA_AUDIO_CHANNEL_FL;
+ transport->channels[1] = SPA_AUDIO_CHANNEL_FR;
+ }
+ spa_log_info(monitor->log, "%p: %s validate conf channels:%d",
+ monitor, path, transport->n_channels);
+
+ spa_bt_device_add_profile(transport->device, transport->profile);
+
+ spa_bt_device_connect_profile(transport->device, transport->profile);
+
+ /* Sync initial volumes */
+ transport_sync_volume(transport);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ DBusError err;
+ DBusMessage *r;
+ const char *transport_path;
+ struct spa_bt_transport *transport;
+
+ dbus_error_init(&err);
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_OBJECT_PATH, &transport_path,
+ DBUS_TYPE_INVALID)) {
+ spa_log_warn(monitor->log, "Bad ClearConfiguration method call: %s",
+ err.message);
+ dbus_error_free(&err);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ transport = spa_bt_transport_find(monitor, transport_path);
+
+ if (transport != NULL) {
+ struct spa_bt_device *device = transport->device;
+
+ spa_log_debug(monitor->log, "transport %p: free %s",
+ transport, transport->path);
+
+ spa_bt_transport_free(transport);
+ if (device != NULL)
+ spa_bt_device_check_profiles(device, false);
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ DBusMessage *r;
+
+ r = dbus_message_new_error(m,
+ BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = ENDPOINT_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration"))
+ res = endpoint_set_configuration(c, path, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration"))
+ res = endpoint_select_configuration(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectProperties"))
+ res = endpoint_select_properties(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration"))
+ res = endpoint_clear_configuration(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release"))
+ res = endpoint_release(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "RegisterEndpoint() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
+ DBusMessageIter dict_entry_it, variant_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_append_basic(&variant_it, variant_type_int, variant);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size) {
+ DBusMessageIter dict_entry_it, variant_it, array_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_open_container(&variant_it, DBUS_TYPE_ARRAY, array_type_str, &array_it);
+ dbus_message_iter_append_fixed_array (&array_it, array_type_int, &data, data_size);
+ dbus_message_iter_close_container(&variant_it, &array_it);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static int bluez_register_endpoint(struct spa_bt_monitor *monitor,
+ const char *path, enum spa_bt_media_direction direction,
+ const char *uuid, const struct media_codec *codec)
+{
+ char *object_path = NULL;
+ DBusMessage *m;
+ DBusMessageIter object_it, dict_it;
+ DBusPendingCall *call;
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ int ret, caps_size;
+ uint16_t codec_id = codec->codec_id;
+ bool sink = (direction == SPA_BT_MEDIA_SINK);
+
+ spa_assert(codec->fill_caps);
+
+ ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ goto error;
+
+ ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, caps);
+ if (ret < 0)
+ goto error;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ path,
+ BLUEZ_MEDIA_INTERFACE,
+ "RegisterEndpoint");
+ if (m == NULL) {
+ ret = -EIO;
+ goto error;
+ }
+
+ dbus_message_iter_init_append(m, &object_it);
+ dbus_message_iter_append_basic(&object_it, DBUS_TYPE_OBJECT_PATH, &object_path);
+
+ dbus_message_iter_open_container(&object_it, DBUS_TYPE_ARRAY, "{sv}", &dict_it);
+
+ append_basic_variant_dict_entry(&dict_it,"UUID", DBUS_TYPE_STRING, "s", &uuid);
+ append_basic_variant_dict_entry(&dict_it, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+ append_basic_array_variant_dict_entry(&dict_it, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+
+ dbus_message_iter_close_container(&object_it, &dict_it);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, bluez_register_endpoint_reply, monitor, NULL);
+ dbus_message_unref(m);
+
+ free(object_path);
+
+ return 0;
+
+error:
+ free(object_path);
+ return ret;
+}
+
+static int adapter_register_endpoints(struct spa_bt_adapter *a)
+{
+ struct spa_bt_monitor *monitor = a->monitor;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ int i;
+ int err = 0;
+
+ if (a->endpoints_registered)
+ return err;
+
+ /* The legacy bluez5 api doesn't support codec switching
+ * It doesn't make sense to register codecs other than SBC
+ * as bluez5 will probably use SBC anyway and we have no control over it
+ * let's incentivize users to upgrade their bluez5 daemon
+ * if they want proper media codec support
+ * */
+ spa_log_warn(monitor->log,
+ "Using legacy bluez5 API for A2DP - only SBC will be supported. "
+ "No LE Audio. Please upgrade bluez5.");
+
+ monitor->le_audio_supported = false;
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_SBC)
+ continue;
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
+ if ((err = bluez_register_endpoint(monitor, a->path,
+ SPA_BT_MEDIA_SOURCE,
+ SPA_BT_UUID_A2DP_SOURCE,
+ codec)))
+ goto out;
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+ if ((err = bluez_register_endpoint(monitor, a->path,
+ SPA_BT_MEDIA_SINK,
+ SPA_BT_UUID_A2DP_SINK,
+ codec)))
+ goto out;
+ }
+
+ a->endpoints_registered = true;
+ break;
+ }
+
+ if (!a->endpoints_registered) {
+ /* Should never happen as SBC support is always enabled */
+ spa_log_error(monitor->log, "Broken PipeWire build - unable to locate SBC codec");
+ err = -ENOSYS;
+ }
+
+out:
+ if (err) {
+ spa_log_error(monitor->log, "Failed to register bluez5 endpoints");
+ }
+ return err;
+}
+
+static void append_media_object(DBusMessageIter *iter, const char *endpoint,
+ const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size)
+{
+ const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE;
+ DBusMessageIter object, array, entry, dict;
+ dbus_bool_t delay_reporting;
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object);
+ dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint);
+
+ dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array);
+
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name);
+
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ append_basic_variant_dict_entry(&dict, "UUID", DBUS_TYPE_STRING, "s", &uuid);
+ append_basic_variant_dict_entry(&dict, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+ append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+ if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_A2DP_SOURCE) {
+ delay_reporting = TRUE;
+ append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting);
+ }
+
+ dbus_message_iter_close_container(&entry, &dict);
+ dbus_message_iter_close_container(&array, &entry);
+ dbus_message_iter_close_container(&object, &array);
+ dbus_message_iter_close_container(iter, &object);
+}
+
+static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *m, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const char *path, *interface, *member;
+ char *endpoint;
+ DBusMessage *r;
+ DBusMessageIter iter, array;
+ DBusHandlerResult res;
+ int i;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = OBJECT_MANAGER_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects")) {
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_init_append(r, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array);
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ int caps_size, ret;
+ uint16_t codec_id = codec->codec_id;
+
+ if (!is_media_codec_enabled(monitor, codec))
+ continue;
+
+ if (codec->bap && !monitor->le_audio_supported) {
+ /* The legacy bluez5 api doesn't support LE Audio
+ * It doesn't make sense to register unsupported codecs as it prevents
+ * registration of A2DP codecs
+ * let's incentivize users to upgrade their bluez5 daemon
+ * if they want proper media codec support
+ * */
+ spa_log_warn(monitor->log, "Trying to use legacy bluez5 API for LE Audio - only A2DP will be supported. "
+ "Please upgrade bluez5.");
+ continue;
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+ caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps);
+ if (caps_size < 0)
+ continue;
+
+ ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK, &endpoint);
+ if (ret == 0) {
+ spa_log_info(monitor->log, "register media sink codec %s: %s", media_codecs[i]->name, endpoint);
+ append_media_object(&array, endpoint,
+ codec->bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK,
+ codec_id, caps, caps_size);
+ free(endpoint);
+ }
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
+ caps_size = codec->fill_caps(codec, 0, caps);
+ if (caps_size < 0)
+ continue;
+
+ ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE, &endpoint);
+ if (ret == 0) {
+ spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint);
+ append_media_object(&array, endpoint,
+ codec->bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE,
+ codec_id, caps, caps_size);
+ free(endpoint);
+ }
+ }
+ }
+
+ dbus_message_iter_close_container(&iter, &array);
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void bluez_register_application_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_adapter *adapter = user_data;
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ DBusMessage *r;
+ bool fallback = true;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) {
+ spa_log_warn(monitor->log, "Registering media applications for adapter %s is disabled in bluez5", adapter->path);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ fallback = false;
+ adapter->application_registered = true;
+
+finish:
+ dbus_message_unref(r);
+
+ if (fallback)
+ adapter_register_endpoints(adapter);
+}
+
+static int register_media_endpoint(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ static const DBusObjectPathVTable vtable_endpoint = {
+ .message_function = endpoint_handler,
+ };
+
+ if (!endpoint_should_be_registered(monitor, codec, direction))
+ return 0;
+
+ char *object_path = NULL;
+ int ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ return ret;
+
+ spa_log_info(monitor->log, "registering endpoint: %s", object_path);
+
+ if (!dbus_connection_register_object_path(monitor->conn,
+ object_path,
+ &vtable_endpoint, monitor))
+ {
+ ret = -EIO;
+ }
+
+ free(object_path);
+ return ret;
+}
+
+static int register_media_application(struct spa_bt_monitor * monitor)
+{
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const DBusObjectPathVTable vtable_object_manager = {
+ .message_function = object_manager_handler,
+ };
+
+ spa_log_info(monitor->log, "Registering media application object: " MEDIA_OBJECT_MANAGER_PATH);
+
+ if (!dbus_connection_register_object_path(monitor->conn,
+ MEDIA_OBJECT_MANAGER_PATH,
+ &vtable_object_manager, monitor))
+ return -EIO;
+
+ for (int i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+ register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
+ }
+
+ return 0;
+}
+
+static void unregister_media_endpoint(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ if (!endpoint_should_be_registered(monitor, codec, direction))
+ return;
+
+ char *object_path = NULL;
+ int ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ return;
+
+ spa_log_info(monitor->log, "unregistering endpoint: %s", object_path);
+
+ if (!dbus_connection_unregister_object_path(monitor->conn, object_path))
+ spa_log_warn(monitor->log, "failed to unregister %s\n", object_path);
+
+ free(object_path);
+}
+
+static void unregister_media_application(struct spa_bt_monitor * monitor)
+{
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+
+ for (int i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+ unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
+ }
+
+ dbus_connection_unregister_object_path(monitor->conn, MEDIA_OBJECT_MANAGER_PATH);
+}
+
+static int adapter_register_application(struct spa_bt_adapter *a) {
+ const char *object_manager_path = MEDIA_OBJECT_MANAGER_PATH;
+ struct spa_bt_monitor *monitor = a->monitor;
+ DBusMessage *m;
+ DBusMessageIter i, d;
+ DBusPendingCall *call;
+
+ if (a->application_registered)
+ return 0;
+
+ spa_log_debug(monitor->log, "Registering bluez5 media application on adapter %s", a->path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ a->path,
+ BLUEZ_MEDIA_INTERFACE,
+ "RegisterApplication");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &object_manager_path);
+ dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, "{sv}", &d);
+ dbus_message_iter_close_container(&i, &d);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, bluez_register_application_reply, a, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+}
+
+static int switch_backend(struct spa_bt_monitor *monitor, struct spa_bt_backend *backend)
+{
+ int res;
+ size_t i;
+
+ spa_return_val_if_fail(backend != NULL, -EINVAL);
+
+ if (!backend->available)
+ return -ENODEV;
+
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ struct spa_bt_backend *b = monitor->backends[i];
+ if (backend != b && b && b->available && b->exclusive)
+ spa_log_warn(monitor->log,
+ "%s running, but not configured as HFP/HSP backend: "
+ "it may interfere with HFP/HSP functionality.",
+ b->name);
+ }
+
+ if (monitor->backend == backend)
+ return 0;
+
+ spa_log_info(monitor->log, "Switching to HFP/HSP backend %s", backend->name);
+
+ spa_bt_backend_unregister_profiles(monitor->backend);
+
+ if ((res = spa_bt_backend_register_profiles(backend)) < 0) {
+ monitor->backend = NULL;
+ return res;
+ }
+
+ monitor->backend = backend;
+ return 0;
+}
+
+static void reselect_backend(struct spa_bt_monitor *monitor, bool silent)
+{
+ struct spa_bt_backend *backend;
+ size_t i;
+
+ spa_log_debug(monitor->log, "re-selecting HFP/HSP backend");
+
+ if (monitor->backend_selection == BACKEND_NONE) {
+ spa_bt_backend_unregister_profiles(monitor->backend);
+ monitor->backend = NULL;
+ return;
+ } else if (monitor->backend_selection == BACKEND_ANY) {
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ backend = monitor->backends[i];
+ if (backend && switch_backend(monitor, backend) == 0)
+ return;
+ }
+ } else {
+ backend = monitor->backends[monitor->backend_selection];
+ if (backend && switch_backend(monitor, backend) == 0)
+ return;
+ }
+
+ spa_bt_backend_unregister_profiles(monitor->backend);
+ monitor->backend = NULL;
+
+ if (!silent)
+ spa_log_error(monitor->log, "Failed to start HFP/HSP backend %s",
+ backend ? backend->name : "none");
+}
+
+static int media_update_props(struct spa_bt_monitor *monitor,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ if (spa_streq(key, "SupportedUUIDs")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ if (spa_streq(uuid, SPA_BT_UUID_BAP_SINK)) {
+ monitor->le_audio_supported = true;
+ spa_log_info(monitor->log, "LE Audio supported");
+ }
+ dbus_message_iter_next(&iter);
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "media: unhandled key %s", key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static void interface_added(struct spa_bt_monitor *monitor,
+ DBusConnection *conn,
+ const char *object_path,
+ const char *interface_name,
+ DBusMessageIter *props_iter)
+{
+ spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name);
+
+ if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+
+ a = adapter_find(monitor, object_path);
+ if (a == NULL) {
+ a = adapter_create(monitor, object_path);
+ if (a == NULL) {
+ spa_log_warn(monitor->log, "can't create adapter: %m");
+ return;
+ }
+ }
+ adapter_update_props(a, props_iter, NULL);
+ adapter_register_application(a);
+ adapter_register_player(a);
+ adapter_update_devices(a);
+ }
+ else if (spa_streq(interface_name, BLUEZ_PROFILE_MANAGER_INTERFACE)) {
+ if (monitor->backends[BACKEND_NATIVE])
+ monitor->backends[BACKEND_NATIVE]->available = true;
+ reselect_backend(monitor, false);
+ }
+ else if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+
+ d = spa_bt_device_find(monitor, object_path);
+ if (d == NULL) {
+ d = device_create(monitor, object_path);
+ if (d == NULL) {
+ spa_log_warn(monitor->log, "can't create Bluetooth device %s: %m",
+ object_path);
+ return;
+ }
+ }
+
+ device_update_props(d, props_iter, NULL);
+ d->reconnect_state = BT_DEVICE_RECONNECT_INIT;
+
+ if (!device_props_ready(d))
+ return;
+
+ device_update_hw_volume_profiles(d);
+
+ /* Trigger bluez device creation before bluez profile negotiation started so that
+ * profile connection handlers can receive per-device settings during profile negotiation. */
+ spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL);
+ }
+ else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_device *d;
+
+ ep = remote_endpoint_find(monitor, object_path);
+ if (ep == NULL) {
+ ep = remote_endpoint_create(monitor, object_path);
+ if (ep == NULL) {
+ spa_log_warn(monitor->log, "can't create Bluetooth remote endpoint %s: %m",
+ object_path);
+ return;
+ }
+ }
+ remote_endpoint_update_props(ep, props_iter, NULL);
+
+ d = ep->device;
+ if (d)
+ spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+ }
+ else if (spa_streq(interface_name, BLUEZ_MEDIA_INTERFACE)) {
+ media_update_props(monitor, props_iter, NULL);
+ }
+}
+
+static void interfaces_added(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter)
+{
+ DBusMessageIter it[3];
+ const char *object_path;
+
+ dbus_message_iter_get_basic(arg_iter, &object_path);
+ dbus_message_iter_next(arg_iter);
+ dbus_message_iter_recurse(arg_iter, &it[0]);
+
+ while (dbus_message_iter_get_arg_type(&it[0]) != DBUS_TYPE_INVALID) {
+ const char *interface_name;
+
+ dbus_message_iter_recurse(&it[0], &it[1]);
+ dbus_message_iter_get_basic(&it[1], &interface_name);
+ dbus_message_iter_next(&it[1]);
+ dbus_message_iter_recurse(&it[1], &it[2]);
+
+ interface_added(monitor, monitor->conn,
+ object_path, interface_name,
+ &it[2]);
+
+ dbus_message_iter_next(&it[0]);
+ }
+}
+
+static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter)
+{
+ const char *object_path;
+ DBusMessageIter it;
+
+ dbus_message_iter_get_basic(arg_iter, &object_path);
+ dbus_message_iter_next(arg_iter);
+ dbus_message_iter_recurse(arg_iter, &it);
+
+ while (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_INVALID) {
+ const char *interface_name;
+
+ dbus_message_iter_get_basic(&it, &interface_name);
+
+ spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name);
+
+ if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+ d = spa_bt_device_find(monitor, object_path);
+ if (d != NULL)
+ device_free(d);
+ } else if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+ a = adapter_find(monitor, object_path);
+ if (a != NULL)
+ adapter_free(a);
+ } else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ ep = remote_endpoint_find(monitor, object_path);
+ if (ep != NULL) {
+ struct spa_bt_device *d = ep->device;
+ remote_endpoint_free(ep);
+ if (d)
+ spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+ }
+ }
+
+ dbus_message_iter_next(&it);
+ }
+}
+
+static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusMessage *r;
+ DBusMessageIter it[6];
+
+ spa_assert(pending == monitor->get_managed_objects_call);
+ monitor->get_managed_objects_call = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available");
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "GetManagedObjects() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &it[0]) ||
+ !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) {
+ spa_log_error(monitor->log, "Invalid reply signature for GetManagedObjects()");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ while (dbus_message_iter_get_arg_type(&it[1]) != DBUS_TYPE_INVALID) {
+ dbus_message_iter_recurse(&it[1], &it[2]);
+
+ interfaces_added(monitor, &it[2]);
+
+ dbus_message_iter_next(&it[1]);
+ }
+
+ reselect_backend(monitor, false);
+
+ monitor->objects_listed = true;
+
+finish:
+ dbus_message_unref(r);
+ return;
+}
+
+static void get_managed_objects(struct spa_bt_monitor *monitor)
+{
+ if (monitor->objects_listed || monitor->get_managed_objects_call)
+ return;
+
+ DBusMessage *m;
+ DBusPendingCall *call;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ "/",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects");
+
+ dbus_message_set_auto_start(m, false);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, get_managed_objects_reply, monitor, NULL);
+ dbus_message_unref(m);
+
+ monitor->get_managed_objects_call = call;
+}
+
+static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(monitor->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto fail;
+ }
+
+ if (spa_streq(name, BLUEZ_SERVICE)) {
+ bool has_old_owner = old_owner && *old_owner;
+ bool has_new_owner = new_owner && *new_owner;
+
+ if (has_old_owner) {
+ spa_log_debug(monitor->log, "Bluetooth daemon disappeared");
+
+ if (monitor->backends[BACKEND_NATIVE])
+ monitor->backends[BACKEND_NATIVE]->available = false;
+
+ reselect_backend(monitor, true);
+ }
+
+ if (has_old_owner || has_new_owner) {
+ struct spa_bt_adapter *a;
+ struct spa_bt_device *d;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+
+ monitor->objects_listed = false;
+
+ spa_list_consume(t, &monitor->transport_list, link)
+ spa_bt_transport_free(t);
+ spa_list_consume(ep, &monitor->remote_endpoint_list, link)
+ remote_endpoint_free(ep);
+ spa_list_consume(d, &monitor->device_list, link)
+ device_free(d);
+ spa_list_consume(a, &monitor->adapter_list, link)
+ adapter_free(a);
+ }
+
+ if (has_new_owner) {
+ spa_log_debug(monitor->log, "Bluetooth daemon appeared");
+ get_managed_objects(monitor);
+ }
+ } else if (spa_streq(name, OFONO_SERVICE)) {
+ if (monitor->backends[BACKEND_OFONO])
+ monitor->backends[BACKEND_OFONO]->available = (new_owner && *new_owner);
+ reselect_backend(monitor, false);
+ } else if (spa_streq(name, HSPHFPD_SERVICE)) {
+ if (monitor->backends[BACKEND_HSPHFPD])
+ monitor->backends[BACKEND_HSPHFPD]->available = (new_owner && *new_owner);
+ reselect_backend(monitor, false);
+ }
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded")) {
+ DBusMessageIter it;
+
+ spa_log_debug(monitor->log, "interfaces added %s", dbus_message_get_path(m));
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) {
+ spa_log_error(monitor->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ interfaces_added(monitor, &it);
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesRemoved")) {
+ DBusMessageIter it;
+
+ spa_log_debug(monitor->log, "interfaces removed %s", dbus_message_get_path(m));
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oas")) {
+ spa_log_error(monitor->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ interfaces_removed(monitor, &it);
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.Properties", "PropertiesChanged")) {
+ DBusMessageIter it[2];
+ const char *iface, *path;
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it[0]) ||
+ !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(monitor->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+ path = dbus_message_get_path(m);
+
+ dbus_message_iter_get_basic(&it[0], &iface);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ if (spa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+
+ a = adapter_find(monitor, path);
+ if (a == NULL) {
+ spa_log_warn(monitor->log,
+ "Properties changed in unknown adapter %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in adapter %s", path);
+
+ adapter_update_props(a, &it[1], NULL);
+ }
+ else if (spa_streq(iface, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+
+ d = spa_bt_device_find(monitor, path);
+ if (d == NULL) {
+ spa_log_debug(monitor->log,
+ "Properties changed in unknown device %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in device %s", path);
+
+ device_update_props(d, &it[1], NULL);
+
+ if (!device_props_ready(d))
+ goto finish;
+
+ device_update_hw_volume_profiles(d);
+
+ spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL);
+ }
+ else if (spa_streq(iface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_device *d;
+
+ ep = remote_endpoint_find(monitor, path);
+ if (ep == NULL) {
+ spa_log_debug(monitor->log,
+ "Properties changed in unknown remote endpoint %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in remote endpoint %s", path);
+
+ remote_endpoint_update_props(ep, &it[1], NULL);
+
+ d = ep->device;
+ if (d)
+ spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+ }
+ else if (spa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) {
+ struct spa_bt_transport *transport;
+
+ transport = spa_bt_transport_find(monitor, path);
+ if (transport == NULL) {
+ spa_log_warn(monitor->log,
+ "Properties changed in unknown transport %s", path);
+ goto finish;
+ }
+
+ spa_log_debug(monitor->log, "Properties changed in transport %s", path);
+
+ transport_update_props(transport, &it[1], NULL);
+ }
+ }
+
+fail:
+ dbus_error_free(&err);
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static void add_filters(struct spa_bt_monitor *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" BLUEZ_SERVICE "'", &err);
+#ifdef HAVE_BLUEZ_5_BACKEND_OFONO
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" OFONO_SERVICE "'", &err);
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" HSPHFPD_SERVICE "'", &err);
+#endif
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_ADAPTER_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_DEVICE_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_MEDIA_ENDPOINT_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", &err);
+
+ this->filters_added = true;
+
+ return;
+
+fail:
+ dbus_error_free(&err);
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ struct spa_bt_monitor *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ add_filters(this);
+ get_managed_objects(this);
+
+ struct spa_bt_device *device;
+ spa_list_for_each(device, &this->device_list, link) {
+ if (device->added)
+ emit_device_info(this, device, this->connection_info_supported);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct spa_bt_monitor *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct spa_bt_monitor *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_adapter *a;
+ struct spa_bt_device *d;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct spa_dict_item *it;
+ size_t i;
+
+ monitor = (struct spa_bt_monitor *) handle;
+
+ /*
+ * We don't call BlueZ API unregister methods here, since BlueZ generally does the
+ * unregistration when the DBus connection is closed below. We'll unregister DBus
+ * object managers and filter callbacks though.
+ */
+
+ unregister_media_application(monitor);
+
+ if (monitor->filters_added) {
+ dbus_connection_remove_filter(monitor->conn, filter_cb, monitor);
+ monitor->filters_added = false;
+ }
+
+ if (monitor->get_managed_objects_call) {
+ dbus_pending_call_cancel(monitor->get_managed_objects_call);
+ dbus_pending_call_unref(monitor->get_managed_objects_call);
+ }
+
+ spa_list_consume(t, &monitor->transport_list, link)
+ spa_bt_transport_free(t);
+ spa_list_consume(ep, &monitor->remote_endpoint_list, link)
+ remote_endpoint_free(ep);
+ spa_list_consume(d, &monitor->device_list, link)
+ device_free(d);
+ spa_list_consume(a, &monitor->adapter_list, link)
+ adapter_free(a);
+
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ spa_bt_backend_free(monitor->backends[i]);
+ monitor->backends[i] = NULL;
+ }
+
+ spa_dict_for_each(it, &monitor->global_settings) {
+ free((void *)it->key);
+ free((void *)it->value);
+ }
+
+ free((void*)monitor->enabled_codecs.items);
+ spa_zero(monitor->enabled_codecs);
+
+ dbus_connection_unref(monitor->conn);
+ spa_dbus_connection_destroy(monitor->dbus_connection);
+ monitor->dbus_connection = NULL;
+ monitor->conn = NULL;
+
+ monitor->objects_listed = false;
+
+ monitor->connection_info_supported = false;
+
+ monitor->backend = NULL;
+ monitor->backend_selection = BACKEND_NATIVE;
+
+ spa_bt_quirks_destroy(monitor->quirks);
+
+ free_media_codecs(monitor->media_codecs);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct spa_bt_monitor);
+}
+
+int spa_bt_profiles_from_json_array(const char *str)
+{
+ struct spa_json it, it_array;
+ char role_name[256];
+ enum spa_bt_profile profiles = SPA_BT_PROFILE_NULL;
+
+ spa_json_init(&it, str, strlen(str));
+
+ if (spa_json_enter_array(&it, &it_array) <= 0)
+ return -EINVAL;
+
+ while (spa_json_get_string(&it_array, role_name, sizeof(role_name)) > 0) {
+ if (spa_streq(role_name, "hsp_hs")) {
+ profiles |= SPA_BT_PROFILE_HSP_HS;
+ } else if (spa_streq(role_name, "hsp_ag")) {
+ profiles |= SPA_BT_PROFILE_HSP_AG;
+ } else if (spa_streq(role_name, "hfp_hf")) {
+ profiles |= SPA_BT_PROFILE_HFP_HF;
+ } else if (spa_streq(role_name, "hfp_ag")) {
+ profiles |= SPA_BT_PROFILE_HFP_AG;
+ } else if (spa_streq(role_name, "a2dp_sink")) {
+ profiles |= SPA_BT_PROFILE_A2DP_SINK;
+ } else if (spa_streq(role_name, "a2dp_source")) {
+ profiles |= SPA_BT_PROFILE_A2DP_SOURCE;
+ } else if (spa_streq(role_name, "bap_sink")) {
+ profiles |= SPA_BT_PROFILE_BAP_SINK;
+ } else if (spa_streq(role_name, "bap_source")) {
+ profiles |= SPA_BT_PROFILE_BAP_SOURCE;
+ }
+ }
+
+ return profiles;
+}
+
+static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict *info)
+{
+ const struct media_codec * const * const media_codecs = this->media_codecs;
+ const char *str;
+ struct spa_dict_item *codecs;
+ struct spa_json it, it_array;
+ char codec_name[256];
+ size_t num_codecs;
+ int i;
+
+ /* Parse bluez5.codecs property to a dict of enabled codecs */
+
+ num_codecs = 0;
+ while (media_codecs[num_codecs])
+ ++num_codecs;
+
+ codecs = calloc(num_codecs, sizeof(struct spa_dict_item));
+ if (codecs == NULL)
+ return -ENOMEM;
+
+ if (info == NULL || (str = spa_dict_lookup(info, "bluez5.codecs")) == NULL)
+ goto fallback;
+
+ spa_json_init(&it, str, strlen(str));
+
+ if (spa_json_enter_array(&it, &it_array) <= 0) {
+ spa_log_error(this->log, "property bluez5.codecs '%s' is not an array", str);
+ goto fallback;
+ }
+
+ this->enabled_codecs = SPA_DICT_INIT(codecs, 0);
+
+ while (spa_json_get_string(&it_array, codec_name, sizeof(codec_name)) > 0) {
+ int i;
+
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+
+ if (!spa_streq(codec->name, codec_name))
+ continue;
+
+ if (spa_dict_lookup_item(&this->enabled_codecs, codec->name) != NULL)
+ continue;
+
+ spa_log_debug(this->log, "enabling codec %s", codec->name);
+
+ spa_assert(this->enabled_codecs.n_items < num_codecs);
+
+ codecs[this->enabled_codecs.n_items].key = codec->name;
+ codecs[this->enabled_codecs.n_items].value = "true";
+ ++this->enabled_codecs.n_items;
+
+ break;
+ }
+ }
+
+ spa_dict_qsort(&this->enabled_codecs);
+
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+ if (!is_media_codec_enabled(this, codec))
+ spa_log_debug(this->log, "disabling codec %s", codec->name);
+ }
+ return 0;
+
+fallback:
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+ spa_log_debug(this->log, "enabling codec %s", codec->name);
+ codecs[i].key = codec->name;
+ codecs[i].value = "true";
+ }
+ this->enabled_codecs = SPA_DICT_INIT(codecs, i);
+ spa_dict_qsort(&this->enabled_codecs);
+ return 0;
+}
+
+static void get_global_settings(struct spa_bt_monitor *this, const struct spa_dict *dict)
+{
+ uint32_t n_items = 0;
+ uint32_t i;
+
+ if (dict == NULL) {
+ this->global_settings = SPA_DICT_INIT(this->global_setting_items, 0);
+ return;
+ }
+
+ for (i = 0; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->global_setting_items); i++) {
+ const struct spa_dict_item *it = &dict->items[i];
+ if (spa_strstartswith(it->key, "bluez5.") && it->value != NULL)
+ this->global_setting_items[n_items++] =
+ SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
+ }
+
+ this->global_settings = SPA_DICT_INIT(this->global_setting_items, n_items);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct spa_bt_monitor *this;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct spa_bt_monitor *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ this->plugin_loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->dbus == NULL) {
+ spa_log_error(this->log, "a dbus is needed");
+ return -EINVAL;
+ }
+
+ if (this->plugin_loader == NULL) {
+ spa_log_error(this->log, "a plugin loader is needed");
+ return -EINVAL;
+ }
+
+ this->media_codecs = NULL;
+ this->quirks = NULL;
+ this->conn = NULL;
+ this->dbus_connection = NULL;
+
+ this->media_codecs = load_media_codecs(this->plugin_loader, this->log);
+ if (this->media_codecs == NULL) {
+ spa_log_error(this->log, "failed to load required media codec plugins");
+ res = -EIO;
+ goto fail;
+ }
+
+ this->quirks = spa_bt_quirks_create(info, this->log);
+ if (this->quirks == NULL) {
+ spa_log_error(this->log, "failed to parse quirk table");
+ res = -EINVAL;
+ goto fail;
+ }
+
+ this->dbus_connection = spa_dbus_get_connection(this->dbus, SPA_DBUS_TYPE_SYSTEM);
+ if (this->dbus_connection == NULL) {
+ spa_log_error(this->log, "no dbus connection");
+ res = -EIO;
+ goto fail;
+ }
+ this->conn = spa_dbus_connection_get(this->dbus_connection);
+ if (this->conn == NULL) {
+ spa_log_error(this->log, "failed to get dbus connection");
+ res = -EIO;
+ goto fail;
+ }
+
+ /* XXX: We should handle spa_dbus reconnecting, but we don't, so ref
+ * XXX: the handle so that we can keep it if spa_dbus unrefs it.
+ */
+ dbus_connection_ref(this->conn);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ spa_list_init(&this->adapter_list);
+ spa_list_init(&this->device_list);
+ spa_list_init(&this->remote_endpoint_list);
+ spa_list_init(&this->transport_list);
+
+ if ((res = parse_codec_array(this, info)) < 0)
+ goto fail;
+
+ this->default_audio_info.rate = A2DP_CODEC_DEFAULT_RATE;
+ this->default_audio_info.channels = A2DP_CODEC_DEFAULT_CHANNELS;
+
+ this->backend_selection = BACKEND_NATIVE;
+
+ get_global_settings(this, info);
+
+ if (info) {
+ const char *str;
+ uint32_t tmp;
+
+ if ((str = spa_dict_lookup(info, "api.bluez5.connection-info")) != NULL &&
+ spa_atob(str))
+ this->connection_info_supported = true;
+
+ if ((str = spa_dict_lookup(info, "bluez5.default.rate")) != NULL &&
+ (tmp = atoi(str)) > 0)
+ this->default_audio_info.rate = tmp;
+
+ if ((str = spa_dict_lookup(info, "bluez5.default.channels")) != NULL &&
+ ((tmp = atoi(str)) > 0))
+ this->default_audio_info.channels = tmp;
+
+ if ((str = spa_dict_lookup(info, "bluez5.hfphsp-backend")) != NULL) {
+ if (spa_streq(str, "none"))
+ this->backend_selection = BACKEND_NONE;
+ else if (spa_streq(str, "any"))
+ this->backend_selection = BACKEND_ANY;
+ else if (spa_streq(str, "ofono"))
+ this->backend_selection = BACKEND_OFONO;
+ else if (spa_streq(str, "hsphfpd"))
+ this->backend_selection = BACKEND_HSPHFPD;
+ else if (spa_streq(str, "native"))
+ this->backend_selection = BACKEND_NATIVE;
+ }
+
+ if ((str = spa_dict_lookup(info, "bluez5.dummy-avrcp-player")) != NULL)
+ this->dummy_avrcp_player = spa_atob(str);
+ else
+ this->dummy_avrcp_player = false;
+ }
+
+ register_media_application(this);
+
+ /* Create backends. They're started after we get a reply from Bluez. */
+ this->backends[BACKEND_NATIVE] = backend_native_new(this, this->conn, info, this->quirks, support, n_support);
+ this->backends[BACKEND_OFONO] = backend_ofono_new(this, this->conn, info, this->quirks, support, n_support);
+ this->backends[BACKEND_HSPHFPD] = backend_hsphfpd_new(this, this->conn, info, this->quirks, support, n_support);
+
+ return 0;
+
+fail:
+ if (this->media_codecs)
+ free_media_codecs(this->media_codecs);
+ if (this->quirks)
+ spa_bt_quirks_destroy(this->quirks);
+ if (this->conn)
+ dbus_connection_unref(this->conn);
+ if (this->dbus_connection)
+ spa_dbus_connection_destroy(this->dbus_connection);
+ this->media_codecs = NULL;
+ this->quirks = NULL;
+ this->conn = NULL;
+ this->dbus_connection = NULL;
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+const struct spa_handle_factory spa_bluez5_dbus_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_ENUM_DBUS,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+// Report battery percentage to BlueZ using experimental (BlueZ 5.56) Battery Provider API. No-op if no changes occurred.
+int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage)
+{
+ if (percentage == SPA_BT_NO_BATTERY) {
+ battery_remove(device);
+ return 0;
+ }
+
+ // BlueZ likely is running without battery provider support, don't try to report battery
+ if (device->adapter->battery_provider_unavailable) return 0;
+
+ // If everything is initialized and battery level has not changed we don't need to send anything to BlueZ
+ if (device->adapter->has_battery_provider && device->has_battery && device->battery == percentage) return 1;
+
+ device->battery = percentage;
+
+ if (!device->adapter->has_battery_provider) {
+ // No provider: register it, create battery when registered
+ register_battery_provider(device);
+ } else if (!device->has_battery) {
+ // Have provider but no battery: create battery with correct percentage
+ battery_create(device);
+ } else {
+ // Just update existing battery percentage
+ battery_update(device);
+ }
+
+ return 1;
+}
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
new file mode 100644
index 0000000..dcdfeaf
--- /dev/null
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -0,0 +1,2398 @@
+/* Spa Bluez5 Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <errno.h>
+
+#include <spa/support/log.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/support/i18n.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/monitor/event.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/bluetooth/audio.h>
+#include <spa/param/bluetooth/type-info.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "defs.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.device");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define MAX_DEVICES 64
+
+#define DEVICE_ID_SOURCE 0
+#define DEVICE_ID_SINK 1
+#define DYNAMIC_NODE_ID_FLAG 0x1000
+
+static struct spa_i18n *_i18n;
+
+#define _(_str) spa_i18n_text(_i18n,(_str))
+#define N_(_str) (_str)
+
+enum {
+ DEVICE_PROFILE_OFF = 0,
+ DEVICE_PROFILE_AG = 1,
+ DEVICE_PROFILE_A2DP = 2,
+ DEVICE_PROFILE_HSP_HFP = 3,
+ DEVICE_PROFILE_BAP = 4,
+ DEVICE_PROFILE_LAST = DEVICE_PROFILE_BAP,
+};
+
+struct props {
+ enum spa_bluetooth_audio_codec codec;
+ bool offload_active;
+};
+
+static void reset_props(struct props *props)
+{
+ props->codec = 0;
+ props->offload_active = false;
+}
+
+struct impl;
+
+struct node {
+ struct impl *impl;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ uint32_t id;
+ unsigned int active:1;
+ unsigned int mute:1;
+ unsigned int save:1;
+ unsigned int a2dp_duplex:1;
+ unsigned int offload_acquired:1;
+ uint32_t n_channels;
+ int64_t latency_offset;
+ uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ float soft_volumes[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct dynamic_node
+{
+ struct impl *impl;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ uint32_t id;
+ const char *factory_name;
+ bool a2dp_duplex;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ uint32_t info_all;
+ struct spa_device_info info;
+#define IDX_EnumProfile 0
+#define IDX_Profile 1
+#define IDX_EnumRoute 2
+#define IDX_Route 3
+#define IDX_PropInfo 4
+#define IDX_Props 5
+ struct spa_param_info params[6];
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+
+ struct spa_bt_device *bt_dev;
+ struct spa_hook bt_dev_listener;
+
+ uint32_t profile;
+ unsigned int switching_codec:1;
+ unsigned int save_profile:1;
+ uint32_t prev_bt_connected_profiles;
+
+ const struct media_codec **supported_codecs;
+ size_t supported_codec_count;
+
+ struct dynamic_node dyn_media_source;
+ struct dynamic_node dyn_media_sink;
+ struct dynamic_node dyn_sco_source;
+ struct dynamic_node dyn_sco_sink;
+
+#define MAX_SETTINGS 32
+ struct spa_dict_item setting_items[MAX_SETTINGS];
+ struct spa_dict setting_dict;
+
+ struct node nodes[2];
+};
+
+static void init_node(struct impl *this, struct node *node, uint32_t id)
+{
+ uint32_t i;
+
+ spa_zero(*node);
+ node->id = id;
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
+ node->volumes[i] = 1.0f;
+ node->soft_volumes[i] = 1.0f;
+ }
+}
+
+static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size)
+{
+ const struct media_codec * const *c;
+
+ spa_assert(size > 0);
+ spa_assert(this->supported_codecs);
+
+ for (c = this->supported_codecs; *c && size > 1; ++c) {
+ if ((*c)->id == id || id == 0) {
+ *codecs++ = *c;
+ --size;
+ }
+ }
+
+ *codecs = NULL;
+}
+
+static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, size_t *idx)
+{
+ const struct media_codec *media_codec = NULL;
+ size_t i;
+ for (i = 0; i < this->supported_codec_count; ++i) {
+ if (this->supported_codecs[i]->id == id) {
+ media_codec = this->supported_codecs[i];
+ if (idx)
+ *idx = i;
+ }
+ }
+ return media_codec;
+}
+
+static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id)
+{
+ switch (id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_CVSD:
+ return HFP_AUDIO_CODEC_CVSD;
+ case SPA_BLUETOOTH_AUDIO_CODEC_MSBC:
+ return HFP_AUDIO_CODEC_MSBC;
+ default:
+ return 0;
+ }
+}
+
+static enum spa_bluetooth_audio_codec get_hfp_codec_id(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return SPA_BLUETOOTH_AUDIO_CODEC_MSBC;
+ case HFP_AUDIO_CODEC_CVSD:
+ return SPA_BLUETOOTH_AUDIO_CODEC_CVSD;
+ }
+ return SPA_ID_INVALID;
+}
+
+static const char *get_hfp_codec_description(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return "mSBC";
+ case HFP_AUDIO_CODEC_CVSD:
+ return "CVSD";
+ }
+ return "unknown";
+}
+
+static const char *get_hfp_codec_name(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return "msbc";
+ case HFP_AUDIO_CODEC_CVSD:
+ return "cvsd";
+ }
+ return "unknown";
+}
+
+static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex)
+{
+ if (t->media_codec != NULL) {
+ if (a2dp_duplex && t->media_codec->duplex_codec)
+ return t->media_codec->duplex_codec->name;
+ return t->media_codec->name;
+ }
+ return get_hfp_codec_name(t->codec);
+}
+
+static void transport_destroy(void *userdata)
+{
+ struct node *node = userdata;
+ node->transport = NULL;
+}
+
+static void emit_node_props(struct impl *this, struct node *node, bool full)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, node->n_channels, node->volumes),
+ SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, node->n_channels, node->soft_volumes),
+ SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, node->n_channels, node->channels));
+ if (full) {
+ spa_pod_builder_add(&b,
+ SPA_PROP_mute, SPA_POD_Bool(node->mute),
+ SPA_PROP_softMute, SPA_POD_Bool(node->mute),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(node->latency_offset),
+ 0);
+ }
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void emit_volume(struct impl *this, struct node *node)
+{
+ emit_node_props(this, node, false);
+}
+
+static void emit_info(struct impl *this, bool full);
+
+static float get_soft_volume_boost(struct node *node)
+{
+ const struct media_codec *codec = node->transport ? node->transport->media_codec : NULL;
+
+ /*
+ * For A2DP duplex, the duplex microphone channel sometimes does not appear
+ * to have hardware gain, and input volume is very low.
+ *
+ * Work around this by boosting the software volume level, i.e. adjust
+ * the scale on the user-visible volume control to something more sensible.
+ * If this causes clipping, the user can just reduce the mic volume to
+ * bring SW gain below 1.
+ */
+ if (node->a2dp_duplex && node->transport && codec && codec->info &&
+ spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) &&
+ node->id == DEVICE_ID_SOURCE &&
+ !node->transport->volumes[SPA_BT_VOLUME_ID_RX].active)
+ return 10.0f; /* 20 dB boost */
+
+ /* In all other cases, no boost */
+ return 1.0f;
+}
+
+static float node_get_hw_volume(struct node *node)
+{
+ uint32_t i;
+ float hw_volume = 0.0f;
+ for (i = 0; i < node->n_channels; i++)
+ hw_volume = SPA_MAX(node->volumes[i], hw_volume);
+ return SPA_MIN(hw_volume, 1.0f);
+}
+
+static void node_update_soft_volumes(struct node *node, float hw_volume)
+{
+ for (uint32_t i = 0; i < node->n_channels; ++i) {
+ node->soft_volumes[i] = hw_volume > 0.0f
+ ? node->volumes[i] / hw_volume
+ : 0.0f;
+ }
+}
+
+static bool node_update_volume_from_transport(struct node *node, bool reset)
+{
+ struct impl *impl = node->impl;
+ struct spa_bt_transport_volume *t_volume;
+ float prev_hw_volume;
+
+ if (!node->transport || !spa_bt_transport_volume_enabled(node->transport))
+ return false;
+
+ /* PW is the controller for remote device. */
+ if (impl->profile != DEVICE_PROFILE_A2DP
+ && impl->profile != DEVICE_PROFILE_BAP
+ && impl->profile != DEVICE_PROFILE_HSP_HFP)
+ return false;
+
+ t_volume = &node->transport->volumes[node->id];
+
+ if (!t_volume->active)
+ return false;
+
+ prev_hw_volume = node_get_hw_volume(node);
+
+ if (!reset) {
+ for (uint32_t i = 0; i < node->n_channels; ++i) {
+ node->volumes[i] = prev_hw_volume > 0.0f
+ ? node->volumes[i] * t_volume->volume / prev_hw_volume
+ : t_volume->volume;
+ }
+ } else {
+ for (uint32_t i = 0; i < node->n_channels; ++i)
+ node->volumes[i] = t_volume->volume;
+ }
+
+ node_update_soft_volumes(node, t_volume->volume);
+
+ /*
+ * Consider volume changes from the headset as requested
+ * by the user, and to be saved by the SM.
+ */
+ node->save = true;
+
+ return true;
+}
+
+static void volume_changed(void *userdata)
+{
+ struct node *node = userdata;
+ struct impl *impl = node->impl;
+
+ if (!node_update_volume_from_transport(node, false))
+ return;
+
+ emit_volume(impl, node);
+
+ impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ impl->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(impl, false);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .destroy = transport_destroy,
+ .volume_changed = volume_changed,
+};
+
+static int node_offload_set_active(struct node *node, bool active)
+{
+ int res = 0;
+
+ if (node->transport == NULL || !node->active)
+ return -ENOTSUP;
+
+ if (active && !node->offload_acquired)
+ res = spa_bt_transport_acquire(node->transport, false);
+ else if (!active && node->offload_acquired)
+ res = spa_bt_transport_release(node->transport);
+
+ if (res >= 0)
+ node->offload_acquired = active;
+
+ return res;
+}
+
+static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels)
+{
+ const struct media_codec *codec;
+ struct spa_audio_info info = { 0 };
+
+ if (!a2dp_duplex || !t->media_codec || !t->media_codec->duplex_codec) {
+ *n_channels = t->n_channels;
+ memcpy(channels, t->channels, t->n_channels * sizeof(uint32_t));
+ return;
+ }
+
+ codec = t->media_codec->duplex_codec;
+
+ if (!codec->validate_config ||
+ codec->validate_config(codec, 0,
+ t->configuration, t->configuration_len,
+ &info) < 0) {
+ *n_channels = 1;
+ channels[0] = SPA_AUDIO_CHANNEL_MONO;
+ return;
+ }
+
+ *n_channels = info.info.raw.channels;
+ memcpy(channels, info.info.raw.position,
+ info.info.raw.channels * sizeof(uint32_t));
+}
+
+static void emit_node(struct impl *this, struct spa_bt_transport *t,
+ uint32_t id, const char *factory_name, bool a2dp_duplex)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_device_object_info info;
+ struct spa_dict_item items[8];
+ uint32_t n_items = 0;
+ char transport[32], str_id[32];
+ bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG);
+
+ snprintf(transport, sizeof(transport), "pointer:%p", t);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PROFILE, spa_bt_profile_name(t->profile));
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CODEC, get_codec_name(t, a2dp_duplex));
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
+ items[4] = SPA_DICT_ITEM_INIT("device.routes", "1");
+ n_items = 5;
+ if (!is_dyn_node) {
+ snprintf(str_id, sizeof(str_id), "%d", id);
+ items[5] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
+ n_items++;
+ }
+ if (spa_streq(spa_bt_profile_name(t->profile), "headset-head-unit")) {
+ items[n_items] = SPA_DICT_ITEM_INIT("device.intended-roles", "Communication");
+ n_items++;
+ }
+ if (a2dp_duplex) {
+ items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.a2dp-duplex", "true");
+ n_items++;
+ }
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+ info.factory_name = factory_name;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ if (!is_dyn_node) {
+ uint32_t prev_channels = this->nodes[id].n_channels;
+ float boost;
+
+ this->nodes[id].impl = this;
+ this->nodes[id].active = true;
+ this->nodes[id].offload_acquired = false;
+ this->nodes[id].a2dp_duplex = a2dp_duplex;
+ get_channels(t, a2dp_duplex, &this->nodes[id].n_channels, this->nodes[id].channels);
+ if (this->nodes[id].transport)
+ spa_hook_remove(&this->nodes[id].transport_listener);
+ this->nodes[id].transport = t;
+ spa_bt_transport_add_listener(t, &this->nodes[id].transport_listener, &transport_events, &this->nodes[id]);
+
+ if (prev_channels > 0) {
+ size_t i;
+ /*
+ * Spread mono volume to all channels, if we had switched HFP -> A2DP.
+ * XXX: we should also use different route for hfp and a2dp
+ */
+ for (i = prev_channels; i < this->nodes[id].n_channels; ++i)
+ this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels];
+ }
+
+ node_update_volume_from_transport(&this->nodes[id], true);
+
+ boost = get_soft_volume_boost(&this->nodes[id]);
+ if (boost != 1.0f) {
+ size_t i;
+ for (i = 0; i < this->nodes[id].n_channels; ++i)
+ this->nodes[id].soft_volumes[i] = this->nodes[id].volumes[i] * boost;
+ }
+
+ emit_node_props(this, &this->nodes[id], true);
+ }
+}
+
+static struct spa_bt_transport *find_transport(struct impl *this, int profile, enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_bt_transport *t;
+
+ spa_list_for_each(t, &device->transport_list, device_link) {
+ bool codec_ok = codec == 0 ||
+ (t->media_codec != NULL && t->media_codec->id == codec) ||
+ get_hfp_codec_id(t->codec) == codec;
+
+ if ((t->profile & device->connected_profiles) &&
+ (t->profile & profile) == t->profile &&
+ codec_ok)
+ return t;
+ }
+
+ return NULL;
+}
+
+static void dynamic_node_transport_destroy(void *data)
+{
+ struct dynamic_node *this = data;
+ spa_log_debug(this->impl->log, "transport %p destroy", this->transport);
+ this->transport = NULL;
+}
+
+static void dynamic_node_transport_state_changed(void *data,
+ enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state)
+{
+ struct dynamic_node *this = data;
+ struct impl *impl = this->impl;
+ struct spa_bt_transport *t = this->transport;
+
+ spa_log_debug(impl->log, "transport %p state %d->%d", t, old, state);
+
+ if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING) {
+ if (!SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
+ SPA_FLAG_SET(this->id, DYNAMIC_NODE_ID_FLAG);
+ spa_bt_transport_keepalive(t, true);
+ emit_node(impl, t, this->id, this->factory_name, this->a2dp_duplex);
+ }
+ } else if (state < SPA_BT_TRANSPORT_STATE_PENDING && old >= SPA_BT_TRANSPORT_STATE_PENDING) {
+ if (SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
+ SPA_FLAG_CLEAR(this->id, DYNAMIC_NODE_ID_FLAG);
+ spa_bt_transport_keepalive(t, false);
+ spa_device_emit_object_info(&impl->hooks, this->id, NULL);
+ }
+ }
+}
+
+static void dynamic_node_volume_changed(void *data)
+{
+ struct dynamic_node *node = data;
+ struct impl *impl = node->impl;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ struct spa_bt_transport_volume *t_volume;
+ int id = node->id, volume_id;
+
+ SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
+
+ /* Remote device is the controller */
+ if (!node->transport || impl->profile != DEVICE_PROFILE_AG
+ || !spa_bt_transport_volume_enabled(node->transport))
+ return;
+
+ if (id == 0 || id == 2)
+ volume_id = SPA_BT_VOLUME_ID_RX;
+ else if (id == 1)
+ volume_id = SPA_BT_VOLUME_ID_TX;
+ else
+ return;
+
+ t_volume = &node->transport->volumes[volume_id];
+ if (!t_volume->active)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_volume, SPA_POD_Float(t_volume->volume));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_log_debug(impl->log, "dynamic node %p: volume %d changed %f, profile %d",
+ node, volume_id, t_volume->volume, node->transport->profile);
+
+ /* Dynamic node doesn't has route, we can only set volume on adaptar node. */
+ spa_device_emit_event(&impl->hooks, event);
+}
+
+static const struct spa_bt_transport_events dynamic_node_transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = dynamic_node_transport_destroy,
+ .state_changed = dynamic_node_transport_state_changed,
+ .volume_changed = dynamic_node_volume_changed,
+};
+
+static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl,
+ struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex)
+{
+ spa_log_debug(impl->log, "dynamic node, transport: %p->%p id: %08x->%08x",
+ this->transport, t, this->id, id);
+
+ if (this->transport) {
+ /* Session manager don't really handles transport ptr changing. */
+ spa_assert(this->transport == t);
+ spa_hook_remove(&this->transport_listener);
+ }
+
+ this->impl = impl;
+ this->transport = t;
+ this->id = id;
+ this->factory_name = factory_name;
+ this->a2dp_duplex = a2dp_duplex;
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &dynamic_node_transport_events, this);
+
+ /* emits the node if the state is already pending */
+ dynamic_node_transport_state_changed (this, SPA_BT_TRANSPORT_STATE_IDLE, t->state);
+}
+
+static void remove_dynamic_node(struct dynamic_node *this)
+{
+ if (this->transport == NULL)
+ return;
+
+ /* destroy the node, if it exists */
+ dynamic_node_transport_state_changed (this, this->transport->state,
+ SPA_BT_TRANSPORT_STATE_IDLE);
+
+ spa_hook_remove(&this->transport_listener);
+ this->impl = NULL;
+ this->transport = NULL;
+ this->id = 0;
+ this->factory_name = NULL;
+}
+
+static int emit_nodes(struct impl *this)
+{
+ struct spa_bt_transport *t;
+
+ switch (this->profile) {
+ case DEVICE_PROFILE_OFF:
+ break;
+ case DEVICE_PROFILE_AG:
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ t = find_transport(this, SPA_BT_PROFILE_HFP_AG, 0);
+ if (!t)
+ t = find_transport(this, SPA_BT_PROFILE_HSP_AG, 0);
+ if (t) {
+ if (t->profile == SPA_BT_PROFILE_HSP_AG)
+ this->props.codec = 0;
+ else
+ this->props.codec = get_hfp_codec_id(t->codec);
+ emit_dynamic_node(&this->dyn_sco_source, this, t,
+ 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
+ emit_dynamic_node(&this->dyn_sco_sink, this, t,
+ 1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
+ }
+ }
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) {
+ t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_dynamic_node(&this->dyn_media_sink, this, t,
+ 3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
+ }
+ }
+ }
+ break;
+ case DEVICE_PROFILE_A2DP:
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
+ t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_node(this, t,
+ DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
+ }
+ }
+ }
+
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
+ t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->props.codec);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_node(this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, true);
+ }
+ }
+ }
+
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ break;
+ case DEVICE_PROFILE_BAP:
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) {
+ t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ if (t->bap_initiator)
+ emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+ else
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+ }
+ }
+
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) {
+ t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ if (t->bap_initiator)
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+ else
+ emit_dynamic_node(&this->dyn_media_sink, this, t,
+ DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+ }
+ }
+
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) {
+ t = find_transport(this, SPA_BT_PROFILE_HFP_HF, this->props.codec);
+ if (!t)
+ t = find_transport(this, SPA_BT_PROFILE_HSP_HS, 0);
+ if (t) {
+ if (t->profile == SPA_BT_PROFILE_HSP_HS)
+ this->props.codec = 0;
+ else
+ this->props.codec = get_hfp_codec_id(t->codec);
+ emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
+ }
+ }
+
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
+ this->props.codec = 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_DEVICE_BUS, "bluetooth" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Device" },
+};
+
+static void emit_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(info_items);
+
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_remove_nodes(struct impl *this)
+{
+ remove_dynamic_node (&this->dyn_media_source);
+ remove_dynamic_node (&this->dyn_media_sink);
+ remove_dynamic_node (&this->dyn_sco_source);
+ remove_dynamic_node (&this->dyn_sco_sink);
+
+ for (uint32_t i = 0; i < 2; i++) {
+ struct node * node = &this->nodes[i];
+ node_offload_set_active(node, false);
+ if (node->transport) {
+ spa_hook_remove(&node->transport_listener);
+ node->transport = NULL;
+ }
+ if (node->active) {
+ spa_device_emit_object_info(&this->hooks, i, NULL);
+ node->active = false;
+ }
+ }
+
+ this->props.offload_active = false;
+}
+
+static bool validate_profile(struct impl *this, uint32_t profile,
+ enum spa_bluetooth_audio_codec codec);
+
+static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec, bool save)
+{
+ if (!validate_profile(this, profile, codec)) {
+ spa_log_warn(this->log, "trying to set invalid profile %d, codec %d, %08x %08x",
+ profile, codec,
+ this->bt_dev->profiles, this->bt_dev->connected_profiles);
+ return -EINVAL;
+ }
+
+ this->save_profile = save;
+
+ if (this->profile == profile &&
+ (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) &&
+ (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) &&
+ (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec))
+ return 0;
+
+ emit_remove_nodes(this);
+
+ spa_bt_device_release_transports(this->bt_dev);
+
+ this->profile = profile;
+ this->prev_bt_connected_profiles = this->bt_dev->connected_profiles;
+ this->props.codec = codec;
+
+ /*
+ * A2DP/BAP: ensure there's a transport with the selected codec (0 means any).
+ * Don't try to switch codecs when the device is in the A2DP source role, since
+ * devices do not appear to like that.
+ */
+ if ((profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
+ && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
+ int ret;
+ const struct media_codec *codecs[64];
+
+ get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs));
+
+ this->switching_codec = true;
+
+ ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs);
+ if (ret < 0) {
+ if (ret != -ENOTSUP)
+ spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
+ } else {
+ return 0;
+ }
+ } else if (profile == DEVICE_PROFILE_HSP_HFP && get_hfp_codec(codec) && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) {
+ int ret;
+
+ this->switching_codec = true;
+
+ ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, get_hfp_codec(codec));
+ if (ret < 0) {
+ if (ret != -ENOTSUP)
+ spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
+ } else {
+ return 0;
+ }
+ }
+
+ this->switching_codec = false;
+ this->props.codec = 0;
+ emit_nodes(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+
+ return 0;
+}
+
+static void codec_switched(void *userdata, int status)
+{
+ struct impl *this = userdata;
+
+ spa_log_debug(this->log, "codec switched (status %d)", status);
+
+ this->switching_codec = false;
+
+ if (status < 0) {
+ /* Failed to switch: return to a fallback profile */
+ spa_log_error(this->log, "failed to switch codec (%d), setting fallback profile", status);
+ if (this->profile == DEVICE_PROFILE_A2DP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else if (this->profile == DEVICE_PROFILE_BAP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else {
+ this->profile = DEVICE_PROFILE_OFF;
+ }
+ }
+
+ emit_remove_nodes(this);
+ emit_nodes(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ if (this->prev_bt_connected_profiles != this->bt_dev->connected_profiles)
+ this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+}
+
+static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t prev_connected_profiles)
+{
+ struct impl *this = userdata;
+ uint32_t connected_change;
+ bool nodes_changed = false;
+
+ connected_change = (this->bt_dev->connected_profiles ^ prev_connected_profiles);
+
+ /* Profiles changed. We have to re-emit device information. */
+ spa_log_info(this->log, "profiles changed to %08x %08x (prev %08x %08x, change %08x)"
+ " switching_codec:%d",
+ this->bt_dev->profiles, this->bt_dev->connected_profiles,
+ prev_profiles, prev_connected_profiles, connected_change,
+ this->switching_codec);
+
+ if (this->switching_codec)
+ return;
+
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK) {
+ free(this->supported_codecs);
+ this->supported_codecs = spa_bt_device_get_supported_media_codecs(
+ this->bt_dev, &this->supported_codec_count, true);
+ }
+
+ switch (this->profile) {
+ case DEVICE_PROFILE_OFF:
+ /* Noop */
+ nodes_changed = false;
+ break;
+ case DEVICE_PROFILE_AG:
+ nodes_changed = (connected_change & (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY |
+ SPA_BT_PROFILE_MEDIA_SOURCE));
+ spa_log_debug(this->log, "profiles changed: AG nodes changed: %d",
+ nodes_changed);
+ break;
+ case DEVICE_PROFILE_A2DP:
+ case DEVICE_PROFILE_BAP:
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ nodes_changed = (connected_change & (SPA_BT_PROFILE_MEDIA_SINK |
+ SPA_BT_PROFILE_MEDIA_SOURCE));
+ spa_log_debug(this->log, "profiles changed: media nodes changed: %d",
+ nodes_changed);
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
+ this->props.codec = 0;
+ nodes_changed = (connected_change & SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
+ spa_log_debug(this->log, "profiles changed: HSP/HFP nodes changed: %d",
+ nodes_changed);
+ break;
+ }
+
+ if (nodes_changed) {
+ emit_remove_nodes(this);
+ emit_nodes(this);
+ }
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; /* Profile changes may affect routes */
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+}
+
+static void set_initial_profile(struct impl *this);
+
+static void device_connected(void *userdata, bool connected) {
+ struct impl *this = userdata;
+
+ spa_log_debug(this->log, "connected: %d", connected);
+
+ if (connected ^ (this->profile != DEVICE_PROFILE_OFF))
+ set_initial_profile(this);
+}
+
+static const struct spa_bt_device_events bt_dev_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .connected = device_connected,
+ .codec_switched = codec_switched,
+ .profiles_changed = profiles_changed,
+};
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info)
+ emit_info(this, true);
+
+ if (events->object_info)
+ emit_nodes(this);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ uint32_t mask;
+ bool have_output = false, have_input = false;
+ const struct media_codec *media_codec;
+
+ switch (index) {
+ case DEVICE_PROFILE_A2DP:
+ case DEVICE_PROFILE_BAP:
+ if (device->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK)
+ have_output = true;
+
+ media_codec = get_supported_media_codec(this, codec, NULL);
+ if (media_codec && media_codec->duplex_codec)
+ have_input = true;
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ have_output = have_input = true;
+ break;
+ default:
+ break;
+ }
+
+ mask = 0;
+ if (have_output)
+ mask |= 1 << SPA_DIRECTION_OUTPUT;
+ if (have_input)
+ mask |= 1 << SPA_DIRECTION_INPUT;
+ return mask;
+}
+
+static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, enum spa_bluetooth_audio_codec *codec)
+{
+ /*
+ * XXX: The codecs should probably become a separate param, and not have
+ * XXX: separate profiles for each one.
+ */
+
+ *codec = 0;
+ *next = index + 1;
+
+ if (index <= DEVICE_PROFILE_LAST) {
+ return index;
+ } else if (index != SPA_ID_INVALID) {
+ const struct spa_type_info *info;
+ uint32_t profile;
+
+ *codec = index - DEVICE_PROFILE_LAST;
+ *next = SPA_ID_INVALID;
+
+ for (info = spa_type_bluetooth_audio_codec; info->type; ++info)
+ if (info->type > *codec)
+ *next = SPA_MIN(info->type + DEVICE_PROFILE_LAST, *next);
+
+ if (get_hfp_codec(*codec))
+ profile = DEVICE_PROFILE_HSP_HFP;
+ else if (*codec == SPA_BLUETOOTH_AUDIO_CODEC_LC3)
+ profile = DEVICE_PROFILE_BAP;
+ else
+ profile = DEVICE_PROFILE_A2DP;
+
+ return profile;
+ }
+
+ *next = SPA_ID_INVALID;
+ return SPA_ID_INVALID;
+}
+
+static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec)
+{
+ if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG)
+ return profile;
+
+ if (profile == DEVICE_PROFILE_A2DP) {
+ if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SOURCE))
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ if (profile == DEVICE_PROFILE_BAP) {
+ if (codec == 0)
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ if (profile == DEVICE_PROFILE_HSP_HFP) {
+ if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG))
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ return SPA_ID_INVALID;
+}
+
+static bool set_initial_hsp_hfp_profile(struct impl *this)
+{
+ struct spa_bt_transport *t;
+ int i;
+
+ for (i = SPA_BT_PROFILE_HSP_HS; i <= SPA_BT_PROFILE_HFP_AG; i <<= 1) {
+ if (!(this->bt_dev->connected_profiles & i))
+ continue;
+
+ t = find_transport(this, i, 0);
+ if (t) {
+ this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ?
+ DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP;
+ this->props.codec = get_hfp_codec_id(t->codec);
+
+ spa_log_debug(this->log, "initial profile HSP/HFP profile:%d codec:%d",
+ this->profile, this->props.codec);
+ return true;
+ }
+ }
+ return false;
+}
+
+static void set_initial_profile(struct impl *this)
+{
+ struct spa_bt_transport *t;
+ int i;
+
+ this->switching_codec = false;
+
+ if (this->supported_codecs)
+ free(this->supported_codecs);
+ this->supported_codecs = spa_bt_device_get_supported_media_codecs(
+ this->bt_dev, &this->supported_codec_count, true);
+
+ /* Prefer BAP, then A2DP, then HFP, then null, but select AG if the device
+ appears not to have BAP_SINK, A2DP_SINK or any HEAD_UNIT profile */
+
+ /* If default profile is set to HSP/HFP, first try those and exit if found. */
+ if (this->bt_dev->settings != NULL) {
+ const char *str = spa_dict_lookup(this->bt_dev->settings, "bluez5.profile");
+ if (spa_streq(str, "off"))
+ goto off;
+ if (spa_streq(str, "headset-head-unit") && set_initial_hsp_hfp_profile(this))
+ return;
+ }
+
+ for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
+ if (!(this->bt_dev->connected_profiles & i))
+ continue;
+
+ t = find_transport(this, i, 0);
+ if (t) {
+ if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE)
+ this->profile = DEVICE_PROFILE_AG;
+ else if (i == SPA_BT_PROFILE_BAP_SINK)
+ this->profile = DEVICE_PROFILE_BAP;
+ else
+ this->profile = DEVICE_PROFILE_A2DP;
+ this->props.codec = t->media_codec->id;
+ spa_log_debug(this->log, "initial profile media profile:%d codec:%d",
+ this->profile, this->props.codec);
+ return;
+ }
+ }
+
+ if (set_initial_hsp_hfp_profile(this))
+ return;
+
+off:
+ spa_log_debug(this->log, "initial profile off");
+
+ this->profile = DEVICE_PROFILE_OFF;
+ this->props.codec = 0;
+}
+
+static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t index, uint32_t profile_index, enum spa_bluetooth_audio_codec codec,
+ bool current)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_pod_frame f[2];
+ const char *name, *desc;
+ char *name_and_codec = NULL;
+ char *desc_and_codec = NULL;
+ uint32_t n_source = 0, n_sink = 0;
+ uint32_t capture[1] = { DEVICE_ID_SOURCE }, playback[1] = { DEVICE_ID_SINK };
+ int priority;
+
+ switch (profile_index) {
+ case DEVICE_PROFILE_OFF:
+ name = "off";
+ desc = _("Off");
+ priority = 0;
+ break;
+ case DEVICE_PROFILE_AG:
+ {
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ if (profile == 0) {
+ return NULL;
+ } else {
+ name = "audio-gateway";
+ desc = _("Audio Gateway (A2DP Source & HSP/HFP AG)");
+ }
+ priority = 256;
+ break;
+ }
+ case DEVICE_PROFILE_A2DP:
+ {
+ /* make this device profile visible only if there is an A2DP sink */
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE);
+ if (!(profile & SPA_BT_PROFILE_A2DP_SINK)) {
+ return NULL;
+ }
+ name = spa_bt_profile_name(profile);
+ n_sink++;
+ if (codec) {
+ size_t idx;
+ const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx);
+ if (media_codec == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
+ name = name_and_codec;
+ if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) {
+ desc_and_codec = spa_aprintf(_("High Fidelity Playback (A2DP Sink, codec %s)"),
+ media_codec->description);
+ } else {
+ desc_and_codec = spa_aprintf(_("High Fidelity Duplex (A2DP Source/Sink, codec %s)"),
+ media_codec->description);
+
+ }
+ desc = desc_and_codec;
+ priority = 16 + this->supported_codec_count - idx; /* order as in codec list */
+ } else {
+ if (profile == SPA_BT_PROFILE_A2DP_SINK) {
+ desc = _("High Fidelity Playback (A2DP Sink)");
+ } else {
+ desc = _("High Fidelity Duplex (A2DP Source/Sink)");
+ }
+ priority = 16;
+ }
+ break;
+ }
+ case DEVICE_PROFILE_BAP:
+ {
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE);
+ size_t idx;
+ const struct media_codec *media_codec;
+
+ if (profile == 0)
+ return NULL;
+
+ if (!codec) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (profile & (SPA_BT_PROFILE_BAP_SINK))
+ n_sink++;
+ if (profile & (SPA_BT_PROFILE_BAP_SOURCE))
+ n_source++;
+
+ name = spa_bt_profile_name(profile);
+
+ media_codec = get_supported_media_codec(this, codec, &idx);
+ if (media_codec == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
+ name = name_and_codec;
+ switch (profile) {
+ case SPA_BT_PROFILE_BAP_SINK:
+ desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"),
+ media_codec->description);
+ break;
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ desc_and_codec = spa_aprintf(_("High Fidelity Input (BAP Source, codec %s)"),
+ media_codec->description);
+ break;
+ default:
+ desc_and_codec = spa_aprintf(_("High Fidelity Duplex (BAP Source/Sink, codec %s)"),
+ media_codec->description);
+ }
+ desc = desc_and_codec;
+ priority = 128 + this->supported_codec_count - idx; /* order as in codec list */
+ break;
+ }
+ case DEVICE_PROFILE_HSP_HFP:
+ {
+ /* make this device profile visible only if there is a head unit */
+ uint32_t profile = device->connected_profiles &
+ SPA_BT_PROFILE_HEADSET_HEAD_UNIT;
+ if (profile == 0) {
+ return NULL;
+ }
+ name = spa_bt_profile_name(profile);
+ n_source++;
+ n_sink++;
+ if (codec) {
+ bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ unsigned int hfp_codec = get_hfp_codec(codec);
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1)
+ codec_ok = false;
+ if (!codec_ok) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec));
+ name = name_and_codec;
+ desc_and_codec = spa_aprintf(_("Headset Head Unit (HSP/HFP, codec %s)"),
+ get_hfp_codec_description(hfp_codec));
+ desc = desc_and_codec;
+ priority = 1 + hfp_codec; /* prefer msbc over cvsd */
+ } else {
+ desc = _("Headset Head Unit (HSP/HFP)");
+ priority = 1;
+ }
+ break;
+ }
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
+ SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
+ SPA_PARAM_PROFILE_priority, SPA_POD_Int(priority),
+ 0);
+ if (n_source > 0 || n_sink > 0) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ if (n_source > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(n_source),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, capture));
+ }
+ if (n_sink > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(n_sink),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ if (current) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0);
+ spa_pod_builder_bool(b, this->save_profile);
+ }
+
+ if (name_and_codec)
+ free(name_and_codec);
+ if (desc_and_codec)
+ free(desc_and_codec);
+
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static bool validate_profile(struct impl *this, uint32_t profile,
+ enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ return (build_profile(this, &b, 0, 0, profile, codec, false) != NULL);
+}
+
+static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t port, uint32_t profile)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_pod_frame f[2];
+ enum spa_direction direction;
+ const char *name_prefix, *description, *hfp_description, *port_type;
+ enum spa_bt_form_factor ff;
+ enum spa_bluetooth_audio_codec codec;
+ char name[128];
+ uint32_t i, j, mask, next;
+ uint32_t dev = SPA_ID_INVALID, enum_dev;
+
+ ff = spa_bt_form_factor_from_class(device->bluetooth_class);
+
+ switch (ff) {
+ case SPA_BT_FORM_FACTOR_HEADSET:
+ name_prefix = "headset";
+ description = _("Headset");
+ hfp_description = _("Handsfree");
+ port_type = "headset";
+ break;
+ case SPA_BT_FORM_FACTOR_HANDSFREE:
+ name_prefix = "handsfree";
+ description = _("Handsfree");
+ hfp_description = _("Handsfree (HFP)");
+ port_type = "handsfree";
+ break;
+ case SPA_BT_FORM_FACTOR_MICROPHONE:
+ name_prefix = "microphone";
+ description = _("Microphone");
+ hfp_description = _("Handsfree");
+ port_type = "mic";
+ break;
+ case SPA_BT_FORM_FACTOR_SPEAKER:
+ name_prefix = "speaker";
+ description = _("Speaker");
+ hfp_description = _("Handsfree");
+ port_type = "speaker";
+ break;
+ case SPA_BT_FORM_FACTOR_HEADPHONE:
+ name_prefix = "headphone";
+ description = _("Headphone");
+ hfp_description = _("Handsfree");
+ port_type = "headphones";
+ break;
+ case SPA_BT_FORM_FACTOR_PORTABLE:
+ name_prefix = "portable";
+ description = _("Portable");
+ hfp_description = _("Handsfree");
+ port_type = "portable";
+ break;
+ case SPA_BT_FORM_FACTOR_CAR:
+ name_prefix = "car";
+ description = _("Car");
+ hfp_description = _("Handsfree");
+ port_type = "car";
+ break;
+ case SPA_BT_FORM_FACTOR_HIFI:
+ name_prefix = "hifi";
+ description = _("HiFi");
+ hfp_description = _("Handsfree");
+ port_type = "hifi";
+ break;
+ case SPA_BT_FORM_FACTOR_PHONE:
+ name_prefix = "phone";
+ description = _("Phone");
+ hfp_description = _("Handsfree");
+ port_type = "phone";
+ break;
+ case SPA_BT_FORM_FACTOR_UNKNOWN:
+ default:
+ name_prefix = "bluetooth";
+ description = _("Bluetooth");
+ hfp_description = _("Bluetooth (HFP)");
+ port_type = "bluetooth";
+ break;
+ }
+
+ switch (port) {
+ case 0:
+ direction = SPA_DIRECTION_INPUT;
+ snprintf(name, sizeof(name), "%s-input", name_prefix);
+ enum_dev = DEVICE_ID_SOURCE;
+ if (profile == DEVICE_PROFILE_A2DP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 1:
+ direction = SPA_DIRECTION_OUTPUT;
+ snprintf(name, sizeof(name), "%s-output", name_prefix);
+ enum_dev = DEVICE_ID_SINK;
+ if (profile == DEVICE_PROFILE_A2DP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 2:
+ direction = SPA_DIRECTION_INPUT;
+ snprintf(name, sizeof(name), "%s-hf-input", name_prefix);
+ description = hfp_description;
+ enum_dev = DEVICE_ID_SOURCE;
+ if (profile == DEVICE_PROFILE_HSP_HFP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 3:
+ direction = SPA_DIRECTION_OUTPUT;
+ snprintf(name, sizeof(name), "%s-hf-output", name_prefix);
+ description = hfp_description;
+ enum_dev = DEVICE_ID_SINK;
+ if (profile == DEVICE_PROFILE_HSP_HFP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (enum_dev == SPA_ID_INVALID) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(name),
+ SPA_PARAM_ROUTE_description, SPA_POD_String(description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_Int(0),
+ SPA_PARAM_ROUTE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, 1);
+ spa_pod_builder_add(b,
+ SPA_POD_String("port.type"),
+ SPA_POD_String(port_type),
+ NULL);
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+
+ mask = 0;
+ for (i = 1; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) {
+ uint32_t profile_mask;
+
+ if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1))
+ continue;
+ if (j == DEVICE_PROFILE_HSP_HFP && !(port == 2 || port == 3))
+ continue;
+
+ profile_mask = profile_direction_mask(this, j, codec);
+ if (!(profile_mask & (1 << direction)))
+ continue;
+
+ /* Check the profile actually exists */
+ if (!validate_profile(this, j, codec))
+ continue;
+
+ mask |= profile_mask;
+ spa_pod_builder_int(b, i);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (!(mask & (1 << direction))) {
+ /* No profile has route direction */
+ return NULL;
+ }
+
+ if (dev != SPA_ID_INVALID) {
+ struct node *node = &this->nodes[dev];
+ struct spa_bt_transport_volume *t_volume;
+
+ mask = profile_direction_mask(this, this->profile, this->props.codec);
+ if (!(mask & (1 << direction)))
+ return NULL;
+
+ t_volume = node->transport
+ ? &node->transport->volumes[node->id]
+ : NULL;
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0);
+ spa_pod_builder_int(b, dev);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id);
+
+ spa_pod_builder_prop(b, SPA_PROP_mute, 0);
+ spa_pod_builder_bool(b, node->mute);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelVolumes,
+ (t_volume && t_volume->active) ? SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ node->n_channels, node->volumes);
+
+ if (t_volume && t_volume->active) {
+ spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, 1.0f / (t_volume->hw_volume_max + 1));
+ }
+
+ spa_pod_builder_prop(b, SPA_PROP_channelMap, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ node->n_channels, node->channels);
+
+ if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) &&
+ dev == DEVICE_ID_SINK) {
+ spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
+ spa_pod_builder_long(b, node->latency_offset);
+ }
+
+ spa_pod_builder_pop(b, &f[1]);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(b, node->save);
+ }
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ spa_pod_builder_int(b, enum_dev);
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (profile != SPA_ID_INVALID) {
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0);
+ spa_pod_builder_int(b, profile);
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec)
+{
+ int i;
+
+next:
+ *j = *j + 1;
+ spa_assert(*j >= 0);
+ if ((size_t)*j >= this->supported_codec_count)
+ return false;
+
+ for (i = 0; i < *j; ++i)
+ if (this->supported_codecs[i]->id == this->supported_codecs[*j]->id)
+ goto next;
+
+ *codec = this->supported_codecs[*j];
+ return true;
+}
+
+static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_builder *b, uint32_t id)
+{
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ const struct media_codec *codec;
+ size_t n;
+ int j;
+
+#define FOR_EACH_MEDIA_CODEC(j, codec) \
+ for (j = -1; iterate_supported_media_codecs(this, &j, &codec);)
+#define FOR_EACH_HFP_CODEC(j) \
+ for (j = HFP_AUDIO_CODEC_MSBC; j >= HFP_AUDIO_CODEC_CVSD; --j) \
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1)
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+
+ /*
+ * XXX: the ids in principle should use builder_id, not builder_int,
+ * XXX: but the type info for _type and _labels doesn't work quite right now.
+ */
+
+ /* Transport codec */
+ spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
+ spa_pod_builder_id(b, SPA_PROP_bluetoothAudioCodec);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
+ spa_pod_builder_string(b, "Air codec");
+ spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]);
+ n = 0;
+ if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+ FOR_EACH_MEDIA_CODEC(j, codec) {
+ if (n == 0)
+ spa_pod_builder_int(b, codec->id);
+ spa_pod_builder_int(b, codec->id);
+ ++n;
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ FOR_EACH_HFP_CODEC(j) {
+ if (n == 0)
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ ++n;
+ }
+ }
+ if (n == 0)
+ choice->body.type = SPA_CHOICE_None;
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+ FOR_EACH_MEDIA_CODEC(j, codec) {
+ spa_pod_builder_int(b, codec->id);
+ spa_pod_builder_string(b, codec->description);
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ FOR_EACH_HFP_CODEC(j) {
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ spa_pod_builder_string(b, get_hfp_codec_description(j));
+ }
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+
+#undef FOR_EACH_MEDIA_CODEC
+#undef FOR_EACH_HFP_CODEC
+}
+
+static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b, uint32_t id)
+{
+ struct props *p = &this->props;
+
+ return spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec),
+ SPA_PROP_bluetoothOffloadActive, SPA_POD_Bool(p->offload_active));
+}
+
+static int impl_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[2048];
+ struct spa_result_device_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ {
+ uint32_t profile;
+ enum spa_bluetooth_audio_codec codec;
+
+ profile = get_profile_from_index(this, result.index, &result.next, &codec);
+
+ switch (profile) {
+ case DEVICE_PROFILE_OFF:
+ case DEVICE_PROFILE_AG:
+ case DEVICE_PROFILE_A2DP:
+ case DEVICE_PROFILE_BAP:
+ case DEVICE_PROFILE_HSP_HFP:
+ param = build_profile(this, &b, id, result.index, profile, codec, false);
+ if (param == NULL)
+ goto next;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Profile:
+ {
+ uint32_t index;
+
+ switch (result.index) {
+ case 0:
+ index = get_index_from_profile(this, this->profile, this->props.codec);
+ param = build_profile(this, &b, id, index, this->profile, this->props.codec, true);
+ if (param == NULL)
+ return 0;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_EnumRoute:
+ {
+ switch (result.index) {
+ case 0: case 1: case 2: case 3:
+ param = build_route(this, &b, id, result.index, SPA_ID_INVALID);
+ if (param == NULL)
+ goto next;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Route:
+ {
+ switch (result.index) {
+ case 0: case 1: case 2: case 3:
+ param = build_route(this, &b, id, result.index, this->profile);
+ if (param == NULL)
+ goto next;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_prop_info_codec(this, &b, id);
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_bluetoothOffloadActive),
+ SPA_PROP_INFO_description, SPA_POD_String("Bluetooth audio offload active"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(false));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_props(this, &b, id);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes)
+{
+ uint32_t i;
+ int changed = 0;
+ struct spa_bt_transport_volume *t_volume;
+
+ if (n_volumes == 0)
+ return -EINVAL;
+
+ spa_log_info(this->log, "node %p volume %f", node, volumes[0]);
+
+ for (i = 0; i < node->n_channels; i++) {
+ if (node->volumes[i] == volumes[i % n_volumes])
+ continue;
+ ++changed;
+ node->volumes[i] = volumes[i % n_volumes];
+ }
+
+ t_volume = node->transport ? &node->transport->volumes[node->id]: NULL;
+
+ if (t_volume && t_volume->active
+ && spa_bt_transport_volume_enabled(node->transport)) {
+ float hw_volume = node_get_hw_volume(node);
+ spa_log_debug(this->log, "node %p hardware volume %f", node, hw_volume);
+
+ node_update_soft_volumes(node, hw_volume);
+ spa_bt_transport_set_volume(node->transport, node->id, hw_volume);
+ } else {
+ float boost = get_soft_volume_boost(node);
+ for (uint32_t i = 0; i < node->n_channels; ++i)
+ node->soft_volumes[i] = node->volumes[i] * boost;
+ }
+
+ emit_volume(this, node);
+
+ return changed;
+}
+
+static int node_set_mute(struct impl *this, struct node *node, bool mute)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ int changed = 0;
+
+ spa_log_info(this->log, "node %p mute %d", node, mute);
+
+ changed = (node->mute != mute);
+ node->mute = mute;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_mute, SPA_POD_Bool(mute),
+ SPA_PROP_softMute, SPA_POD_Bool(mute));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+
+ return changed;
+}
+
+static int node_set_latency_offset(struct impl *this, struct node *node, int64_t latency_offset)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ int changed = 0;
+
+ spa_log_info(this->log, "node %p latency offset %"PRIi64" nsec", node, latency_offset);
+
+ changed = (node->latency_offset != latency_offset);
+ node->latency_offset = latency_offset;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(latency_offset));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+
+ return changed;
+}
+
+static int apply_device_props(struct impl *this, struct node *node, struct spa_pod *props)
+{
+ float volume = 0;
+ bool mute = 0;
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) props;
+ int changed = 0;
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t n_volumes = 0, SPA_UNUSED n_channels = 0;
+ int64_t latency_offset = 0;
+
+ if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props))
+ return -EINVAL;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &volume) == 0) {
+ int res = node_set_volume(this, node, &volume, 1);
+ if (res > 0)
+ ++changed;
+ }
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &mute) == 0) {
+ int res = node_set_mute(this, node, mute);
+ if (res > 0)
+ ++changed;
+ }
+ break;
+ case SPA_PROP_channelVolumes:
+ n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ volumes, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ case SPA_PROP_channelMap:
+ n_channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ channels, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ case SPA_PROP_latencyOffsetNsec:
+ if (spa_pod_get_long(&prop->value, &latency_offset) == 0) {
+ int res = node_set_latency_offset(this, node, latency_offset);
+ if (res > 0)
+ ++changed;
+ }
+ }
+ }
+ if (n_volumes > 0) {
+ int res = node_set_volume(this, node, volumes, n_volumes);
+ if (res > 0)
+ ++changed;
+ }
+
+ return changed;
+}
+
+static void apply_prop_offload_active(struct impl *this, bool active)
+{
+ bool old_value = this->props.offload_active;
+
+ this->props.offload_active = active;
+
+ for (int i = 0; i < 2; i++) {
+ node_offload_set_active(&this->nodes[i], active);
+ if (!this->nodes[i].offload_acquired)
+ this->props.offload_active = false;
+ }
+
+ if (this->props.offload_active != old_value) {
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+ }
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx, next;
+ uint32_t profile;
+ enum spa_bluetooth_audio_codec codec;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ profile = get_profile_from_index(this, idx, &next, &codec);
+ if (profile == SPA_ID_INVALID)
+ return -EINVAL;
+
+ spa_log_debug(this->log, "setting profile %d codec:%d save:%d", profile, codec, (int)save);
+ return set_profile(this, profile, codec, save);
+ }
+ case SPA_PARAM_Route:
+ {
+ uint32_t idx, device;
+ struct spa_pod *props = NULL;
+ struct node *node;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props),
+ SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse route");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+ if (device > 1 || !this->nodes[device].active)
+ return -EINVAL;
+
+ node = &this->nodes[device];
+ node->save = save;
+ if (props) {
+ int changed = apply_device_props(this, node, props);
+ if (changed > 0) {
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ }
+ emit_info(this, false);
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ uint32_t codec_id = SPA_ID_INVALID;
+ bool offload_active = this->props.offload_active;
+
+ if (param == NULL)
+ return 0;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id),
+ SPA_PROP_bluetoothOffloadActive, SPA_POD_OPT_Bool(&offload_active))) < 0) {
+ spa_log_warn(this->log, "can't parse props");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ spa_log_debug(this->log, "setting props codec:%d offload:%d", (int)codec_id, (int)offload_active);
+
+ apply_prop_offload_active(this, offload_active);
+
+ if (codec_id == SPA_ID_INVALID)
+ return 0;
+
+ if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+ size_t j;
+ for (j = 0; j < this->supported_codec_count; ++j) {
+ if (this->supported_codecs[j]->id == codec_id) {
+ return set_profile(this, this->profile, codec_id, true);
+ }
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD &&
+ spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_CVSD) == 1) {
+ return set_profile(this, this->profile, codec_id, true);
+ } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_MSBC &&
+ spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_MSBC) == 1) {
+ return set_profile(this, this->profile, codec_id, true);
+ }
+ }
+ return -EINVAL;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ const struct spa_dict_item *it;
+
+ emit_remove_nodes(this);
+
+ free(this->supported_codecs);
+ if (this->bt_dev) {
+ this->bt_dev->settings = NULL;
+ spa_hook_remove(&this->bt_dev_listener);
+ }
+
+ spa_dict_for_each(it, &this->setting_dict) {
+ if(it->key)
+ free((void *)it->key);
+ if(it->value)
+ free((void *)it->value);
+ }
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static const struct spa_dict*
+filter_bluez_device_setting(struct impl *this, const struct spa_dict *dict)
+{
+ uint32_t n_items = 0;
+ for (uint32_t i = 0
+ ; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->setting_items)
+ ; i++)
+ {
+ const struct spa_dict_item *it = &dict->items[i];
+ if (it->key != NULL && strncmp(it->key, "bluez", 5) == 0 && it->value != NULL) {
+ this->setting_items[n_items++] =
+ SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
+ }
+ }
+ this->setting_dict = SPA_DICT_INIT(this->setting_items, n_items);
+ return &this->setting_dict;
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ _i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_DEVICE)))
+ sscanf(str, "pointer:%p", &this->bt_dev);
+
+ if (this->bt_dev == NULL) {
+ spa_log_error(this->log, "a device is needed");
+ return -EINVAL;
+ }
+
+ if (info) {
+ int profiles;
+ this->bt_dev->settings = filter_bluez_device_setting(this, info);
+
+ if ((str = spa_dict_lookup(info, "bluez5.auto-connect")) != NULL) {
+ if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
+ this->bt_dev->reconnect_profiles = profiles;
+ }
+
+ if ((str = spa_dict_lookup(info, "bluez5.hw-volume")) != NULL) {
+ if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
+ this->bt_dev->hw_volume_profiles = profiles;
+ }
+ }
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ init_node(this, &this->nodes[0], 0);
+ init_node(this, &this->nodes[1], 1);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ);
+ this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 6;
+
+ spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, this);
+
+ set_initial_profile(this);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+static const struct spa_dict_item handle_info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "A bluetooth device" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_DEVICE"=<device>" },
+};
+
+static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items);
+
+const struct spa_handle_factory spa_bluez5_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_DEVICE,
+ &handle_info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
new file mode 100644
index 0000000..f8363f8
--- /dev/null
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -0,0 +1,234 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "codec-loader.h"
+
+#define MEDIA_CODEC_LIB_BASE "bluez5/libspa-codec-bluez5-"
+
+/* AVDTP allows 0x3E endpoints, can't have more codecs than that */
+#define MAX_CODECS 0x3E
+#define MAX_HANDLES MAX_CODECS
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ const struct media_codec *codecs[MAX_CODECS + 1];
+ struct spa_handle *handles[MAX_HANDLES];
+ size_t n_codecs;
+ size_t n_handles;
+ struct spa_plugin_loader *loader;
+ struct spa_log *log;
+};
+
+static int codec_order(const struct media_codec *c)
+{
+ static const enum spa_bluetooth_audio_codec order[] = {
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3,
+ SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX,
+ SPA_BLUETOOTH_AUDIO_CODEC_AAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+ SPA_BLUETOOTH_AUDIO_CODEC_MPEG,
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC,
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+ };
+ size_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(order); ++i)
+ if (c->id == order[i])
+ return i;
+ return SPA_N_ELEMENTS(order);
+}
+
+static int codec_order_cmp(const void *a, const void *b)
+{
+ const struct media_codec * const *ca = a;
+ const struct media_codec * const *cb = b;
+ int ia = codec_order(*ca);
+ int ib = codec_order(*cb);
+ if (*ca == *cb)
+ return 0;
+ return (ia == ib) ? (*ca < *cb ? -1 : 1) : ia - ib;
+}
+
+static int load_media_codecs_from(struct impl *impl, const char *factory_name, const char *libname)
+{
+ struct spa_handle *handle = NULL;
+ void *iface;
+ const struct spa_bluez5_codec_a2dp *bluez5_codec_a2dp;
+ int n_codecs = 0;
+ int res;
+ size_t i;
+ struct spa_dict_item info_items[] = {
+ { SPA_KEY_LIBRARY_NAME, libname },
+ };
+ struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+ handle = spa_plugin_loader_load(impl->loader, factory_name, &info);
+ if (handle == NULL) {
+ spa_log_info(impl->log, "Bluetooth codec plugin %s not available", factory_name);
+ return -ENOENT;
+ }
+
+ spa_log_debug(impl->log, "loading codecs from %s", factory_name);
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Bluez5CodecMedia, &iface)) < 0) {
+ spa_log_warn(impl->log, "Bluetooth codec plugin %s has no codec interface",
+ factory_name);
+ goto fail;
+ }
+
+ bluez5_codec_a2dp = iface;
+
+ if (bluez5_codec_a2dp->iface.version != SPA_VERSION_BLUEZ5_CODEC_MEDIA) {
+ spa_log_warn(impl->log, "codec plugin %s has incompatible ABI version (%d != %d)",
+ factory_name, bluez5_codec_a2dp->iface.version, SPA_VERSION_BLUEZ5_CODEC_MEDIA);
+ res = -ENOENT;
+ goto fail;
+ }
+
+ for (i = 0; bluez5_codec_a2dp->codecs[i]; ++i) {
+ const struct media_codec *c = bluez5_codec_a2dp->codecs[i];
+ const char *ep = c->endpoint_name ? c->endpoint_name : c->name;
+ size_t j;
+
+ if (!ep)
+ goto next_codec;
+
+ if (impl->n_codecs >= MAX_CODECS) {
+ spa_log_error(impl->log, "too many A2DP codecs");
+ break;
+ }
+
+ /* Don't load duplicate endpoints */
+ for (j = 0; j < impl->n_codecs; ++j) {
+ const struct media_codec *c2 = impl->codecs[j];
+ const char *ep2 = c2->endpoint_name ? c2->endpoint_name : c2->name;
+ if (spa_streq(ep, ep2) && c->fill_caps && c2->fill_caps) {
+ spa_log_debug(impl->log, "media codec %s from %s duplicate endpoint %s",
+ c->name, factory_name, ep);
+ goto next_codec;
+ }
+ }
+
+ spa_log_debug(impl->log, "loaded media codec %s from %s, endpoint:%s",
+ c->name, factory_name, ep);
+
+ if (c->set_log)
+ c->set_log(impl->log);
+
+ impl->codecs[impl->n_codecs++] = c;
+ ++n_codecs;
+
+ next_codec:
+ continue;
+ }
+
+ if (n_codecs > 0)
+ impl->handles[impl->n_handles++] = handle;
+ else
+ spa_plugin_loader_unload(impl->loader, handle);
+
+ return 0;
+
+fail:
+ if (handle)
+ spa_plugin_loader_unload(impl->loader, handle);
+ return res;
+}
+
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log)
+{
+ struct impl *impl;
+ bool has_sbc;
+ size_t i;
+ const struct { const char *factory; const char *lib; } plugins[] = {
+#define MEDIA_CODEC_FACTORY_LIB(basename) \
+ { MEDIA_CODEC_FACTORY_NAME(basename), MEDIA_CODEC_LIB_BASE basename }
+ MEDIA_CODEC_FACTORY_LIB("aac"),
+ MEDIA_CODEC_FACTORY_LIB("aptx"),
+ MEDIA_CODEC_FACTORY_LIB("faststream"),
+ MEDIA_CODEC_FACTORY_LIB("ldac"),
+ MEDIA_CODEC_FACTORY_LIB("sbc"),
+ MEDIA_CODEC_FACTORY_LIB("lc3plus"),
+ MEDIA_CODEC_FACTORY_LIB("opus"),
+ MEDIA_CODEC_FACTORY_LIB("lc3")
+#undef MEDIA_CODEC_FACTORY_LIB
+ };
+
+ impl = calloc(sizeof(struct impl), 1);
+ if (impl == NULL)
+ return NULL;
+
+ impl->loader = loader;
+ impl->log = log;
+
+ spa_log_topic_init(impl->log, &log_topic);
+
+ for (i = 0; i < SPA_N_ELEMENTS(plugins); ++i)
+ load_media_codecs_from(impl, plugins[i].factory, plugins[i].lib);
+
+ has_sbc = false;
+ for (i = 0; i < impl->n_codecs; ++i)
+ if (impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_SBC)
+ has_sbc = true;
+
+ if (!has_sbc) {
+ spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins");
+ free_media_codecs(impl->codecs);
+ errno = ENOENT;
+ return NULL;
+ }
+
+ qsort(impl->codecs, impl->n_codecs, sizeof(const struct media_codec *), codec_order_cmp);
+
+ return impl->codecs;
+}
+
+void free_media_codecs(const struct media_codec * const *media_codecs)
+{
+ struct impl *impl = SPA_CONTAINER_OF(media_codecs, struct impl, codecs);
+ size_t i;
+
+ for (i = 0; i < impl->n_handles; ++i)
+ spa_plugin_loader_unload(impl->loader, impl->handles[i]);
+
+ free(impl);
+}
diff --git a/spa/plugins/bluez5/codec-loader.h b/spa/plugins/bluez5/codec-loader.h
new file mode 100644
index 0000000..b77d9c4
--- /dev/null
+++ b/spa/plugins/bluez5/codec-loader.h
@@ -0,0 +1,39 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_CODEC_LOADER_H_
+#define SPA_BLUEZ5_CODEC_LOADER_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <spa/support/plugin-loader.h>
+
+#include "a2dp-codec-caps.h"
+#include "media-codecs.h"
+
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log);
+void free_media_codecs(const struct media_codec * const *media_codecs);
+
+#endif
diff --git a/spa/plugins/bluez5/dbus-monitor.c b/spa/plugins/bluez5/dbus-monitor.c
new file mode 100644
index 0000000..6fbfb2a
--- /dev/null
+++ b/spa/plugins/bluez5/dbus-monitor.c
@@ -0,0 +1,265 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+
+#include "dbus-monitor.h"
+
+
+static void on_g_properties_changed(GDBusProxy *proxy,
+ GVariant *changed_properties, char **invalidated_properties,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(G_DBUS_INTERFACE(proxy));
+ const char *name = info ? info->name : NULL;
+ const struct dbus_monitor_proxy_type *p;
+
+ spa_log_trace(monitor->log, "%p: dbus object updated path=%s, name=%s",
+ monitor, g_dbus_proxy_get_object_path(proxy), name ? name : "<null>");
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID ; ++p) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(proxy, p->proxy_type)) {
+ if (p->on_update)
+ p->on_update(monitor, G_DBUS_INTERFACE(proxy));
+ }
+ }
+}
+
+static void on_remove(struct dbus_monitor *monitor, GDBusProxy *proxy)
+{
+ const struct dbus_monitor_proxy_type *p;
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID ; ++p) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(proxy, p->proxy_type)) {
+ if (p->on_remove)
+ p->on_remove(monitor, G_DBUS_INTERFACE(proxy));
+ }
+ }
+}
+
+static void on_interface_added(GDBusObjectManager *self, GDBusObject *object,
+ GDBusInterface *iface, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(iface);
+ const char *name = info ? info->name : NULL;
+
+ spa_log_trace(monitor->log, "%p: dbus interface added path=%s, name=%s",
+ monitor, g_dbus_object_get_object_path(object), name ? name : "<null>");
+
+ if (!g_object_get_data(G_OBJECT(iface), "dbus-monitor-signals-connected")) {
+ g_object_set_data(G_OBJECT(iface), "dbus-monitor-signals-connected", GUINT_TO_POINTER(1));
+ g_signal_connect(iface, "g-properties-changed",
+ G_CALLBACK(on_g_properties_changed),
+ monitor);
+ }
+
+ on_g_properties_changed(G_DBUS_PROXY(iface),
+ NULL, NULL, monitor);
+}
+
+static void on_interface_removed(GDBusObjectManager *manager, GDBusObject *object,
+ GDBusInterface *iface, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(iface);
+ const char *name = info ? info->name : NULL;
+
+ spa_log_trace(monitor->log, "%p: dbus interface removed path=%s, name=%s",
+ monitor, g_dbus_object_get_object_path(object), name ? name : "<null>");
+
+ if (g_object_get_data(G_OBJECT(iface), "dbus-monitor-signals-connected")) {
+ g_object_disconnect(G_OBJECT(iface), "g-properties-changed",
+ G_CALLBACK(on_g_properties_changed),
+ monitor, NULL);
+ g_object_set_data(G_OBJECT(iface), "dbus-monitor-signals-connected", NULL);
+ }
+
+ on_remove(monitor, G_DBUS_PROXY(iface));
+}
+
+static void on_object_added(GDBusObjectManager *self, GDBusObject *object,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GList *interfaces = g_dbus_object_get_interfaces(object);
+
+ /*
+ * on_interface_added won't necessarily be called on objects on
+ * name owner changes, so we have to call it here for all interfaces.
+ */
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_added(dbus_monitor_manager(monitor),
+ object, G_DBUS_INTERFACE(lli->data), monitor);
+ }
+
+ g_list_free_full(interfaces, g_object_unref);
+}
+
+static void on_object_removed(GDBusObjectManager *manager, GDBusObject *object,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GList *interfaces = g_dbus_object_get_interfaces(object);
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_removed(dbus_monitor_manager(monitor),
+ object, G_DBUS_INTERFACE(lli->data), monitor);
+ }
+
+ g_list_free_full(interfaces, g_object_unref);
+}
+
+static void on_notify(GObject *gobject, GParamSpec *pspec, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+
+ if (spa_streq(pspec->name, "name-owner") && monitor->on_name_owner_change)
+ monitor->on_name_owner_change(monitor);
+}
+
+static GType get_proxy_type(GDBusObjectManagerClient *manager, const gchar *object_path,
+ const gchar *interface_name, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ const struct dbus_monitor_proxy_type *p;
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID; ++p) {
+ if (spa_streq(p->interface_name, interface_name))
+ return p->proxy_type;
+ }
+
+ return G_TYPE_DBUS_PROXY;
+}
+
+static void init_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GError *error = NULL;
+ GList *objects;
+ GObject *ret;
+
+ g_clear_object(&monitor->call);
+
+ ret = g_async_initable_new_finish(G_ASYNC_INITABLE(source_object), res, &error);
+ if (!ret) {
+ spa_log_error(monitor->log, "%p: creating DBus object monitor failed: %s",
+ monitor, error->message);
+ g_error_free(error);
+ return;
+ }
+ monitor->manager = G_DBUS_OBJECT_MANAGER_CLIENT(ret);
+
+ spa_log_debug(monitor->log, "%p: DBus monitor started", monitor);
+
+ g_signal_connect(monitor->manager, "interface-added",
+ G_CALLBACK(on_interface_added), monitor);
+ g_signal_connect(monitor->manager, "interface-removed",
+ G_CALLBACK(on_interface_removed), monitor);
+ g_signal_connect(monitor->manager, "object-added",
+ G_CALLBACK(on_object_added), monitor);
+ g_signal_connect(monitor->manager, "object-removed",
+ G_CALLBACK(on_object_removed), monitor);
+ g_signal_connect(monitor->manager, "notify",
+ G_CALLBACK(on_notify), monitor);
+
+ /* List all objects now */
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor));
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_added(dbus_monitor_manager(monitor),
+ G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data),
+ monitor);
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+}
+
+void dbus_monitor_init(struct dbus_monitor *monitor,
+ GType client_type,
+ struct spa_log *log, GDBusConnection *conn,
+ const char *name, const char *object_path,
+ const struct dbus_monitor_proxy_type *proxy_types,
+ void (*on_name_owner_change)(struct dbus_monitor *monitor))
+{
+ GDBusObjectManagerClientFlags flags = G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START;
+ size_t i;
+
+ spa_zero(*monitor);
+
+ monitor->log = log;
+ monitor->call = g_cancellable_new();
+ monitor->on_name_owner_change = on_name_owner_change;
+
+ spa_zero(monitor->proxy_types);
+
+ for (i = 0; proxy_types && proxy_types[i].proxy_type != G_TYPE_INVALID; ++i) {
+ spa_assert(i < DBUS_MONITOR_MAX_TYPES);
+ monitor->proxy_types[i] = proxy_types[i];
+ }
+
+ g_async_initable_new_async(client_type, G_PRIORITY_DEFAULT,
+ monitor->call, init_done, monitor,
+ "flags", flags, "name", name, "connection", conn,
+ "object-path", object_path,
+ "get-proxy-type-func", get_proxy_type,
+ "get-proxy-type-user-data", monitor,
+ NULL);
+}
+
+void dbus_monitor_clear(struct dbus_monitor *monitor)
+{
+ g_cancellable_cancel(monitor->call);
+ g_clear_object(&monitor->call);
+
+ if (monitor->manager) {
+ /*
+ * Indicate all objects should stop now.
+ *
+ * This has to be a separate hook, because the proxy finalizers
+ * may be called later asynchronously via e.g. DBus callbacks.
+ */
+ GList *objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor));
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_removed(dbus_monitor_manager(monitor),
+ G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data),
+ monitor);
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+ }
+
+ g_clear_object(&monitor->manager);
+ spa_zero(*monitor);
+}
diff --git a/spa/plugins/bluez5/dbus-monitor.h b/spa/plugins/bluez5/dbus-monitor.h
new file mode 100644
index 0000000..f9fa12b
--- /dev/null
+++ b/spa/plugins/bluez5/dbus-monitor.h
@@ -0,0 +1,83 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef DBUS_MONITOR_H_
+#define DBUS_MONITOR_H_
+
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+
+#define DBUS_MONITOR_MAX_TYPES 16
+
+struct dbus_monitor;
+
+struct dbus_monitor_proxy_type
+{
+ /** Interface name to monitor, or NULL for object type */
+ const char *interface_name;
+
+ /** GObject type for the proxy */
+ GType proxy_type;
+
+ /** Hook called when object added or properties changed */
+ void (*on_update)(struct dbus_monitor *monitor, GDBusInterface *iface);
+
+ /** Hook called when object is removed (or on monitor shutdown) */
+ void (*on_remove)(struct dbus_monitor *monitor, GDBusInterface *iface);
+};
+
+struct dbus_monitor
+{
+ GDBusObjectManagerClient *manager;
+ struct spa_log *log;
+ GCancellable *call;
+ struct dbus_monitor_proxy_type proxy_types[DBUS_MONITOR_MAX_TYPES+1];
+ void (*on_name_owner_change)(struct dbus_monitor *monitor);
+ void *user_data;
+};
+
+static inline GDBusObjectManager *dbus_monitor_manager(struct dbus_monitor *monitor)
+{
+ return G_DBUS_OBJECT_MANAGER(monitor->manager);
+}
+
+/**
+ * Create a DBus object monitor, with a given interface to proxy type map.
+ *
+ * \param proxy_types Mapping between interface names and watched proxy
+ * types, terminated by G_TYPE_INVALID.
+ * \param on_object_update Called for all objects and interfaces on
+ * startup, and when object properties are modified.
+ */
+void dbus_monitor_init(struct dbus_monitor *monitor,
+ GType client_type, struct spa_log *log, GDBusConnection *conn,
+ const char *name, const char *object_path,
+ const struct dbus_monitor_proxy_type *proxy_types,
+ void (*on_name_owner_change)(struct dbus_monitor *monitor));
+
+void dbus_monitor_clear(struct dbus_monitor *monitor);
+
+#endif DBUS_MONITOR_H_
diff --git a/spa/plugins/bluez5/decode-buffer.h b/spa/plugins/bluez5/decode-buffer.h
new file mode 100644
index 0000000..434f735
--- /dev/null
+++ b/spa/plugins/bluez5/decode-buffer.h
@@ -0,0 +1,486 @@
+/* Spa Bluez5 decode buffer
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * \file decode-buffer.h Buffering for Bluetooth sources
+ *
+ * A linear buffer, which is compacted when it gets half full.
+ *
+ * Also contains buffering logic, which calculates a rate correction
+ * factor to maintain the buffer level at the target value.
+ *
+ * Consider typical packet intervals with nominal frame duration
+ * of 10ms:
+ *
+ * ... 5ms | 5ms | 20ms | 5ms | 5ms | 20ms ...
+ *
+ * ... 3ms | 3ms | 4ms | 30ms | 3ms | 3ms | 4ms | 30ms ...
+ *
+ * plus random jitter; 10ms nominal may occasionally have 20+ms interval.
+ * The regular timer cycle cannot be aligned with this, so process()
+ * may occur at any time.
+ *
+ * The buffer level is the difference between the number of samples in
+ * buffer immediately after receiving a packet, and the samples consumed
+ * before receiving the next packet.
+ *
+ * The buffer level indicates how much any packet can be delayed without
+ * underrun. If it is positive, there are no underruns.
+ *
+ * The rate correction aims to maintain the average level at a safety margin.
+ */
+
+#ifndef SPA_BLUEZ5_DECODE_BUFFER_H
+#define SPA_BLUEZ5_DECODE_BUFFER_H
+
+#include <stdlib.h>
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+
+#define BUFFERING_LONG_MSEC (2*60000)
+#define BUFFERING_SHORT_MSEC 1000
+#define BUFFERING_RATE_DIFF_MAX 0.005
+
+/**
+ * Safety margin.
+ *
+ * The spike is the long-window maximum difference
+ * between minimum and average buffer level.
+ */
+#define BUFFERING_TARGET(spike,packet_size) \
+ SPA_CLAMP((spike)*3/2, (packet_size), 6*(packet_size))
+
+/**
+ * Rate controller.
+ *
+ * It's here in a form, where it operates on the running average
+ * so it's compatible with the level spike determination, and
+ * clamping the rate to a range is easy. The impulse response
+ * is similar to spa_dll, and step response does not have sign changes.
+ *
+ * The controller iterates as
+ *
+ * avg(j+1) = (1 - beta) avg(j) + beta level(j)
+ * corr(j+1) = corr(j) + a [avg(j+1) - avg(j)] / duration
+ * + b [avg(j) - target] / duration
+ *
+ * with beta = duration/avg_period < 0.5 is the moving average parameter,
+ * and a = beta/3 + ..., b = beta^2/27 + ....
+ *
+ * This choice results to c(j) being low-pass filtered, and buffer level(j)
+ * converging towards target with stable damped evolution with eigenvalues
+ * real and close to each other around (1 - beta)^(1/3).
+ *
+ * Derivation:
+ *
+ * The deviation from the buffer level target evolves as
+ *
+ * delta(j) = level(j) - target
+ * delta(j+1) = delta(j) + r(j) - c(j+1)
+ *
+ * where r is samples received in one duration, and c corrected rate
+ * (samples per duration).
+ *
+ * The rate correction is in general determined by linear filter f
+ *
+ * c(j+1) = c(j) + \sum_{k=0}^\infty delta(j - k) f(k)
+ *
+ * If \sum_k f(k) is not zero, the only fixed point is c=r, delta=0,
+ * so this structure (if the filter is stable) rate matches and
+ * drives buffer level to target.
+ *
+ * The z-transform then is
+ *
+ * delta(z) = G(z) r(z)
+ * c(z) = F(z) delta(z)
+ * G(z) = (z - 1) / [(z - 1)^2 + z f(z)]
+ * F(z) = f(z) / (z - 1)
+ *
+ * We now want: poles of G(z) must be in |z|<1 for stability, F(z)
+ * should damp high frequencies, and f(z) is causal.
+ *
+ * To satisfy the conditions, take
+ *
+ * (z - 1)^2 + z f(z) = p(z) / q(z)
+ *
+ * where p(z) is polynomial with leading term z^n with wanted root
+ * structure, and q(z) is any polynomial with leading term z^{n-2}.
+ * This guarantees f(z) is causal, and G(z) = (z-1) q(z) / p(z).
+ * We can choose p(z) and q(z) to improve low-pass properties of F(z).
+ *
+ * Simplest choice is p(z)=(z-x)^2 and q(z)=1, but that gives flat
+ * high frequency response in F(z). Better choice is p(z) = (z-u)*(z-v)*(z-w)
+ * and q(z) = z - r. To make F(z) better lowpass, one can cancel
+ * a resulting 1/z pole in F(z) by setting r=u*v*w. Then,
+ *
+ * G(z) = (z - u*v*w)*(z - 1) / [(z - u)*(z - v)*(z - w)]
+ * F(z) = (a z + b - a) / (z - 1) * H(z)
+ * H(z) = beta / (z - 1 + beta)
+ * beta = 1 - u*v*w
+ * a = [(1-u) + (1-v) + (1-w) - beta] / beta
+ * b = (1-u)*(1-v)*(1-w) / beta
+ *
+ * which corresponds to iteration for c(j):
+ *
+ * avg(j+1) = (1 - beta) avg(j) + beta delta(j)
+ * c(j+1) = c(j) + a [avg(j+1) - avg(j)] + b avg(j)
+ *
+ * So the controller operates on the running average,
+ * which gives the low-pass property for c(j).
+ *
+ * The simplest filter is obtained by putting the poles at
+ * u=v=w=(1-beta)**(1/3). Since beta << 1, computing the root
+ * can be avoided by expanding in series.
+ *
+ * Overshoot in impulse response could be reduced by moving one of the
+ * poles closer to z=1, but this increases the step response time.
+ */
+struct spa_bt_rate_control
+{
+ double avg;
+ double corr;
+};
+
+static void spa_bt_rate_control_init(struct spa_bt_rate_control *this, double level)
+{
+ this->avg = level;
+ this->corr = 1.0;
+}
+
+static double spa_bt_rate_control_update(struct spa_bt_rate_control *this, double level,
+ double target, double duration, double period)
+{
+ /*
+ * u = (1 - beta)^(1/3)
+ * x = a / beta
+ * y = b / beta
+ * a = (2 + u) * (1 - u)^2 / beta
+ * b = (1 - u)^3 / beta
+ * beta -> 0
+ */
+ const double beta = SPA_CLAMP(duration / period, 0, 0.5);
+ const double x = 1.0/3;
+ const double y = beta/27;
+ double avg;
+
+ avg = beta * level + (1 - beta) * this->avg;
+ this->corr += x * (avg - this->avg) / period
+ + y * (this->avg - target) / period;
+ this->avg = avg;
+
+ this->corr = SPA_CLAMP(this->corr,
+ 1 - BUFFERING_RATE_DIFF_MAX,
+ 1 + BUFFERING_RATE_DIFF_MAX);
+
+ return this->corr;
+}
+
+
+/** Windowed min/max */
+struct spa_bt_ptp
+{
+ union {
+ int32_t min;
+ int32_t mins[4];
+ };
+ union {
+ int32_t max;
+ int32_t maxs[4];
+ };
+ uint32_t pos;
+ uint32_t period;
+};
+
+struct spa_bt_decode_buffer
+{
+ struct spa_log *log;
+
+ uint32_t frame_size;
+ uint32_t rate;
+
+ uint8_t *buffer_decoded;
+ uint32_t buffer_size;
+ uint32_t buffer_reserve;
+ uint32_t write_index;
+ uint32_t read_index;
+
+ struct spa_bt_ptp spike; /**< spikes (long window) */
+ struct spa_bt_ptp packet_size; /**< packet size (short window) */
+
+ struct spa_bt_rate_control ctl;
+ double corr;
+
+ uint32_t prev_consumed;
+ uint32_t prev_avail;
+ uint32_t prev_duration;
+ uint32_t underrun;
+ uint32_t pos;
+
+ uint8_t received:1;
+ uint8_t buffering:1;
+};
+
+static void spa_bt_ptp_init(struct spa_bt_ptp *p, int32_t period)
+{
+ size_t i;
+
+ spa_zero(*p);
+ for (i = 0; i < SPA_N_ELEMENTS(p->mins); ++i) {
+ p->mins[i] = INT32_MAX;
+ p->maxs[i] = INT32_MIN;
+ }
+ p->period = period;
+}
+
+static void spa_bt_ptp_update(struct spa_bt_ptp *p, int32_t value, uint32_t duration)
+{
+ const size_t n = SPA_N_ELEMENTS(p->mins);
+ size_t i;
+
+ for (i = 0; i < n; ++i) {
+ p->mins[i] = SPA_MIN(p->mins[i], value);
+ p->maxs[i] = SPA_MAX(p->maxs[i], value);
+ }
+
+ p->pos += duration;
+ if (p->pos >= p->period / (n - 1)) {
+ p->pos = 0;
+ for (i = 1; i < SPA_N_ELEMENTS(p->mins); ++i) {
+ p->mins[i-1] = p->mins[i];
+ p->maxs[i-1] = p->maxs[i];
+ }
+ p->mins[n-1] = INT32_MAX;
+ p->maxs[n-1] = INT32_MIN;
+ }
+}
+
+static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log,
+ uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve)
+{
+ spa_zero(*this);
+ this->frame_size = frame_size;
+ this->rate = rate;
+ this->log = log;
+ this->buffer_reserve = this->frame_size * reserve;
+ this->buffer_size = this->frame_size * quantum_limit * 2;
+ this->buffer_size += this->buffer_reserve;
+ this->corr = 1.0;
+ this->buffering = true;
+
+ spa_bt_rate_control_init(&this->ctl, 0);
+
+ spa_bt_ptp_init(&this->spike, (uint64_t)this->rate * BUFFERING_LONG_MSEC / 1000);
+ spa_bt_ptp_init(&this->packet_size, (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000);
+
+ if ((this->buffer_decoded = malloc(this->buffer_size)) == NULL) {
+ this->buffer_size = 0;
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this)
+{
+ free(this->buffer_decoded);
+ spa_zero(*this);
+}
+
+static void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this)
+{
+ uint32_t avail;
+
+ spa_assert(this->read_index <= this->write_index);
+
+ if (this->read_index == this->write_index) {
+ this->read_index = 0;
+ this->write_index = 0;
+ goto done;
+ }
+
+ if (this->write_index > this->read_index + this->buffer_size - this->buffer_reserve) {
+ /* Drop data to keep buffer reserve free */
+ spa_log_info(this->log, "%p buffer overrun: dropping data", this);
+ this->read_index = this->write_index + this->buffer_reserve - this->buffer_size;
+ }
+
+ if (this->write_index < (this->buffer_size - this->buffer_reserve) / 2
+ || this->read_index == 0)
+ goto done;
+
+ avail = this->write_index - this->read_index;
+ spa_memmove(this->buffer_decoded,
+ SPA_PTROFF(this->buffer_decoded, this->read_index, void),
+ avail);
+ this->read_index = 0;
+ this->write_index = avail;
+
+done:
+ spa_assert(this->buffer_size - this->write_index >= this->buffer_reserve);
+}
+
+static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+ spa_bt_decode_buffer_compact(this);
+ spa_assert(this->buffer_size >= this->write_index);
+ *avail = this->buffer_size - this->write_index;
+ return SPA_PTROFF(this->buffer_decoded, this->write_index, void);
+}
+
+static void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+ spa_assert(size % this->frame_size == 0);
+ this->write_index += size;
+ this->received = true;
+ spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size);
+}
+
+static void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+ spa_assert(this->write_index >= this->read_index);
+ if (!this->buffering)
+ *avail = this->write_index - this->read_index;
+ else
+ *avail = 0;
+ return SPA_PTROFF(this->buffer_decoded, this->read_index, void);
+}
+
+static void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+ spa_assert(size % this->frame_size == 0);
+ this->read_index += size;
+}
+
+static void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this)
+{
+ int32_t size = (this->write_index - this->read_index) / this->frame_size;
+ int32_t level;
+
+ this->prev_avail = size * this->frame_size;
+ this->prev_consumed = this->prev_duration;
+
+ level = (int32_t)this->prev_avail/this->frame_size
+ - (int32_t)this->prev_duration;
+ this->corr = 1.0;
+
+ spa_bt_rate_control_init(&this->ctl, level);
+}
+
+static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration)
+{
+ const uint32_t data_size = samples * this->frame_size;
+ const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8);
+ const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration);
+ uint32_t avail;
+
+ if (SPA_UNLIKELY(duration != this->prev_duration)) {
+ this->prev_duration = duration;
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ if (SPA_UNLIKELY(this->buffering)) {
+ int32_t size = (this->write_index - this->read_index) / this->frame_size;
+
+ this->corr = 1.0;
+
+ spa_log_trace(this->log, "%p buffering size:%d", this, (int)size);
+
+ if (this->received &&
+ packet_size > 0 &&
+ size >= SPA_MAX(3*packet_size, (int32_t)duration))
+ this->buffering = false;
+ else
+ return;
+
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ spa_bt_decode_buffer_get_read(this, &avail);
+
+ if (this->received) {
+ const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000;
+ int32_t level, target;
+
+ /* Track buffer level */
+ level = (int32_t)(this->prev_avail/this->frame_size) - (int32_t)this->prev_consumed;
+ level = SPA_MAX(level, -max_level);
+ this->prev_consumed = SPA_MIN(this->prev_consumed, avg_period);
+
+ spa_bt_ptp_update(&this->spike, this->ctl.avg - level, this->prev_consumed);
+
+ /* Update target level */
+ target = BUFFERING_TARGET(this->spike.max, packet_size);
+
+ if (level > SPA_MAX(4 * target, 2*(int32_t)duration) &&
+ avail > data_size) {
+ /* Lagging too much: drop data */
+ uint32_t size = SPA_MIN(avail - data_size,
+ (level - target*5/2) * this->frame_size);
+
+ spa_bt_decode_buffer_read(this, size);
+ spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d",
+ this, (int)size/this->frame_size,
+ (int)level, (int)target);
+
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ this->pos += this->prev_consumed;
+ if (this->pos > this->rate) {
+ spa_log_debug(this->log,
+ "%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f",
+ this,
+ (int)this->ctl.avg,
+ (int)target,
+ (int)level,
+ (int)(avail / this->frame_size),
+ (int)this->spike.max,
+ (double)this->corr);
+ this->pos = 0;
+ }
+
+ this->corr = spa_bt_rate_control_update(&this->ctl,
+ level, target, this->prev_consumed, avg_period);
+
+ spa_bt_decode_buffer_get_read(this, &avail);
+
+ this->prev_consumed = 0;
+ this->prev_avail = avail;
+ this->underrun = 0;
+ this->received = false;
+ }
+
+ if (avail < data_size) {
+ spa_log_trace(this->log, "%p underrun samples:%d", this,
+ (data_size - avail) / this->frame_size);
+ this->underrun += samples;
+ if (this->underrun >= SPA_MIN((uint32_t)max_level, this->buffer_size / this->frame_size)) {
+ this->buffering = true;
+ spa_log_debug(this->log, "%p underrun too much: start buffering", this);
+ }
+ }
+
+ this->prev_consumed += samples;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h
new file mode 100644
index 0000000..5d194b3
--- /dev/null
+++ b/spa/plugins/bluez5/defs.h
@@ -0,0 +1,822 @@
+/* Spa Bluez5 Monitor
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_DEFS_H
+#define SPA_BLUEZ5_DEFS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <math.h>
+
+#include <spa/support/dbus.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/hook.h>
+
+#include <dbus/dbus.h>
+
+#include "config.h"
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1"
+#define BLUEZ_PROFILE_INTERFACE BLUEZ_SERVICE ".Profile1"
+#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1"
+#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1"
+#define BLUEZ_MEDIA_INTERFACE BLUEZ_SERVICE ".Media1"
+#define BLUEZ_MEDIA_ENDPOINT_INTERFACE BLUEZ_SERVICE ".MediaEndpoint1"
+#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1"
+#define BLUEZ_INTERFACE_BATTERY_PROVIDER BLUEZ_SERVICE ".BatteryProvider1"
+#define BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER BLUEZ_SERVICE ".BatteryProviderManager1"
+
+#define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager"
+#define DBUS_SIGNAL_INTERFACES_ADDED "InterfacesAdded"
+#define DBUS_SIGNAL_INTERFACES_REMOVED "InterfacesRemoved"
+#define DBUS_SIGNAL_PROPERTIES_CHANGED "PropertiesChanged"
+
+#define PIPEWIRE_BATTERY_PROVIDER "/org/freedesktop/pipewire/battery"
+
+#define OBJECT_MANAGER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"org.freedesktop.DBus.ObjectManager\">\n" \
+ " <method name=\"GetManagedObjects\">\n" \
+ " <arg name=\"objects\" direction=\"out\" type=\"a{oa{sa{sv}}}\"/>\n" \
+ " </method>\n" \
+ " <signal name=\"InterfacesAdded\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"a{sa{sv}}\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"InterfacesRemoved\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"as\"/>\n" \
+ " </signal>\n" \
+ " </interface>\n" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ " <node name=\"A2DPSink\"/>\n" \
+ " <node name=\"A2DPSource\"/>\n" \
+ "</node>\n"
+
+#define ENDPOINT_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" BLUEZ_MEDIA_ENDPOINT_INTERFACE "\">" \
+ " <method name=\"SetConfiguration\">" \
+ " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"properties\" direction=\"in\" type=\"ay\"/>" \
+ " </method>" \
+ " <method name=\"SelectConfiguration\">" \
+ " <arg name=\"capabilities\" direction=\"in\" type=\"ay\"/>" \
+ " <arg name=\"configuration\" direction=\"out\" type=\"ay\"/>" \
+ " </method>" \
+ " <method name=\"ClearConfiguration\">" \
+ " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \
+ " </method>" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define PROFILE_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" BLUEZ_PROFILE_INTERFACE "\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"RequestDisconnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
+ " <arg name=\"opts\" direction=\"in\" type=\"a{sv}\"/>" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported"
+
+#define SPA_BT_UUID_A2DP_SOURCE "0000110a-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_A2DP_SINK "0000110b-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS "00001108-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS_ALT "00001131-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_AG "00001112-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_HF "0000111e-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_AG "0000111f-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_PACS "00001850-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SINK "00002bc9-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SOURCE "00002bcb-0000-1000-8000-00805f9b34fb"
+
+#define PROFILE_HSP_AG "/Profile/HSPAG"
+#define PROFILE_HSP_HS "/Profile/HSPHS"
+#define PROFILE_HFP_AG "/Profile/HFPAG"
+#define PROFILE_HFP_HF "/Profile/HFPHF"
+
+#define HSP_HS_DEFAULT_CHANNEL 3
+
+#define SOURCE_ID_BLUETOOTH 0x1 /* Bluetooth SIG */
+#define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */
+
+#define BUS_TYPE_USB 1
+#define BUS_TYPE_OTHER 255
+
+#define HFP_AUDIO_CODEC_CVSD 0x01
+#define HFP_AUDIO_CODEC_MSBC 0x02
+
+#define MEDIA_OBJECT_MANAGER_PATH "/MediaEndpoint"
+#define A2DP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSink"
+#define A2DP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSource"
+
+#define BAP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSink"
+#define BAP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSource"
+
+#define SPA_BT_UNKNOWN_DELAY 0
+
+#define SPA_BT_NO_BATTERY ((uint8_t)255)
+
+/* HFP uses SBC encoding with precisely defined parameters. Hence, the size
+ * of the input (number of PCM samples) and output is known up front. */
+#define MSBC_DECODED_SIZE 240
+#define MSBC_ENCODED_SIZE 60 /* 2 bytes header + 57 mSBC payload + 1 byte padding */
+#define MSBC_PAYLOAD_SIZE 57
+
+enum spa_bt_media_direction {
+ SPA_BT_MEDIA_SOURCE,
+ SPA_BT_MEDIA_SINK,
+};
+
+enum spa_bt_profile {
+ SPA_BT_PROFILE_NULL = 0,
+ SPA_BT_PROFILE_BAP_SINK = (1 << 0),
+ SPA_BT_PROFILE_BAP_SOURCE = (1 << 1),
+ SPA_BT_PROFILE_A2DP_SINK = (1 << 2),
+ SPA_BT_PROFILE_A2DP_SOURCE = (1 << 3),
+ SPA_BT_PROFILE_HSP_HS = (1 << 4),
+ SPA_BT_PROFILE_HSP_AG = (1 << 5),
+ SPA_BT_PROFILE_HFP_HF = (1 << 6),
+ SPA_BT_PROFILE_HFP_AG = (1 << 7),
+
+ SPA_BT_PROFILE_A2DP_DUPLEX = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE),
+ SPA_BT_PROFILE_BAP_DUPLEX = (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE),
+ SPA_BT_PROFILE_HEADSET_HEAD_UNIT = (SPA_BT_PROFILE_HSP_HS | SPA_BT_PROFILE_HFP_HF),
+ SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY = (SPA_BT_PROFILE_HSP_AG | SPA_BT_PROFILE_HFP_AG),
+ SPA_BT_PROFILE_HEADSET_AUDIO = (SPA_BT_PROFILE_HEADSET_HEAD_UNIT | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY),
+
+ SPA_BT_PROFILE_MEDIA_SINK = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK),
+ SPA_BT_PROFILE_MEDIA_SOURCE = (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE),
+};
+
+static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid)
+{
+ if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SOURCE) == 0)
+ return SPA_BT_PROFILE_A2DP_SOURCE;
+ else if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SINK) == 0)
+ return SPA_BT_PROFILE_A2DP_SINK;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS) == 0)
+ return SPA_BT_PROFILE_HSP_HS;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS_ALT) == 0)
+ return SPA_BT_PROFILE_HSP_HS;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_AG) == 0)
+ return SPA_BT_PROFILE_HSP_AG;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HFP_HF) == 0)
+ return SPA_BT_PROFILE_HFP_HF;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HFP_AG) == 0)
+ return SPA_BT_PROFILE_HFP_AG;
+ else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SINK) == 0)
+ return SPA_BT_PROFILE_BAP_SINK;
+ else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SOURCE) == 0)
+ return SPA_BT_PROFILE_BAP_SOURCE;
+ else
+ return 0;
+}
+int spa_bt_profiles_from_json_array(const char *str);
+
+int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id,
+ uint16_t product_id, char *vendor_str, int vendor_str_size,
+ char *product_str, int product_str_size);
+
+enum spa_bt_hfp_ag_feature {
+ SPA_BT_HFP_AG_FEATURE_NONE = (0),
+ SPA_BT_HFP_AG_FEATURE_3WAY = (1 << 0),
+ SPA_BT_HFP_AG_FEATURE_ECNR = (1 << 1),
+ SPA_BT_HFP_AG_FEATURE_VOICE_RECOG = (1 << 2),
+ SPA_BT_HFP_AG_FEATURE_IN_BAND_RING_TONE = (1 << 3),
+ SPA_BT_HFP_AG_FEATURE_ATTACH_VOICE_TAG = (1 << 4),
+ SPA_BT_HFP_AG_FEATURE_REJECT_CALL = (1 << 5),
+ SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS = (1 << 6),
+ SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_CONTROL = (1 << 7),
+ SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE = (1 << 8),
+ SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION = (1 << 9),
+ SPA_BT_HFP_AG_FEATURE_HF_INDICATORS = (1 << 10),
+ SPA_BT_HFP_AG_FEATURE_ESCO_S4 = (1 << 11),
+};
+
+enum spa_bt_hfp_sdp_ag_features {
+ SPA_BT_HFP_SDP_AG_FEATURE_NONE = (0),
+ SPA_BT_HFP_SDP_AG_FEATURE_3WAY = (1 << 0),
+ SPA_BT_HFP_SDP_AG_FEATURE_ECNR = (1 << 1),
+ SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG = (1 << 2),
+ SPA_BT_HFP_SDP_AG_FEATURE_IN_BAND_RING_TONE = (1 << 3),
+ SPA_BT_HFP_SDP_AG_FEATURE_ATTACH_VOICE_TAG = (1 << 4),
+ SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH = (1 << 5),
+};
+
+enum spa_bt_hfp_hf_feature {
+ SPA_BT_HFP_HF_FEATURE_NONE = (0),
+ SPA_BT_HFP_HF_FEATURE_ECNR = (1 << 0),
+ SPA_BT_HFP_HF_FEATURE_3WAY = (1 << 1),
+ SPA_BT_HFP_HF_FEATURE_CLIP = (1 << 2),
+ SPA_BT_HFP_HF_FEATURE_VOICE_RECOGNITION = (1 << 3),
+ SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL = (1 << 4),
+ SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS = (1 << 5),
+ SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_CONTROL = (1 << 6),
+ SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION = (1 << 7),
+ SPA_BT_HFP_HF_FEATURE_HF_INDICATORS = (1 << 8),
+ SPA_BT_HFP_HF_FEATURE_ESCO_S4 = (1 << 9),
+};
+
+/* https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Hands-Free%20Profile.pdf */
+enum spa_bt_hfp_hf_indicator {
+ SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY = 1,
+ SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL = 2,
+};
+
+/* HFP Command AT+IPHONEACCEV
+ * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf */
+enum spa_bt_hfp_hf_iphoneaccev_keys {
+ SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL = 1,
+ SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE = 2,
+};
+
+/* HFP Command AT+XAPL
+ * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
+ * Bits 0, 5 and above are reserved. */
+enum spa_bt_hfp_hf_xapl_features {
+ SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING = (1 << 1),
+ SPA_BT_HFP_HF_XAPL_FEATURE_DOCKED_OR_POWERED = (1 << 2),
+ SPA_BT_HFP_HF_XAPL_FEATURE_SIRI_STATUS_REPORTING = (1 << 3),
+ SPA_BT_HFP_HF_XAPL_FEATURE_NOISE_REDUCTION_REPORTING = (1 << 4),
+};
+
+enum spa_bt_hfp_sdp_hf_features {
+ SPA_BT_HFP_SDP_HF_FEATURE_NONE = (0),
+ SPA_BT_HFP_SDP_HF_FEATURE_ECNR = (1 << 0),
+ SPA_BT_HFP_SDP_HF_FEATURE_3WAY = (1 << 1),
+ SPA_BT_HFP_SDP_HF_FEATURE_CLIP = (1 << 2),
+ SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOGNITION = (1 << 3),
+ SPA_BT_HFP_SDP_HF_FEATURE_REMOTE_VOLUME_CONTROL = (1 << 4),
+ SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH = (1 << 5),
+};
+
+static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) {
+ switch (profile) {
+ case SPA_BT_PROFILE_A2DP_SOURCE:
+ return "a2dp-source";
+ case SPA_BT_PROFILE_A2DP_SINK:
+ return "a2dp-sink";
+ case SPA_BT_PROFILE_A2DP_DUPLEX:
+ return "a2dp-duplex";
+ case SPA_BT_PROFILE_HSP_HS:
+ case SPA_BT_PROFILE_HFP_HF:
+ case SPA_BT_PROFILE_HEADSET_HEAD_UNIT:
+ return "headset-head-unit";
+ case SPA_BT_PROFILE_HSP_AG:
+ case SPA_BT_PROFILE_HFP_AG:
+ case SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY:
+ return "headset-audio-gateway";
+ case SPA_BT_PROFILE_HEADSET_AUDIO:
+ return "headset-audio";
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ return "bap-source";
+ case SPA_BT_PROFILE_BAP_SINK:
+ return "bap-sink";
+ case SPA_BT_PROFILE_BAP_DUPLEX:
+ return "bap-duplex";
+ default:
+ break;
+ }
+ return "unknown";
+}
+
+struct spa_bt_monitor;
+struct spa_bt_backend;
+struct spa_bt_player;
+
+struct spa_bt_adapter {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_player *dummy_player;
+ char *path;
+ char *alias;
+ char *address;
+ char *name;
+ int bus_type;
+ uint16_t source_id;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint16_t version_id;
+ uint32_t bluetooth_class;
+ uint32_t profiles;
+ int powered;
+ unsigned int has_msbc:1;
+ unsigned int msbc_probed:1;
+ unsigned int endpoints_registered:1;
+ unsigned int application_registered:1;
+ unsigned int player_registered:1;
+ unsigned int has_battery_provider:1;
+ unsigned int battery_provider_unavailable:1;
+};
+
+enum spa_bt_form_factor {
+ SPA_BT_FORM_FACTOR_UNKNOWN,
+ SPA_BT_FORM_FACTOR_HEADSET,
+ SPA_BT_FORM_FACTOR_HANDSFREE,
+ SPA_BT_FORM_FACTOR_MICROPHONE,
+ SPA_BT_FORM_FACTOR_SPEAKER,
+ SPA_BT_FORM_FACTOR_HEADPHONE,
+ SPA_BT_FORM_FACTOR_PORTABLE,
+ SPA_BT_FORM_FACTOR_CAR,
+ SPA_BT_FORM_FACTOR_HIFI,
+ SPA_BT_FORM_FACTOR_PHONE,
+};
+
+static inline const char *spa_bt_form_factor_name(enum spa_bt_form_factor ff)
+{
+ switch (ff) {
+ case SPA_BT_FORM_FACTOR_HEADSET:
+ return "headset";
+ case SPA_BT_FORM_FACTOR_HANDSFREE:
+ return "hands-free";
+ case SPA_BT_FORM_FACTOR_MICROPHONE:
+ return "microphone";
+ case SPA_BT_FORM_FACTOR_SPEAKER:
+ return "speaker";
+ case SPA_BT_FORM_FACTOR_HEADPHONE:
+ return "headphone";
+ case SPA_BT_FORM_FACTOR_PORTABLE:
+ return "portable";
+ case SPA_BT_FORM_FACTOR_CAR:
+ return "car";
+ case SPA_BT_FORM_FACTOR_HIFI:
+ return "hifi";
+ case SPA_BT_FORM_FACTOR_PHONE:
+ return "phone";
+ case SPA_BT_FORM_FACTOR_UNKNOWN:
+ default:
+ return "unknown";
+ }
+}
+
+static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t bluetooth_class)
+{
+ uint32_t major, minor;
+ /* See Bluetooth Assigned Numbers:
+ * https://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm */
+ major = (bluetooth_class >> 8) & 0x1F;
+ minor = (bluetooth_class >> 2) & 0x3F;
+
+ switch (major) {
+ case 2:
+ return SPA_BT_FORM_FACTOR_PHONE;
+ case 4:
+ switch (minor) {
+ case 1:
+ return SPA_BT_FORM_FACTOR_HEADSET;
+ case 2:
+ return SPA_BT_FORM_FACTOR_HANDSFREE;
+ case 4:
+ return SPA_BT_FORM_FACTOR_MICROPHONE;
+ case 5:
+ return SPA_BT_FORM_FACTOR_SPEAKER;
+ case 6:
+ return SPA_BT_FORM_FACTOR_HEADPHONE;
+ case 7:
+ return SPA_BT_FORM_FACTOR_PORTABLE;
+ case 8:
+ return SPA_BT_FORM_FACTOR_CAR;
+ case 10:
+ return SPA_BT_FORM_FACTOR_HIFI;
+ }
+ }
+ return SPA_BT_FORM_FACTOR_UNKNOWN;
+}
+
+struct spa_bt_media_codec_switch;
+struct spa_bt_transport;
+
+struct spa_bt_device_events {
+#define SPA_VERSION_BT_DEVICE_EVENTS 0
+ uint32_t version;
+
+ /** Device connection status */
+ void (*connected) (void *data, bool connected);
+
+ /** Codec switching completed */
+ void (*codec_switched) (void *data, int status);
+
+ /** Profile configuration changed */
+ void (*profiles_changed) (void *data, uint32_t prev_profiles, uint32_t prev_connected);
+
+ /** Device freed */
+ void (*destroy) (void *data);
+};
+
+struct media_codec;
+
+struct spa_bt_device {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_adapter *adapter;
+ uint32_t id;
+ char *path;
+ char *alias;
+ char *address;
+ char *adapter_path;
+ char *battery_path;
+ char *name;
+ char *icon;
+ uint16_t source_id;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint16_t version_id;
+ uint32_t bluetooth_class;
+ uint16_t appearance;
+ uint16_t RSSI;
+ int paired;
+ int trusted;
+ int connected;
+ int blocked;
+ uint32_t profiles;
+ uint32_t connected_profiles;
+ uint32_t reconnect_profiles;
+ int reconnect_state;
+ struct spa_source timer;
+ struct spa_list remote_endpoint_list;
+ struct spa_list transport_list;
+ struct spa_list codec_switch_list;
+ uint8_t battery;
+ int has_battery;
+
+ uint32_t hw_volume_profiles;
+ /* Even though A2DP volume is exposed on transport interface, the
+ * volume activation info would not be variate between transports
+ * under same device. So it's safe to cache activation info here. */
+ bool a2dp_volume_active[2];
+
+ uint64_t last_bluez_action_time;
+
+ struct spa_hook_list listener_list;
+ bool added;
+
+ const struct spa_dict *settings;
+
+ DBusPendingCall *battery_pending_call;
+
+ const struct media_codec *preferred_codec;
+};
+
+struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path);
+struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address);
+int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
+int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
+int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force);
+int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs);
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink);
+const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink);
+int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec);
+int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec);
+int spa_bt_device_release_transports(struct spa_bt_device *device);
+int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage);
+void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device);
+
+#define spa_bt_device_emit(d,m,v,...) spa_hook_list_call(&(d)->listener_list, \
+ struct spa_bt_device_events, \
+ m, v, ##__VA_ARGS__)
+#define spa_bt_device_emit_connected(d,...) spa_bt_device_emit(d, connected, 0, __VA_ARGS__)
+#define spa_bt_device_emit_codec_switched(d,...) spa_bt_device_emit(d, codec_switched, 0, __VA_ARGS__)
+#define spa_bt_device_emit_profiles_changed(d,...) spa_bt_device_emit(d, profiles_changed, 0, __VA_ARGS__)
+#define spa_bt_device_emit_destroy(d) spa_bt_device_emit(d, destroy, 0)
+#define spa_bt_device_add_listener(d,listener,events,data) \
+ spa_hook_list_append(&(d)->listener_list, listener, events, data)
+
+struct spa_bt_sco_io;
+
+struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop, int fd, uint16_t read_mtu, uint16_t write_mtu);
+void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io);
+void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *userdata, uint8_t *data, int size), void *userdata);
+void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *userdata), void *userdata);
+int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *data, int size);
+
+#define SPA_BT_VOLUME_ID_RX 0
+#define SPA_BT_VOLUME_ID_TX 1
+#define SPA_BT_VOLUME_ID_TERM 2
+
+#define SPA_BT_VOLUME_INVALID -1
+#define SPA_BT_VOLUME_HS_MAX 15
+#define SPA_BT_VOLUME_A2DP_MAX 127
+
+enum spa_bt_transport_state {
+ SPA_BT_TRANSPORT_STATE_IDLE,
+ SPA_BT_TRANSPORT_STATE_PENDING,
+ SPA_BT_TRANSPORT_STATE_ACTIVE,
+};
+
+struct spa_bt_transport_events {
+#define SPA_VERSION_BT_TRANSPORT_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+ void (*delay_changed) (void *data);
+ void (*state_changed) (void *data, enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state);
+ void (*volume_changed) (void *data);
+};
+
+struct spa_bt_transport_implementation {
+#define SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION 0
+ uint32_t version;
+
+ int (*acquire) (void *data, bool optional);
+ int (*release) (void *data);
+ int (*set_volume) (void *data, int id, float volume);
+ int (*destroy) (void *data);
+};
+
+struct spa_bt_transport_volume {
+ bool active;
+ float volume;
+ int hw_volume_max;
+
+ /* XXX: items below should be put to user_data */
+ int hw_volume;
+ int new_hw_volume;
+};
+
+struct spa_bt_transport {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_backend *backend;
+ char *path;
+ struct spa_bt_device *device;
+ struct spa_list device_link;
+ enum spa_bt_profile profile;
+ enum spa_bt_transport_state state;
+ const struct media_codec *media_codec;
+ unsigned int codec;
+ void *configuration;
+ int configuration_len;
+ char *endpoint_path;
+ bool bap_initiator;
+ struct spa_list bap_transport_linked;
+
+ uint32_t n_channels;
+ uint32_t channels[64];
+
+ struct spa_bt_transport_volume volumes[SPA_BT_VOLUME_ID_TERM];
+
+ int acquire_refcount;
+ bool acquired;
+ bool keepalive;
+ int fd;
+ uint16_t read_mtu;
+ uint16_t write_mtu;
+ uint16_t delay;
+
+ struct spa_bt_sco_io *sco_io;
+
+ struct spa_source volume_timer;
+ struct spa_source release_timer;
+
+ struct spa_hook_list listener_list;
+ struct spa_callbacks impl;
+
+ /* user_data must be the last item in the struct */
+ void *user_data;
+};
+
+struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra);
+void spa_bt_transport_free(struct spa_bt_transport *transport);
+void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state);
+struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path);
+struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor,
+ bool (*callback) (struct spa_bt_transport *t, const void *data),
+ const void *data);
+int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *transport);
+bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport);
+
+int spa_bt_transport_acquire(struct spa_bt_transport *t, bool optional);
+int spa_bt_transport_release(struct spa_bt_transport *t);
+int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive);
+int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop);
+
+#define spa_bt_transport_emit(t,m,v,...) spa_hook_list_call(&(t)->listener_list, \
+ struct spa_bt_transport_events, \
+ m, v, ##__VA_ARGS__)
+#define spa_bt_transport_emit_destroy(t) spa_bt_transport_emit(t, destroy, 0)
+#define spa_bt_transport_emit_delay_changed(t) spa_bt_transport_emit(t, delay_changed, 0)
+#define spa_bt_transport_emit_state_changed(t,...) spa_bt_transport_emit(t, state_changed, 0, __VA_ARGS__)
+#define spa_bt_transport_emit_volume_changed(t) spa_bt_transport_emit(t, volume_changed, 0)
+
+#define spa_bt_transport_add_listener(t,listener,events,data) \
+ spa_hook_list_append(&(t)->listener_list, listener, events, data)
+
+#define spa_bt_transport_set_implementation(t,_impl,_data) \
+ (t)->impl = SPA_CALLBACKS_INIT(_impl, _data)
+
+#define spa_bt_transport_impl(t,m,v,...) \
+({ \
+ int res = 0; \
+ spa_callbacks_call_res(&(t)->impl, \
+ struct spa_bt_transport_implementation, \
+ res, m, v, ##__VA_ARGS__); \
+ res; \
+})
+
+#define spa_bt_transport_destroy(t) spa_bt_transport_impl(t, destroy, 0)
+#define spa_bt_transport_set_volume(t,...) spa_bt_transport_impl(t, set_volume, 0, __VA_ARGS__)
+
+static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(const char *value)
+{
+ if (strcasecmp("idle", value) == 0)
+ return SPA_BT_TRANSPORT_STATE_IDLE;
+ else if (strcasecmp("pending", value) == 0)
+ return SPA_BT_TRANSPORT_STATE_PENDING;
+ else if (strcasecmp("active", value) == 0)
+ return SPA_BT_TRANSPORT_STATE_ACTIVE;
+ else
+ return SPA_BT_TRANSPORT_STATE_IDLE;
+}
+
+#define DEFAULT_AG_VOLUME 1.0f
+#define DEFAULT_RX_VOLUME 1.0f
+#define DEFAULT_TX_VOLUME 0.064f /* spa_bt_volume_hw_to_linear(40, 100) */
+
+/* AVRCP/HSP volume is considered as percentage, so map it to pulseaudio (cubic) volume. */
+static inline uint32_t spa_bt_volume_linear_to_hw(double v, uint32_t hw_volume_max)
+{
+ if (v <= 0.0)
+ return 0;
+ if (v >= 1.0)
+ return hw_volume_max;
+ return SPA_CLAMP((uint64_t) lround(cbrt(v) * hw_volume_max),
+ 0u, hw_volume_max);
+}
+
+static inline double spa_bt_volume_hw_to_linear(uint32_t v, uint32_t hw_volume_max)
+{
+ double f;
+ if (v <= 0)
+ return 0.0;
+ if (v >= hw_volume_max)
+ return 1.0;
+ f = ((double) v / hw_volume_max);
+ return f * f * f;
+}
+
+enum spa_bt_feature {
+ SPA_BT_FEATURE_MSBC = (1 << 0),
+ SPA_BT_FEATURE_MSBC_ALT1 = (1 << 1),
+ SPA_BT_FEATURE_MSBC_ALT1_RTL = (1 << 2),
+ SPA_BT_FEATURE_HW_VOLUME = (1 << 3),
+ SPA_BT_FEATURE_HW_VOLUME_MIC = (1 << 4),
+ SPA_BT_FEATURE_SBC_XQ = (1 << 5),
+ SPA_BT_FEATURE_FASTSTREAM = (1 << 6),
+ SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7),
+};
+
+struct spa_bt_quirks;
+
+struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log);
+int spa_bt_quirks_get_features(const struct spa_bt_quirks *quirks,
+ const struct spa_bt_adapter *adapter,
+ const struct spa_bt_device *device,
+ uint32_t *features);
+void spa_bt_quirks_destroy(struct spa_bt_quirks *quirks);
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter);
+
+struct spa_bt_backend_implementation {
+#define SPA_VERSION_BT_BACKEND_IMPLEMENTATION 0
+ uint32_t version;
+
+ int (*free) (void *data);
+ int (*register_profiles) (void *data);
+ int (*unregister_profiles) (void *data);
+ int (*ensure_codec) (void *data, struct spa_bt_device *device, unsigned int codec);
+ int (*supports_codec) (void *data, struct spa_bt_device *device, unsigned int codec);
+};
+
+struct spa_bt_backend {
+ struct spa_callbacks impl;
+ const char *name;
+ bool available;
+ bool exclusive;
+};
+
+#define spa_bt_backend_set_implementation(b,_impl,_data) \
+ (b)->impl = SPA_CALLBACKS_INIT(_impl, _data)
+
+#define spa_bt_backend_impl(b,m,v,...) \
+({ \
+ int res = -ENOTSUP; \
+ if (b) \
+ spa_callbacks_call_res(&(b)->impl, \
+ struct spa_bt_backend_implementation, \
+ res, m, v, ##__VA_ARGS__); \
+ res; \
+})
+
+#define spa_bt_backend_free(b) spa_bt_backend_impl(b, free, 0)
+#define spa_bt_backend_register_profiles(b) spa_bt_backend_impl(b, register_profiles, 0)
+#define spa_bt_backend_unregister_profiles(b) spa_bt_backend_impl(b, unregister_profiles, 0)
+#define spa_bt_backend_ensure_codec(b,...) spa_bt_backend_impl(b, ensure_codec, 0, __VA_ARGS__)
+#define spa_bt_backend_supports_codec(b,...) spa_bt_backend_impl(b, supports_codec, 0, __VA_ARGS__)
+
+static inline struct spa_bt_backend *dummy_backend_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ return NULL;
+}
+
+#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE
+struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_native_new dummy_backend_new
+#endif
+
+#define OFONO_SERVICE "org.ofono"
+#ifdef HAVE_BLUEZ_5_BACKEND_OFONO
+struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_ofono_new dummy_backend_new
+#endif
+
+#define HSPHFPD_SERVICE "org.hsphfpd"
+#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD
+struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_hsphfpd_new dummy_backend_new
+#endif
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BLUEZ5_DEFS_H */
diff --git a/spa/plugins/bluez5/hci.c b/spa/plugins/bluez5/hci.c
new file mode 100644
index 0000000..88dc1fc
--- /dev/null
+++ b/spa/plugins/bluez5/hci.c
@@ -0,0 +1,93 @@
+/* Spa HSP/HFP native backend HCI support
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/uio.h>
+#include <sys/socket.h>
+
+#include "defs.h"
+
+#ifndef HAVE_BLUEZ_5_HCI
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter)
+{
+ if (adapter->msbc_probed)
+ return adapter->has_msbc;
+ return -EOPNOTSUPP;
+}
+
+#else
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/hci.h>
+#include <bluetooth/hci_lib.h>
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter)
+{
+ int hci_id, res;
+ int sock = -1;
+ uint8_t features[8], max_page = 0;
+ struct sockaddr_hci a;
+ const char *str;
+
+ if (adapter->msbc_probed)
+ return adapter->has_msbc;
+
+ str = strrchr(adapter->path, '/'); /* hciXX */
+ if (str == NULL || sscanf(str, "/hci%d", &hci_id) != 1 || hci_id < 0)
+ return -ENOENT;
+
+ sock = socket(AF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC, BTPROTO_HCI);
+ if (sock < 0)
+ goto error;
+
+ memset(&a, 0, sizeof(a));
+ a.hci_family = AF_BLUETOOTH;
+ a.hci_dev = hci_id;
+ if (bind(sock, (struct sockaddr *) &a, sizeof(a)) < 0)
+ goto error;
+
+ if (hci_read_local_ext_features(sock, 0, &max_page, features, 1000) < 0)
+ goto error;
+
+ close(sock);
+
+ adapter->msbc_probed = true;
+ adapter->has_msbc = ((features[2] & LMP_TRSP_SCO) && (features[3] & LMP_ESCO)) ? 1 : 0;
+ return adapter->has_msbc;
+
+error:
+ res = -errno;
+ if (sock >= 0)
+ close(sock);
+ return res;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/media-codecs.c b/spa/plugins/bluez5/media-codecs.c
new file mode 100644
index 0000000..28445fc
--- /dev/null
+++ b/spa/plugins/bluez5/media-codecs.c
@@ -0,0 +1,212 @@
+/*
+ * BlueALSA - bluez-a2dp.c
+ * Copyright (c) 2016-2017 Arkadiusz Bokowy
+ *
+ * This file is a part of bluez-alsa.
+ *
+ * This project is licensed under the terms of the MIT license.
+ *
+ */
+
+#include <spa/utils/string.h>
+
+#include "media-codecs.h"
+
+int media_codec_select_config(const struct media_codec_config configs[], size_t n,
+ uint32_t cap, int preferred_value)
+{
+ size_t i;
+ int *scores, res;
+ unsigned int max_priority;
+
+ if (n == 0)
+ return -EINVAL;
+
+ scores = calloc(n, sizeof(int));
+ if (scores == NULL)
+ return -errno;
+
+ max_priority = configs[0].priority;
+ for (i = 1; i < n; ++i) {
+ if (configs[i].priority > max_priority)
+ max_priority = configs[i].priority;
+ }
+
+ for (i = 0; i < n; ++i) {
+ if (!(configs[i].config & cap)) {
+ scores[i] = -1;
+ continue;
+ }
+ if (configs[i].value == preferred_value)
+ scores[i] = 100 * (max_priority + 1);
+ else if (configs[i].value > preferred_value)
+ scores[i] = 10 * (max_priority + 1);
+ else
+ scores[i] = 1;
+
+ scores[i] *= configs[i].priority + 1;
+ }
+
+ res = 0;
+ for (i = 1; i < n; ++i) {
+ if (scores[i] > scores[res])
+ res = i;
+ }
+
+ if (scores[res] < 0)
+ res = -EINVAL;
+
+ free(scores);
+ return res;
+}
+
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings)
+{
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ int res;
+
+ if (codec_id != codec->codec_id)
+ return false;
+
+ if (caps == NULL)
+ return false;
+
+ res = codec->select_config(codec, 0, caps, caps_size, info, global_settings, config);
+ if (res < 0)
+ return false;
+
+ if (codec->bap)
+ return true;
+ else
+ return ((size_t)res == caps_size);
+}
+
+#ifdef CODEC_PLUGIN
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_bluez5_codec_a2dp bluez5_codec_a2dp;
+};
+
+static int
+impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Bluez5CodecMedia))
+ *interface = &this->bluez5_codec_a2dp;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int
+impl_clear(struct spa_handle *handle)
+{
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->bluez5_codec_a2dp.codecs = codec_plugin_media_codecs;
+ this->bluez5_codec_a2dp.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Bluez5CodecMedia,
+ SPA_VERSION_BLUEZ5_CODEC_MEDIA,
+ NULL,
+ this);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Bluez5CodecMedia,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+
+ return 1;
+}
+
+static const struct spa_dict_item handle_info_items[] = {
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluetooth codec plugin" },
+};
+
+static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items);
+
+static struct spa_handle_factory handle_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NULL,
+ &handle_info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (handle_factory.name == NULL)
+ handle_factory.name = codec_plugin_factory_name;
+
+ switch (*index) {
+ case 0:
+ *factory = &handle_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h
new file mode 100644
index 0000000..d3447db
--- /dev/null
+++ b/spa/plugins/bluez5/media-codecs.h
@@ -0,0 +1,178 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BLUEZ5_A2DP_CODECS_H_
+#define SPA_BLUEZ5_A2DP_CODECS_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/bluetooth/audio.h>
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/support/log.h>
+
+#include "a2dp-codec-caps.h"
+#include "bap-codec-caps.h"
+
+/*
+ * The codec plugin SPA interface is private. The version should be incremented
+ * when any of the structs or semantics change.
+ */
+
+#define SPA_TYPE_INTERFACE_Bluez5CodecMedia SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private"
+
+#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 7
+
+struct spa_bluez5_codec_a2dp {
+ struct spa_interface iface;
+ const struct media_codec * const *codecs; /**< NULL terminated array */
+};
+
+#define MEDIA_CODEC_FACTORY_NAME(basename) (SPA_NAME_API_CODEC_BLUEZ5_MEDIA "." basename)
+
+#ifdef CODEC_PLUGIN
+#define MEDIA_CODEC_EXPORT_DEF(basename,...) \
+ const char *codec_plugin_factory_name = MEDIA_CODEC_FACTORY_NAME(basename); \
+ static const struct media_codec * const codec_plugin_media_codec_list[] = { __VA_ARGS__, NULL }; \
+ const struct media_codec * const * const codec_plugin_media_codecs = codec_plugin_media_codec_list;
+
+extern const struct media_codec * const * const codec_plugin_media_codecs;
+extern const char *codec_plugin_factory_name;
+#endif
+
+#define MEDIA_CODEC_FLAG_SINK (1 << 0)
+
+#define A2DP_CODEC_DEFAULT_RATE 48000
+#define A2DP_CODEC_DEFAULT_CHANNELS 2
+
+enum {
+ NEED_FLUSH_NO = 0,
+ NEED_FLUSH_ALL = 1,
+ NEED_FLUSH_FRAGMENT = 2,
+};
+
+struct media_codec_audio_info {
+ uint32_t rate;
+ uint32_t channels;
+};
+
+struct media_codec {
+ enum spa_bluetooth_audio_codec id;
+ uint8_t codec_id;
+ a2dp_vendor_codec_t vendor;
+
+ bool bap;
+
+ const char *name;
+ const char *description;
+ const char *endpoint_name; /**< Endpoint name. If NULL, same as name */
+ const struct spa_dict *info;
+
+ const size_t send_buf_size;
+
+ const struct media_codec *duplex_codec; /**< Codec for non-standard A2DP duplex channel */
+
+ struct spa_log *log;
+
+ /** If fill_caps is NULL, no endpoint is registered (for sharing with another codec). */
+ int (*fill_caps) (const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE]);
+
+ int (*select_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]);
+ int (*enum_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *builder, struct spa_pod **param);
+ int (*validate_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info);
+ int (*get_qos)(const struct media_codec *codec,
+ const void *config, size_t config_size,
+ const struct bap_endpoint_qos *endpoint_qos,
+ struct bap_codec_qos *qos);
+
+ /** qsort comparison sorting caps in order of preference for the codec.
+ * Used in codec switching to select best remote endpoints.
+ * The caps handed in correspond to this codec_id, but are
+ * otherwise not checked beforehand.
+ */
+ int (*caps_preference_cmp) (const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings);
+
+ void *(*init_props) (const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings);
+ void (*clear_props) (void *);
+ int (*enum_props) (void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *builder, struct spa_pod **param);
+ int (*set_props) (void *props, const struct spa_pod *param);
+
+ void *(*init) (const struct media_codec *codec, uint32_t flags, void *config, size_t config_size,
+ const struct spa_audio_info *info, void *props, size_t mtu);
+ void (*deinit) (void *data);
+
+ int (*update_props) (void *data, void *props);
+
+ int (*get_block_size) (void *data);
+
+ int (*abr_process) (void *data, size_t unsent);
+
+ int (*start_encode) (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp);
+ int (*encode) (void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush);
+
+ int (*start_decode) (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp);
+ int (*decode) (void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out);
+
+ int (*reduce_bitpool) (void *data);
+ int (*increase_bitpool) (void *data);
+
+ void (*set_log) (struct spa_log *global_log);
+};
+
+struct media_codec_config {
+ uint32_t config;
+ int value;
+ unsigned int priority;
+};
+
+int media_codec_select_config(const struct media_codec_config configs[], size_t n,
+ uint32_t cap, int preferred_value);
+
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
+ const void *caps, size_t caps_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings);
+
+#endif
diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c
new file mode 100644
index 0000000..29eec1f
--- /dev/null
+++ b/spa/plugins/bluez5/media-sink.c
@@ -0,0 +1,1868 @@
+/* Spa Media Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.media");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ int64_t latency_offset;
+ char clock_name[64];
+};
+
+#define FILL_FRAMES 4
+#define MIN_BUFFERS 2
+#define MAX_BUFFERS 32
+#define BUFFER_SIZE (8192*8)
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ uint32_t frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ size_t ready_offset;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define N_NODE_PARAMS 2
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int is_output:1;
+ unsigned int flush_pending:1;
+
+ unsigned int is_duplex:1;
+
+ struct spa_source source;
+ int timerfd;
+ struct spa_source flush_source;
+ struct spa_source flush_timer_source;
+ int flush_timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t last_error;
+ uint64_t process_time;
+
+ uint64_t prev_flush_time;
+ uint64_t next_flush_time;
+
+ const struct media_codec *codec;
+ bool codec_props_changed;
+ void *codec_props;
+ void *codec_data;
+ struct spa_audio_info codec_format;
+
+ int need_flush;
+ bool fragment;
+ uint32_t block_size;
+ uint8_t buffer[BUFFER_SIZE];
+ uint32_t buffer_used;
+ uint32_t header_size;
+ uint32_t block_count;
+ uint16_t seqnum;
+ uint32_t timestamp;
+ uint64_t sample_count;
+ uint8_t tmp_buffer[BUFFER_SIZE];
+ uint32_t tmp_buffer_used;
+ uint32_t fd_buffer_size;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static void reset_props(struct impl *this, struct props *props)
+{
+ props->latency_offset = 0;
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0, index_offset = 0;
+ bool enum_codec = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX));
+ break;
+ default:
+ enum_codec = true;
+ index_offset = 1;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset));
+ break;
+ default:
+ enum_codec = true;
+ index_offset = 1;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (enum_codec) {
+ int res;
+ if (this->codec->enum_props == NULL || this->codec_props == NULL ||
+ this->transport == NULL)
+ return 0;
+ else if ((res = this->codec->enum_props(this->codec_props,
+ this->transport->device->settings,
+ id, result.index - index_offset, &b, &param)) != 1)
+ return res;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static void emit_port_info(struct impl *this, struct port *port, bool full);
+
+static void set_latency(struct impl *this, bool emit_latency)
+{
+ struct port *port = &this->port;
+ int64_t delay;
+
+ if (this->transport == NULL)
+ return;
+
+ delay = spa_bt_transport_get_delay_nsec(this->transport);
+ delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2);
+ port->latency.min_ns = port->latency.max_ns = delay;
+
+ if (emit_latency) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ }
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(this, &new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset));
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+
+ if (changed)
+ set_latency(this, true);
+
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ int res, codec_res = 0;
+ res = apply_props(this, param);
+ if (this->codec_props && this->codec->set_props) {
+ codec_res = this->codec->set_props(this->codec_props, param);
+ if (codec_res > 0)
+ this->codec_props_changed = true;
+ }
+ if (res > 0 || codec_res > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int reset_buffer(struct impl *this)
+{
+ if (this->codec_props_changed && this->codec_props
+ && this->codec->update_props) {
+ this->codec->update_props(this->codec_data, this->codec_props);
+ this->codec_props_changed = false;
+ }
+ this->need_flush = 0;
+ this->block_count = 0;
+ this->fragment = false;
+ this->buffer_used = this->codec->start_encode(this->codec_data,
+ this->buffer, sizeof(this->buffer),
+ this->seqnum++, this->timestamp);
+ this->header_size = this->buffer_used;
+ this->timestamp = this->sample_count;
+ return 0;
+}
+
+static int get_transport_unused_size(struct impl *this)
+{
+ int res, value;
+ res = ioctl(this->flush_source.fd, TIOCOUTQ, &value);
+ if (res < 0) {
+ spa_log_error(this->log, "%p: ioctl fail: %m", this);
+ return -errno;
+ }
+ spa_log_trace(this->log, "%p: fd unused buffer size:%d/%d", this, value, this->fd_buffer_size);
+ return value;
+}
+
+static int send_buffer(struct impl *this)
+{
+ int written, unsent;
+
+ unsent = get_transport_unused_size(this);
+ if (unsent >= 0) {
+ unsent = this->fd_buffer_size - unsent;
+ this->codec->abr_process(this->codec_data, unsent);
+ }
+
+ written = send(this->flush_source.fd, this->buffer,
+ this->buffer_used, MSG_DONTWAIT | MSG_NOSIGNAL);
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec ts;
+ uint64_t now;
+ uint64_t dt;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ dt = now - this->prev_flush_time;
+ this->prev_flush_time = now;
+
+ spa_log_trace(this->log,
+ "%p: send blocks:%d block:%u seq:%u ts:%u size:%u "
+ "wrote:%d dt:%"PRIu64,
+ this, this->block_count, this->block_size, this->seqnum,
+ this->timestamp, this->buffer_used, written, dt);
+ }
+
+ if (written < 0) {
+ spa_log_debug(this->log, "%p: %m", this);
+ return -errno;
+ }
+
+ return written;
+}
+
+static int encode_buffer(struct impl *this, const void *data, uint32_t size)
+{
+ int processed;
+ size_t out_encoded;
+ struct port *port = &this->port;
+ const void *from_data = data;
+ int from_size = size;
+
+ spa_log_trace(this->log, "%p: encode %d used %d, %d %d %d",
+ this, size, this->buffer_used, port->frame_size, this->block_size,
+ this->block_count);
+
+ if (this->need_flush)
+ return 0;
+
+ if (this->buffer_used >= sizeof(this->buffer))
+ return -ENOSPC;
+
+ if (size < this->block_size - this->tmp_buffer_used) {
+ memcpy(this->tmp_buffer + this->tmp_buffer_used, data, size);
+ this->tmp_buffer_used += size;
+ return size;
+ } else if (this->tmp_buffer_used > 0) {
+ memcpy(this->tmp_buffer + this->tmp_buffer_used, data, this->block_size - this->tmp_buffer_used);
+ from_data = this->tmp_buffer;
+ from_size = this->block_size;
+ this->tmp_buffer_used = this->block_size - this->tmp_buffer_used;
+ }
+
+ processed = this->codec->encode(this->codec_data,
+ from_data, from_size,
+ this->buffer + this->buffer_used,
+ sizeof(this->buffer) - this->buffer_used,
+ &out_encoded, &this->need_flush);
+ if (processed < 0)
+ return processed;
+
+ this->sample_count += processed / port->frame_size;
+ this->block_count += processed / this->block_size;
+ this->buffer_used += out_encoded;
+
+ spa_log_trace(this->log, "%p: processed %d %zd used %d",
+ this, processed, out_encoded, this->buffer_used);
+
+ if (this->tmp_buffer_used) {
+ processed = this->tmp_buffer_used;
+ this->tmp_buffer_used = 0;
+ }
+ return processed;
+}
+
+static int encode_fragment(struct impl *this)
+{
+ int res;
+ size_t out_encoded;
+ struct port *port = &this->port;
+
+ spa_log_trace(this->log, "%p: encode fragment used %d, %d %d %d",
+ this, this->buffer_used, port->frame_size, this->block_size,
+ this->block_count);
+
+ if (this->need_flush)
+ return 0;
+
+ res = this->codec->encode(this->codec_data,
+ NULL, 0,
+ this->buffer + this->buffer_used,
+ sizeof(this->buffer) - this->buffer_used,
+ &out_encoded, &this->need_flush);
+ if (res < 0)
+ return res;
+ if (res != 0)
+ return -EINVAL;
+
+ this->buffer_used += out_encoded;
+
+ spa_log_trace(this->log, "%p: processed fragment %zd used %d",
+ this, out_encoded, this->buffer_used);
+
+ return 0;
+}
+
+static int flush_buffer(struct impl *this)
+{
+ spa_log_trace(this->log, "%p: used:%d block_size:%d", this,
+ this->buffer_used, this->block_size);
+
+ if (this->need_flush)
+ return send_buffer(this);
+
+ return 0;
+}
+
+static int add_data(struct impl *this, const void *data, uint32_t size)
+{
+ int processed, total = 0;
+
+ while (size > 0) {
+ processed = encode_buffer(this, data, size);
+
+ if (processed <= 0)
+ return total > 0 ? total : processed;
+
+ data = SPA_PTROFF(data, processed, void);
+ size -= processed;
+ total += processed;
+ }
+ return total;
+}
+
+static void enable_flush_timer(struct impl *this, bool enabled)
+{
+ struct itimerspec ts;
+
+ if (!enabled)
+ this->next_flush_time = 0;
+
+ ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system,
+ this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+ this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t bytes = 0;
+ struct buffer *b;
+
+ spa_list_for_each(b, &port->ready, link) {
+ struct spa_data *d = b->buf->datas;
+
+ bytes += d[0].chunk->size;
+ }
+
+ if (bytes > port->ready_offset)
+ bytes -= port->ready_offset;
+ else
+ bytes = 0;
+
+ return bytes / port->frame_size;
+}
+
+static int flush_data(struct impl *this, uint64_t now_time)
+{
+ int written;
+ uint32_t total_frames;
+ struct port *port = &this->port;
+ int unused_buffer;
+
+ if (!this->flush_source.loop) {
+ /* I/O in error state */
+ return -EIO;
+ }
+
+ total_frames = 0;
+again:
+ written = 0;
+ if (this->fragment && !this->need_flush) {
+ int res;
+ this->fragment = false;
+ if ((res = encode_fragment(this)) < 0) {
+ /* Error */
+ reset_buffer(this);
+ return res;
+ }
+ }
+ while (!spa_list_is_empty(&port->ready) && !this->need_flush) {
+ uint8_t *src;
+ uint32_t n_bytes, n_frames;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t index, offs, avail, l0, l1;
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ d = b->buf->datas;
+
+ src = d[0].data;
+
+ index = d[0].chunk->offset + port->ready_offset;
+ avail = d[0].chunk->size - port->ready_offset;
+ avail /= port->frame_size;
+
+ offs = index % d[0].maxsize;
+ n_frames = avail;
+ n_bytes = n_frames * port->frame_size;
+
+ l0 = SPA_MIN(n_bytes, d[0].maxsize - offs);
+ l1 = n_bytes - l0;
+
+ written = add_data(this, src + offs, l0);
+ if (written > 0 && l1 > 0)
+ written += add_data(this, src, l1);
+ if (written <= 0) {
+ if (written < 0 && written != -ENOSPC) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ this->port.io->buffer_id = b->id;
+ spa_log_warn(this->log, "%p: error %s, reuse buffer %u",
+ this, spa_strerror(written), b->id);
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ port->ready_offset = 0;
+ }
+ break;
+ }
+
+ n_frames = written / port->frame_size;
+
+ port->ready_offset += written;
+
+ if (port->ready_offset >= d[0].chunk->size) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ spa_log_trace(this->log, "%p: reuse buffer %u", this, b->id);
+ this->port.io->buffer_id = b->id;
+
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ port->ready_offset = 0;
+ }
+ total_frames += n_frames;
+
+ spa_log_trace(this->log, "%p: written %u frames", this, total_frames);
+ }
+
+ if (written > 0 && this->buffer_used == this->header_size) {
+ enable_flush_timer(this, false);
+ return 0;
+ }
+
+ if (this->flush_pending) {
+ spa_log_trace(this->log, "%p: wait for flush timer", this);
+ return 0;
+ }
+
+ /*
+ * Get socket queue size before writing to it.
+ * This should be the same as buffer size to increase bitpool
+ * Bitpool shouldn't be increased when data is left over in the buffer
+ */
+ unused_buffer = get_transport_unused_size(this);
+
+ written = flush_buffer(this);
+
+ if (written == -EAGAIN) {
+ spa_log_trace(this->log, "%p: fail flush", this);
+ if (now_time - this->last_error > SPA_NSEC_PER_SEC / 2) {
+ int res = this->codec->reduce_bitpool(this->codec_data);
+
+ spa_log_debug(this->log, "%p: reduce bitpool: %i", this, res);
+ this->last_error = now_time;
+ }
+
+ /*
+ * The socket buffer is full, and the device is not processing data
+ * fast enough, so should just skip this packet. There will be a sound
+ * glitch in any case.
+ */
+ written = this->buffer_used;
+ }
+
+ if (written < 0) {
+ spa_log_trace(this->log, "%p: error flushing %s", this,
+ spa_strerror(written));
+ reset_buffer(this);
+ enable_flush_timer(this, false);
+ return written;
+ }
+ else if (written > 0) {
+ /*
+ * We cannot write all data we have at once, since this can exceed device
+ * buffers (esp. for the A2DP low-latency codecs) and socket buffers, so
+ * flush needs to be delayed.
+ */
+ uint32_t packet_samples = this->block_count * this->block_size
+ / port->frame_size;
+ uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate;
+
+ if (SPA_LIKELY(this->position)) {
+ uint32_t frames = get_queued_frames(this);
+ uint64_t duration_ns;
+
+ /*
+ * Flush at the time position of the next buffered sample.
+ */
+ duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+ / this->position->clock.rate.denom);
+ this->next_flush_time = this->process_time + duration_ns
+ - ((uint64_t)frames * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate);
+
+ /*
+ * We could delay the output by one packet to avoid waiting
+ * for the next buffer and so make send intervals exactly regular.
+ * However, this is not needed for A2DP or BAP. The controller
+ * will do the scheduling for us, and there's also the socket buffer
+ * in between.
+ */
+#if 0
+ this->next_flush_time += SPA_MIN(packet_time,
+ duration_ns * (port->n_buffers - 1));
+#endif
+ } else {
+ if (this->next_flush_time == 0)
+ this->next_flush_time = this->process_time;
+ this->next_flush_time += packet_time;
+ }
+
+ if (this->need_flush == NEED_FLUSH_FRAGMENT) {
+ reset_buffer(this);
+ this->fragment = true;
+ goto again;
+ }
+
+ if (now_time - this->last_error > SPA_NSEC_PER_SEC) {
+ if (unused_buffer == (int)this->fd_buffer_size) {
+ int res = this->codec->increase_bitpool(this->codec_data);
+
+ spa_log_debug(this->log, "%p: increase bitpool: %i", this, res);
+ }
+ this->last_error = now_time;
+ }
+
+ spa_log_trace(this->log, "%p: flush at:%"PRIu64" process:%"PRIu64, this,
+ this->next_flush_time, this->process_time);
+ reset_buffer(this);
+ enable_flush_timer(this, true);
+ }
+ else {
+ /* Don't want to flush yet, or failed to write anything */
+ spa_log_trace(this->log, "%p: skip flush", this);
+ enable_flush_timer(this, false);
+ }
+ return 0;
+}
+
+static void media_on_flush_error(struct spa_source *source)
+{
+ struct impl *this = source->data;
+
+ spa_log_trace(this->log, "%p: flush event", this);
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "%p: error %d", this, source->rmask);
+ if (this->flush_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_source);
+ return;
+ }
+}
+
+static void media_on_flush_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ spa_log_trace(this->log, "%p: flush on timeout", this);
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ if (this->transport == NULL) {
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ while (exp-- > 0) {
+ this->flush_pending = false;
+ flush_data(this, this->current_time);
+ }
+}
+
+static void media_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ struct spa_io_buffers *io = port->io;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s",
+ spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ int64_t delay_nsec;
+
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+
+ delay_nsec = spa_bt_transport_get_delay_nsec(this->transport);
+
+ /* Negative delay doesn't work properly, so disallow it */
+ delay_nsec += SPA_CLAMP(this->props.latency_offset, -delay_nsec, INT64_MAX / 2);
+
+ this->clock->delay = (delay_nsec * this->clock->rate.denom) / SPA_NSEC_PER_SEC;
+ }
+
+
+ spa_log_trace(this->log, "%p: %d", this, io->status);
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this)
+{
+ int res, val, size;
+ struct port *port;
+ socklen_t len;
+ uint8_t *conf;
+ uint32_t flags;
+
+ if (this->started)
+ return 0;
+
+ spa_return_val_if_fail(this->transport, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d", this, this->following);
+
+ if ((res = spa_bt_transport_acquire(this->transport, false)) < 0)
+ return res;
+
+ port = &this->port;
+
+ conf = this->transport->configuration;
+ size = this->transport->configuration_len;
+
+ spa_log_debug(this->log, "Transport configuration:");
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 2, conf, (size_t)size);
+
+ flags = this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0;
+
+ this->codec_data = this->codec->init(this->codec,
+ flags,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ &port->current_format,
+ this->codec_props,
+ this->transport->write_mtu);
+ if (this->codec_data == NULL)
+ return -EIO;
+
+ spa_log_info(this->log, "%p: using %s codec %s, delay:%"PRIi64" ms", this,
+ this->codec->bap ? "BAP" : "A2DP", this->codec->description,
+ (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC));
+
+ this->seqnum = 0;
+
+ this->block_size = this->codec->get_block_size(this->codec_data);
+ if (this->block_size > sizeof(this->tmp_buffer)) {
+ spa_log_error(this->log, "block-size %d > %zu",
+ this->block_size, sizeof(this->tmp_buffer));
+ return -EIO;
+ }
+
+ spa_log_debug(this->log, "%p: block_size %d", this, this->block_size);
+
+ val = this->codec->send_buf_size > 0
+ /* The kernel doubles the SO_SNDBUF option value set by setsockopt(). */
+ ? this->codec->send_buf_size / 2 + this->codec->send_buf_size % 2
+ : FILL_FRAMES * this->transport->write_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+
+ len = sizeof(val);
+ if (getsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, &len) < 0) {
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+ }
+ else {
+ spa_log_debug(this->log, "%p: SO_SNDBUF: %d", this, val);
+ }
+ this->fd_buffer_size = val;
+
+ val = FILL_FRAMES * this->transport->read_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_RCVBUF %m", this);
+
+ val = 6;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "SO_PRIORITY failed: %m");
+
+ reset_buffer(this);
+
+ this->source.data = this;
+ this->source.fd = this->timerfd;
+ this->source.func = media_on_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->flush_timer_source.data = this;
+ this->flush_timer_source.fd = this->flush_timerfd;
+ this->flush_timer_source.func = media_on_flush_timeout;
+ this->flush_timer_source.mask = SPA_IO_IN;
+ this->flush_timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_timer_source);
+
+ this->flush_source.data = this;
+ this->flush_source.fd = this->transport->fd;
+ this->flush_source.func = media_on_flush_error;
+ this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP;
+ this->flush_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_source);
+
+ this->flush_pending = false;
+
+ set_timers(this);
+ this->started = true;
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ if (this->flush_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_source);
+
+ if (this->flush_timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_trace(this->log, "%p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->transport)
+ res = spa_bt_transport_release(this->transport);
+
+ if (this->codec_data)
+ this->codec->deinit(this->codec_data);
+ this->codec_data = NULL;
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, this->is_output ? "Audio/Sink" : "Stream/Input/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : this->codec->bap ? "BAP" : "A2DP" ) },
+ { SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" },
+ };
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (this->codec == NULL)
+ return -EIO;
+ if (this->transport == NULL)
+ return -EIO;
+
+ if ((res = this->codec->enum_config(this->codec,
+ this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ id, result.index, &b, &param)) != 1)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(
+ MIN_BUFFERS,
+ MIN_BUFFERS,
+ MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels;
+ switch (info.info.raw.format) {
+ case SPA_AUDIO_FORMAT_S16:
+ port->frame_size *= 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ port->frame_size *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_F32:
+ port->frame_size *= 4;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ b->buf = buffers[i];
+ b->id = i;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (buffers[i]->datas[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+
+ if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id);
+
+ spa_list_append(&port->ready, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_OK;
+ }
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now;
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ this->process_time = this->current_time;
+
+ if (!spa_list_is_empty(&port->ready)) {
+ spa_log_trace(this->log, "%p: flush on process", this);
+ flush_data(this, this->current_time);
+ }
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static void transport_delay_changed(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p delay changed", this->transport);
+ set_latency(this, true);
+}
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static void transport_state_changed(void *data,
+ enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state)
+{
+ struct impl *this = data;
+
+ spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state);
+
+ if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && old == SPA_BT_TRANSPORT_STATE_ACTIVE &&
+ this->started) {
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+
+ spa_log_debug(this->log, "%p: transport %p becomes inactive: stop and indicate error",
+ this, this->transport);
+
+ /*
+ * If establishing connection fails due to remote end not activating
+ * the transport, we won't get a write error, but instead see a transport
+ * state change.
+ *
+ * Stop and emit a node error, to let upper levels handle it.
+ */
+
+ do_stop(this);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_node_emit_event(&this->hooks,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_Error));
+ }
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .delay_changed = transport_delay_changed,
+ .state_changed = transport_state_changed,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->codec_props && this->codec->clear_props)
+ this->codec->clear_props(this->codec_props);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_system_close(this->data_system, this->flush_timerfd);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS |
+ SPA_NODE_CHANGE_MASK_PROPS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ spa_list_init(&port->ready);
+
+ this->quantum_limit = 8192;
+
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
+ this->is_duplex = spa_atob(str);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ if (this->transport->media_codec == NULL) {
+ spa_log_error(this->log, "a transport codec is needed");
+ return -EINVAL;
+ }
+
+ this->codec = this->transport->media_codec;
+
+ if (this->is_duplex) {
+ if (!this->codec->duplex_codec) {
+ spa_log_error(this->log, "transport codec doesn't support duplex");
+ return -EINVAL;
+ }
+ this->codec = this->codec->duplex_codec;
+ }
+
+ if (this->codec->init_props != NULL)
+ this->codec_props = this->codec->init_props(this->codec,
+ this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
+ this->transport->device->settings);
+
+ if (this->codec->bap)
+ this->is_output = this->transport->bap_initiator;
+ else
+ this->is_output = true;
+
+ reset_props(this, &this->props);
+
+ set_latency(this, false);
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ this->flush_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the media" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_media_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MEDIA_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility: */
+const struct spa_handle_factory spa_a2dp_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_A2DP_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c
new file mode 100644
index 0000000..360f812
--- /dev/null
+++ b/spa/plugins/bluez5/media-source.c
@@ -0,0 +1,1707 @@
+/* Spa Media Source
+ *
+ * Copyright © 2018 Wim Taymans
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include "defs.h"
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.media");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#include "decode-buffer.h"
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define FILL_FRAMES 2
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ uint32_t frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct spa_bt_decode_buffer buffer;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int transport_acquired:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resampling:1;
+
+ unsigned int is_input:1;
+ unsigned int is_duplex:1;
+ unsigned int use_duplex_source:1;
+
+ int fd;
+ struct spa_source source;
+
+ struct spa_source timer_source;
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ const struct media_codec *codec;
+ bool codec_props_changed;
+ void *codec_props;
+ void *codec_data;
+ struct spa_audio_info codec_format;
+
+ uint8_t buffer_read[4096];
+ struct timespec now;
+ uint64_t sample_count;
+
+ int duplex_timerfd;
+ uint64_t duplex_timeout;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0, index_offset = 0;
+ bool enum_codec = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ enum_codec = true;
+ index_offset = 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ default:
+ enum_codec = true;
+ index_offset = 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (enum_codec) {
+ int res;
+ if (this->codec->enum_props == NULL || this->codec_props == NULL ||
+ this->transport == NULL)
+ return 0;
+ else if ((res = this->codec->enum_props(this->codec_props,
+ this->transport->device->settings,
+ id, result.index - index_offset,
+ &b, &param)) != 1)
+ return res;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->port;
+
+ set_timers(this);
+ spa_bt_decode_buffer_recover(&port->buffer);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ /* noop */
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ int res, codec_res = 0;
+ res = apply_props(this, param);
+ if (this->codec_props && this->codec->set_props) {
+ codec_res = this->codec->set_props(this->codec_props, param);
+ if (codec_res > 0)
+ this->codec_props_changed = true;
+ }
+ if (res > 0 || codec_res > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outstanding) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static int32_t read_data(struct impl *this) {
+ const ssize_t b_size = sizeof(this->buffer_read);
+ int32_t size_read = 0;
+
+again:
+ /* read data from socket */
+ size_read = recv(this->fd, this->buffer_read, b_size, MSG_DONTWAIT);
+
+ if (size_read == 0)
+ return 0;
+ else if (size_read < 0) {
+ /* retry if interrupted */
+ if (errno == EINTR)
+ goto again;
+
+ /* return socket has no data */
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return 0;
+
+ /* go to 'stop' if socket has an error */
+ spa_log_error(this->log, "read error: %s", strerror(errno));
+ return -errno;
+ }
+
+ return size_read;
+}
+
+static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size,
+ uint8_t *dst, uint32_t dst_size)
+{
+ ssize_t processed;
+ size_t written, avail;
+
+ if ((processed = this->codec->start_decode(this->codec_data,
+ src, src_size, NULL, NULL)) < 0)
+ return processed;
+
+ src += processed;
+ src_size -= processed;
+
+ /* decode */
+ avail = dst_size;
+ while (src_size > 0) {
+ if ((processed = this->codec->decode(this->codec_data,
+ src, src_size, dst, avail, &written)) <= 0)
+ return processed;
+
+ /* update source and dest pointers */
+ spa_return_val_if_fail (avail > written, -ENOSPC);
+ src_size -= processed;
+ src += processed;
+ avail -= written;
+ dst += written;
+ }
+ return dst_size - avail;
+}
+
+static void media_on_ready_read(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ struct timespec now;
+ void *buf;
+ int32_t size_read, decoded;
+ uint32_t avail;
+ uint64_t dt;
+
+ /* make sure the source is an input */
+ if ((source->rmask & SPA_IO_IN) == 0) {
+ spa_log_error(this->log, "source is not an input, rmask=%d", source->rmask);
+ goto stop;
+ }
+ if (this->transport == NULL) {
+ spa_log_debug(this->log, "no transport, stop reading");
+ goto stop;
+ }
+
+ spa_log_trace(this->log, "socket poll");
+
+ /* read */
+ size_read = read_data (this);
+ if (size_read == 0)
+ return;
+ if (size_read < 0) {
+ spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read));
+ goto stop;
+ }
+
+ /* update the current pts */
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+
+ if (this->codec_props_changed && this->codec_props
+ && this->codec->update_props) {
+ this->codec->update_props(this->codec_data, this->codec_props);
+ this->codec_props_changed = false;
+ }
+
+ /* decode to buffer */
+ buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+ spa_log_trace(this->log, "read socket data size:%d, avail:%d", size_read, avail);
+ decoded = decode_data(this, this->buffer_read, size_read, buf, avail);
+ if (decoded < 0) {
+ spa_log_debug(this->log, "failed to decode data: %d", decoded);
+ return;
+ }
+ if (decoded == 0) {
+ spa_log_trace(this->log, "no decoded socket data");
+ return;
+ }
+
+ /* discard when not started */
+ if (!this->started)
+ return;
+
+ spa_bt_decode_buffer_write_packet(&port->buffer, decoded);
+
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now);
+ this->now = now;
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
+
+ spa_log_trace(this->log, "decoded socket data size:%d frames:%d dt:%d dms",
+ (int)decoded, (int)decoded/port->frame_size,
+ (int)(dt / 100000));
+
+ return;
+
+stop:
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+}
+
+static int set_duplex_timeout(struct impl *this, uint64_t timeout)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->duplex_timerfd, 0, &ts, NULL);
+}
+
+static void media_on_duplex_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->duplex_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ set_duplex_timeout(this, this->duplex_timeout);
+
+ media_on_ready_read(source);
+}
+
+static int setup_matching(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ if (this->position && port->rate_match) {
+ port->rate_match->rate = 1 / port->buffer.corr;
+
+ this->matching = this->following;
+ this->resampling = this->matching ||
+ (port->current_format.info.raw.rate != this->position->clock.rate.denom);
+ } else {
+ this->matching = false;
+ this->resampling = false;
+ }
+
+ if (port->rate_match)
+ SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
+
+ return 0;
+}
+
+static int produce_buffer(struct impl *this);
+
+static void media_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ setup_matching(this);
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = port->buffer.corr;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ if (port->io) {
+ int status = produce_buffer(this);
+ spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+ }
+
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int transport_start(struct impl *this)
+{
+ int res, val;
+ struct port *port = &this->port;
+ uint32_t flags;
+
+ if (this->transport_acquired)
+ return 0;
+
+ spa_log_debug(this->log, "%p: transport %p acquire", this,
+ this->transport);
+ if ((res = spa_bt_transport_acquire(this->transport, false)) < 0)
+ return res;
+
+ this->transport_acquired = true;
+
+ flags = this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK;
+
+ this->codec_data = this->codec->init(this->codec,
+ flags,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ &port->current_format,
+ this->codec_props,
+ this->transport->read_mtu);
+ if (this->codec_data == NULL)
+ return -EIO;
+
+ spa_log_info(this->log, "%p: using %s codec %s", this,
+ this->codec->bap ? "BAP" : "A2DP", this->codec->description);
+
+ val = fcntl(this->transport->fd, F_GETFL);
+ if (fcntl(this->transport->fd, F_SETFL, val | O_NONBLOCK) < 0)
+ spa_log_warn(this->log, "%p: fcntl %u %m", this, val | O_NONBLOCK);
+
+ val = FILL_FRAMES * this->transport->write_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+
+ val = FILL_FRAMES * this->transport->read_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_RCVBUF %m", this);
+
+ val = 6;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "SO_PRIORITY failed: %m");
+
+ reset_buffers(port);
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+ if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+ port->frame_size, port->current_format.info.raw.rate,
+ this->quantum_limit, this->quantum_limit)) < 0)
+ return res;
+
+ this->fd = this->transport->fd;
+
+ this->source.data = this;
+
+ if (!this->use_duplex_source) {
+ this->source.fd = this->transport->fd;
+ this->source.func = media_on_ready_read;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+ } else {
+ /*
+ * XXX: For an unknown reason (on Linux 5.13.10), the socket when working with
+ * XXX: "duplex" stream sometimes stops waking up from the poll, even though
+ * XXX: you can recv() from the socket with no problem.
+ * XXX:
+ * XXX: The reason for this should be found and fixed.
+ * XXX: To work around this, for now we just do the stupid thing and poll
+ * XXX: on a timer, chosen so that it's fast enough for the aptX-LL codec
+ * XXX: we currently support (which sends mSBC data), and also for Opus
+ * XXX: forward stream.
+ */
+ this->source.fd = this->duplex_timerfd;
+ this->source.func = media_on_duplex_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->duplex_timeout = SPA_NSEC_PER_MSEC * 25/10;
+ set_duplex_timeout(this, this->duplex_timeout);
+ }
+
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = media_on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ this->sample_count = 0;
+
+ setup_matching(this);
+
+ set_timers(this);
+
+ return 0;
+}
+
+static int do_start(struct impl *this)
+{
+ int res = 0;
+
+ if (this->started)
+ return 0;
+
+ spa_return_val_if_fail(this->transport != NULL, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start state:%d following:%d",
+ this, this->transport->state, this->following);
+
+ if (this->transport->state >= SPA_BT_TRANSPORT_STATE_PENDING ||
+ this->is_duplex || this->codec->bap)
+ res = transport_start(this);
+
+ this->started = true;
+
+ return res;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ spa_log_debug(this->log, "%p: remove source", this);
+
+ set_duplex_timeout(this, 0);
+
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int transport_stop(struct impl *this)
+{
+ struct port *port = &this->port;
+ int res;
+
+ spa_log_debug(this->log, "%p: transport stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ if (this->transport && this->transport_acquired)
+ res = spa_bt_transport_release(this->transport);
+ else
+ res = 0;
+
+ this->transport_acquired = false;
+
+ if (this->codec_data)
+ this->codec->deinit(this->codec_data);
+ this->codec_data = NULL;
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+
+ return res;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_debug(this->log, "%p: stop", this);
+
+ res = transport_stop(this);
+
+ this->started = false;
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" },
+ { SPA_KEY_NODE_LATENCY, this->is_input ? "" : "512/48000" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : this->codec->bap ? "BAP" : "A2DP") },
+ { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
+ };
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (result.index > 0)
+ return 0;
+ if (this->codec == NULL)
+ return -EIO;
+ if (this->transport == NULL)
+ return -EIO;
+
+ if ((res = this->codec->enum_config(this->codec,
+ this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ id, result.index, &b, &param)) != 1)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels;
+
+ switch (info.info.raw.format) {
+ case SPA_AUDIO_FORMAT_S16:
+ port->frame_size *= 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ port->frame_size *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_F32:
+ port->frame_size *= 4;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+ port = &this->port;
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+ struct port *port = &this->port;
+ uint32_t samples;
+
+ if (SPA_LIKELY(port->rate_match) && this->resampling) {
+ samples = port->rate_match->size;
+ } else {
+ if (SPA_LIKELY(this->position))
+ samples = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else
+ samples = 1024;
+ }
+
+ if (SPA_LIKELY(this->position))
+ *duration = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else if (SPA_LIKELY(this->clock))
+ *duration = this->clock->duration * port->current_format.info.raw.rate
+ / this->clock->rate.denom;
+ else
+ *duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+ return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t duration;
+ const uint32_t samples = get_samples(this, &duration);
+ uint32_t avail;
+ void *buf;
+
+ spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+ setup_matching(this);
+
+ buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+ /* copy data to buffers */
+ if (!spa_list_is_empty(&port->free) && avail > 0) {
+ struct buffer *buffer;
+ struct spa_data *datas;
+ uint32_t data_size;
+
+ data_size = samples * port->frame_size;
+
+ avail = SPA_MIN(avail, data_size);
+
+ spa_bt_decode_buffer_read(&port->buffer, avail);
+
+ buffer = spa_list_first(&port->free, struct buffer, link);
+ spa_list_remove(&buffer->link);
+
+ spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+ if (buffer->h) {
+ buffer->h->seq = this->sample_count;
+ buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now);
+ buffer->h->dts_offset = 0;
+ }
+
+ datas = buffer->buf->datas;
+
+ spa_assert(datas[0].maxsize >= data_size);
+
+ datas[0].chunk->offset = 0;
+ datas[0].chunk->size = avail;
+ datas[0].chunk->stride = port->frame_size;
+
+ memcpy(datas[0].data, buf, avail);
+
+ this->sample_count += avail / port->frame_size;
+
+ /* ready buffer if full */
+ spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+ spa_list_append(&port->ready, &buffer->link);
+ }
+}
+
+static int produce_buffer(struct impl *this)
+{
+ struct buffer *buffer;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+
+ if (io == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Handle buffering */
+ process_buffering(this);
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outstanding = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ spa_log_trace(this->log, "%p status:%d", this, io->status);
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Follower produces buffers here, driver in timeout */
+ if (this->following)
+ return produce_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ this->transport_acquired = false;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ struct port *port = &this->port;
+
+ do_stop(this);
+ if (this->codec_props && this->codec->clear_props)
+ this->codec->clear_props(this->codec_props);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ if (this->duplex_timerfd >= 0) {
+ spa_system_close(this->data_system, this->duplex_timerfd);
+ this->duplex_timerfd = -1;
+ }
+ spa_bt_decode_buffer_clear(&port->buffer);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ /* set the node info */
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 0;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+
+ this->quantum_limit = 8192;
+
+ if (info != NULL) {
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL)
+ sscanf(str, "pointer:%p", &this->transport);
+ if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL)
+ this->is_input = spa_streq(str, "input");
+ if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
+ this->is_duplex = spa_atob(str);
+ }
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ if (this->transport->media_codec == NULL) {
+ spa_log_error(this->log, "a transport codec is needed");
+ return -EINVAL;
+ }
+ this->codec = this->transport->media_codec;
+
+ if (this->is_duplex) {
+ if (!this->codec->duplex_codec) {
+ spa_log_error(this->log, "transport codec doesn't support duplex");
+ return -EINVAL;
+ }
+ this->codec = this->codec->duplex_codec;
+ this->is_input = true;
+ }
+ this->use_duplex_source = this->is_duplex || (this->codec->duplex_codec != NULL);
+
+ if (this->codec->bap)
+ this->is_input = this->transport->bap_initiator;
+
+ if (this->codec->init_props != NULL)
+ this->codec_props = this->codec->init_props(this->codec,
+ this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
+ this->transport->device->settings);
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ if (this->use_duplex_source) {
+ this->duplex_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ } else {
+ this->duplex_timerfd = -1;
+ }
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with media" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_media_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MEDIA_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility */
+const struct spa_handle_factory spa_a2dp_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_A2DP_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build
new file mode 100644
index 0000000..f189570
--- /dev/null
+++ b/spa/plugins/bluez5/meson.build
@@ -0,0 +1,206 @@
+gnome = import('gnome')
+
+bluez5_deps = [ mathlib, dbus_dep, glib2_dep, sbc_dep, bluez_dep, gio_dep, gio_unix_dep ]
+foreach dep: bluez5_deps
+ if not dep.found()
+ subdir_done()
+ endif
+endforeach
+
+cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE',
+ get_option('bluez5-backend-hsp-native').allowed() or
+ get_option('bluez5-backend-hfp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HSP_NATIVE', get_option('bluez5-backend-hsp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HFP_NATIVE', get_option('bluez5-backend-hfp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE_MM', get_option('bluez5-backend-native-mm').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_OFONO', get_option('bluez5-backend-ofono').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HSPHFPD', get_option('bluez5-backend-hsphfpd').allowed())
+cdata.set('HAVE_BLUEZ_5_HCI', dependency('bluez', version: '< 6', required: false).found())
+
+bluez5_sources = [
+ 'plugin.c',
+ 'codec-loader.c',
+ 'media-codecs.c',
+ 'media-sink.c',
+ 'media-source.c',
+ 'sco-sink.c',
+ 'sco-source.c',
+ 'sco-io.c',
+ 'quirks.c',
+ 'player.c',
+ 'bluez5-device.c',
+ 'bluez5-dbus.c',
+ 'hci.c',
+ 'dbus-monitor.c',
+ 'midi-enum.c',
+ 'midi-parser.c',
+ 'midi-node.c',
+ 'midi-server.c',
+]
+
+bluez5_interface_src = gnome.gdbus_codegen('bluez5-interface-gen',
+ sources: 'org.bluez.xml',
+ interface_prefix : 'org.bluez.',
+ object_manager: true,
+ namespace : 'Bluez5',
+ annotations : [
+ ['org.bluez.GattCharacteristic1.AcquireNotify()', 'org.gtk.GDBus.C.UnixFD', 'true'],
+ ['org.bluez.GattCharacteristic1.AcquireWrite()', 'org.gtk.GDBus.C.UnixFD', 'true'],
+ ]
+)
+bluez5_sources += [ bluez5_interface_src ]
+
+bluez5_data = ['bluez-hardware.conf']
+
+install_data(bluez5_data, install_dir : spa_datadir / 'bluez5')
+
+if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed()
+ if libusb_dep.found()
+ bluez5_deps += libusb_dep
+ endif
+ if mm_dep.found()
+ bluez5_deps += mm_dep
+ bluez5_sources += ['modemmanager.c']
+ endif
+ bluez5_sources += ['backend-native.c', 'upower.c']
+endif
+
+if get_option('bluez5-backend-ofono').allowed()
+ bluez5_sources += ['backend-ofono.c']
+endif
+
+if get_option('bluez5-backend-hsphfpd').allowed()
+ bluez5_sources += ['backend-hsphfpd.c']
+endif
+
+# The library uses GObject, and cannot be unloaded
+bluez5_link_args = [ '-Wl,-z', '-Wl,nodelete' ]
+
+bluez5lib = shared_library('spa-bluez5',
+ bluez5_sources,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, bluez5_deps ],
+ link_args : bluez5_link_args,
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+codec_args = [ '-DCODEC_PLUGIN' ]
+
+bluez_codec_sbc = shared_library('spa-codec-bluez5-sbc',
+ [ 'a2dp-codec-sbc.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream',
+ [ 'a2dp-codec-faststream.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+if fdk_aac_dep.found()
+ bluez_codec_aac = shared_library('spa-codec-bluez5-aac',
+ [ 'a2dp-codec-aac.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, fdk_aac_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if aptx_dep.found()
+ bluez_codec_aptx = shared_library('spa-codec-bluez5-aptx',
+ [ 'a2dp-codec-aptx.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, aptx_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if ldac_dep.found()
+ ldac_args = codec_args
+ ldac_dep = [ ldac_dep ]
+ if ldac_abr_dep.found()
+ ldac_args += [ '-DENABLE_LDAC_ABR' ]
+ ldac_dep += ldac_abr_dep
+ endif
+ bluez_codec_ldac = shared_library('spa-codec-bluez5-ldac',
+ [ 'a2dp-codec-ldac.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : ldac_args,
+ dependencies : [ spa_dep, ldac_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
+ bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
+ [ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, lc3plus_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-opus').allowed() and opus_dep.found()
+ opus_args = codec_args
+ opus_dep = [ opus_dep ]
+ bluez_codec_opus = shared_library('spa-codec-bluez5-opus',
+ [ 'a2dp-codec-opus.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : opus_args,
+ dependencies : [ spa_dep, opus_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()
+ bluez_codec_lc3 = shared_library('spa-codec-bluez5-lc3',
+ [ 'bap-codec-lc3.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, lc3_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+test_apps = [
+ 'test-midi',
+]
+bluez5_test_lib = static_library('bluez5_test_lib',
+ [ 'midi-parser.c' ],
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, bluez5_deps ],
+ install : false
+)
+
+foreach a : test_apps
+ test(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, bluez5_deps ],
+ include_directories : [ configinc ],
+ link_with : [ bluez5_test_lib ],
+ install_rpath : spa_plugindir / 'bluez5',
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'bluez5'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'bluez5' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'bluez5',
+ configuration: test_conf
+ )
+ endif
+endforeach
diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c
new file mode 100644
index 0000000..a966591
--- /dev/null
+++ b/spa/plugins/bluez5/midi-enum.c
@@ -0,0 +1,887 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/node/node.h>
+#include <spa/node/keys.h>
+
+#include "midi.h"
+#include "config.h"
+
+#include "bluez5-interface-gen.h"
+#include "dbus-monitor.h"
+
+#define MIDI_OBJECT_PATH "/midi"
+#define MIDI_PROFILE_PATH MIDI_OBJECT_PATH "/profile"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl
+{
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ GDBusConnection *conn;
+ struct dbus_monitor monitor;
+ GDBusObjectManagerServer *manager;
+
+ struct spa_hook_list hooks;
+
+ uint32_t id;
+};
+
+struct _MidiEnumCharacteristicProxy
+{
+ Bluez5GattCharacteristic1Proxy parent_instance;
+
+ struct impl *impl;
+
+ gchar *description;
+ uint32_t id;
+ GCancellable *read_call;
+ GCancellable *dsc_call;
+ unsigned int node_emitted:1;
+ unsigned int read_probed:1;
+ unsigned int read_done:1;
+ unsigned int dsc_probed:1;
+ unsigned int dsc_done:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, MIDI_ENUM,
+ CHARACTERISTIC_PROXY, Bluez5GattCharacteristic1Proxy)
+G_DEFINE_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, BLUEZ5_TYPE_GATT_CHARACTERISTIC1_PROXY)
+#define MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY (midi_enum_characteristic_proxy_get_type())
+
+struct _MidiEnumManagerProxy
+{
+ Bluez5GattManager1Proxy parent_instance;
+
+ GCancellable *register_call;
+ unsigned int registered:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, MIDI_ENUM,
+ MANAGER_PROXY, Bluez5GattManager1Proxy)
+G_DEFINE_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
+#define MIDI_ENUM_TYPE_MANAGER_PROXY (midi_enum_manager_proxy_get_type())
+
+
+static void emit_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, Bluez5Device1 *device)
+{
+ struct spa_device_object_info info;
+ char nick[512], class[16];
+ struct spa_dict_item items[23];
+ uint32_t n_items = 0;
+ const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
+ const char *alias = bluez5_device1_get_alias(device);
+
+ spa_log_debug(impl->log, "emit node for path=%s", path);
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+ info.factory_name = SPA_NAME_API_BLUEZ5_MIDI_NODE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Midi/Bridge");
+ items[n_items++] = SPA_DICT_ITEM_INIT("node.description",
+ alias ? alias : bluez5_device1_get_name(device));
+ if (chr->description && chr->description[0] != '\0') {
+ spa_scnprintf(nick, sizeof(nick), "%s (%s)", alias, chr->description);
+ items[n_items++] = SPA_DICT_ITEM_INIT("node.nick", nick);
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, bluez5_device1_get_icon(device));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, bluez5_device1_get_address(device));
+ snprintf(class, sizeof(class), "0x%06x", bluez5_device1_get_class(device));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ROLE, "client");
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&impl->hooks, chr->id, &info);
+}
+
+static void remove_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ spa_log_debug(impl->log, "remove node for path=%s", g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ spa_device_emit_object_info(&impl->hooks, chr->id, NULL);
+}
+
+static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr);
+
+static void read_probe_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(source_object);
+ struct impl *impl = user_data;
+ gchar *value = NULL;
+ GError *err = NULL;
+
+ bluez5_gatt_characteristic1_call_read_value_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &value, res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.ReadValue() failed: %s",
+ BLUEZ_GATT_CHR_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ g_free(value);
+
+ spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ chr->read_done = true;
+
+ check_chr_node(impl, chr);
+
+done:
+ g_clear_object(&chr->read_call);
+}
+
+static int read_probe(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ /*
+ * BLE MIDI-1.0 §5: The Central shall read the MIDI I/O characteristic
+ * of the Peripheral after establishing a connection with the accessory.
+ */
+
+ if (chr->read_probed)
+ return 0;
+ if (chr->read_call)
+ return -EBUSY;
+
+ chr->read_probed = true;
+
+ spa_log_debug(impl->log, "MIDI GATT read probe for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ chr->read_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_characteristic1_call_read_value(BLUEZ5_GATT_CHARACTERISTIC1(chr),
+ options,
+ chr->read_call,
+ read_probe_reply,
+ impl);
+
+ return 0;
+}
+
+Bluez5GattDescriptor1 *find_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
+ Bluez5GattDescriptor1 *found = NULL;;
+ GList *objects;
+
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
+
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ Bluez5GattDescriptor1 *dsc;
+
+ if (!BLUEZ5_IS_GATT_DESCRIPTOR1(lli->data))
+ continue;
+
+ dsc = BLUEZ5_GATT_DESCRIPTOR1(lli->data);
+
+ if (!spa_streq(bluez5_gatt_descriptor1_get_uuid(dsc),
+ BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID))
+ continue;
+
+ if (spa_streq(bluez5_gatt_descriptor1_get_characteristic(dsc), path)) {
+ found = dsc;
+ break;
+ }
+ }
+ g_list_free_full(interfaces, g_object_unref);
+
+ if (found)
+ break;
+ }
+ g_list_free_full(objects, g_object_unref);
+
+ return found;
+}
+
+static void read_dsc_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(user_data);
+ struct impl *impl = chr->impl;
+ gchar *value = NULL;
+ GError *err = NULL;
+
+ chr->dsc_done = true;
+
+ bluez5_gatt_descriptor1_call_read_value_finish(
+ BLUEZ5_GATT_DESCRIPTOR1(source_object), &value, res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.ReadValue() failed: %s",
+ BLUEZ_GATT_DSC_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ g_free(chr->description);
+ chr->description = value;
+
+ spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'",
+ chr->description);
+
+ check_chr_node(impl, chr);
+
+done:
+ g_clear_object(&chr->dsc_call);
+}
+
+static int read_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ Bluez5GattDescriptor1 *dsc;
+ GVariant *options;
+ GVariantBuilder builder;
+
+ if (chr->dsc_probed)
+ return 0;
+ if (chr->dsc_call)
+ return -EBUSY;
+
+ chr->dsc_probed = true;
+
+ dsc = find_dsc(impl, chr);
+ if (dsc == NULL) {
+ chr->dsc_done = true;
+ return -ENOENT;
+ }
+
+ spa_log_debug(impl->log, "MIDI GATT user descriptor read, path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(dsc)));
+
+ chr->dsc_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_descriptor1_call_read_value(BLUEZ5_GATT_DESCRIPTOR1(dsc),
+ options,
+ chr->dsc_call,
+ read_dsc_reply,
+ chr);
+
+ return 0;
+}
+
+static int read_probe_reset(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ g_cancellable_cancel(chr->read_call);
+ g_clear_object(&chr->read_call);
+
+ g_cancellable_cancel(chr->dsc_call);
+ g_clear_object(&chr->dsc_call);
+
+ chr->read_probed = false;
+ chr->read_done = false;
+ chr->dsc_probed = false;
+ chr->dsc_done = false;
+ return 0;
+}
+
+static void lookup_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr,
+ Bluez5GattService1 **service, Bluez5Device1 **device)
+{
+ GDBusObject *object;
+ const char *service_path;
+ const char *device_path;
+
+ *service = NULL;
+ *device = NULL;
+
+ service_path = bluez5_gatt_characteristic1_get_service(BLUEZ5_GATT_CHARACTERISTIC1(chr));
+ if (!service_path)
+ return;
+
+ object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), service_path);
+ if (object) {
+ GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_GATT_SERVICE_INTERFACE);
+ *service = BLUEZ5_GATT_SERVICE1(iface);
+ }
+
+ if (!*service)
+ return;
+
+ device_path = bluez5_gatt_service1_get_device(*service);
+ if (!device_path)
+ return;
+
+ object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), device_path);
+ if (object) {
+ GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_DEVICE_INTERFACE);
+ *device = BLUEZ5_DEVICE1(iface);
+ }
+}
+
+static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ Bluez5GattService1 *service;
+ Bluez5Device1 *device;
+ bool available;
+
+ lookup_chr_node(impl, chr, &service, &device);
+
+ if (!device || !bluez5_device1_get_connected(device)) {
+ /* Retry read probe on each connection */
+ read_probe_reset(impl, chr);
+ }
+
+ spa_log_debug(impl->log,
+ "At %s, connected:%d resolved:%d",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)),
+ bluez5_device1_get_connected(device),
+ bluez5_device1_get_services_resolved(device));
+
+ available = service && device &&
+ bluez5_device1_get_connected(device) &&
+ bluez5_device1_get_services_resolved(device) &&
+ spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID) &&
+ spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
+ BT_MIDI_CHR_UUID);
+
+ if (available && !chr->read_done) {
+ read_probe(impl, chr);
+ available = false;
+ }
+
+ if (available && !chr->dsc_done) {
+ read_dsc(impl, chr);
+ available = chr->dsc_done;
+ }
+
+ if (chr->node_emitted && !available) {
+ remove_chr_node(impl, chr);
+ chr->node_emitted = false;
+ } else if (!chr->node_emitted && available) {
+ emit_chr_node(impl, chr, device);
+ chr->node_emitted = true;
+ }
+}
+
+static GList *get_all_valid_chr(struct impl *impl)
+{
+ GList *lst = NULL;
+ GList *objects;
+
+ if (!dbus_monitor_manager(&impl->monitor)) {
+ /* Still initializing (or it failed) */
+ return NULL;
+ }
+
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
+ for (GList *p = g_list_first(objects); p; p = p->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(p->data));
+
+ for (GList *p2 = g_list_first(interfaces); p2; p2 = p2->next) {
+ MidiEnumCharacteristicProxy *chr;
+
+ if (!MIDI_ENUM_IS_CHARACTERISTIC_PROXY(p2->data))
+ continue;
+
+ chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p2->data);
+ if (chr->impl == NULL)
+ continue;
+
+ lst = g_list_append(lst, g_object_ref(chr));
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+
+ return lst;
+}
+
+static void check_all_nodes(struct impl *impl)
+{
+ /*
+ * Check if the nodes we have emitted are in sync with connected devices.
+ */
+
+ GList *chrs = get_all_valid_chr(impl);
+
+ for (GList *p = chrs; p; p = p->next)
+ check_chr_node(impl, MIDI_ENUM_CHARACTERISTIC_PROXY(p->data));
+
+ g_list_free_full(chrs, g_object_unref);
+}
+
+static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(source_object);
+ struct impl *impl = user_data;
+ GError *err = NULL;
+
+ bluez5_gatt_manager1_call_register_application_finish(
+ BLUEZ5_GATT_MANAGER1(source_object), res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ manager->registered = true;
+
+done:
+ g_clear_object(&manager->register_call);
+}
+
+static int manager_register_application(struct impl *impl, MidiEnumManagerProxy *manager)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ if (manager->registered)
+ return 0;
+ if (manager->register_call)
+ return -EBUSY;
+
+ spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));
+
+ manager->register_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ options,
+ manager->register_call,
+ manager_register_application_reply,
+ impl);
+
+ return 0;
+}
+
+/*
+ * DBus monitoring (Glib)
+ */
+
+static void midi_enum_characteristic_proxy_init(MidiEnumCharacteristicProxy *chr)
+{
+}
+
+static void midi_enum_characteristic_proxy_finalize(GObject *object)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(object);
+
+ g_cancellable_cancel(chr->read_call);
+ g_clear_object(&chr->read_call);
+
+ g_cancellable_cancel(chr->dsc_call);
+ g_clear_object(&chr->dsc_call);
+
+ if (chr->impl && chr->node_emitted)
+ remove_chr_node(chr->impl, chr);
+
+ chr->impl = NULL;
+
+ g_free(chr->description);
+ chr->description = NULL;
+}
+
+static void midi_enum_characteristic_proxy_class_init(MidiEnumCharacteristicProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_enum_characteristic_proxy_finalize;
+}
+
+static void midi_enum_manager_proxy_init(MidiEnumManagerProxy *manager)
+{
+}
+
+static void midi_enum_manager_proxy_finalize(GObject *object)
+{
+ MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(object);
+
+ g_cancellable_cancel(manager->register_call);
+ g_clear_object(&manager->register_call);
+}
+
+static void midi_enum_manager_proxy_class_init(MidiEnumManagerProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_enum_manager_proxy_finalize;
+}
+
+static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ manager_register_application(impl, MIDI_ENUM_MANAGER_PROXY(iface));
+}
+
+static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_enum_manager_proxy_finalize(G_OBJECT(iface));
+}
+
+static void device_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ check_all_nodes(impl);
+}
+
+static void service_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+ Bluez5GattService1 *service = BLUEZ5_GATT_SERVICE1(iface);
+
+ if (!spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID))
+ return;
+
+ check_all_nodes(impl);
+}
+
+static void chr_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(iface);
+
+ if (!spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
+ BT_MIDI_CHR_UUID))
+ return;
+
+ if (chr->impl == NULL) {
+ chr->impl = impl;
+ chr->id = ++impl->id;
+ }
+
+ check_chr_node(impl, chr);
+}
+
+static void chr_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_enum_characteristic_proxy_finalize(G_OBJECT(iface));
+}
+
+static void monitor_start(struct impl *impl)
+{
+ struct dbus_monitor_proxy_type proxy_types[] = {
+ { BLUEZ_DEVICE_INTERFACE, BLUEZ5_TYPE_DEVICE1_PROXY, device_update, NULL },
+ { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_ENUM_TYPE_MANAGER_PROXY, manager_update, manager_clear },
+ { BLUEZ_GATT_SERVICE_INTERFACE, BLUEZ5_TYPE_GATT_SERVICE1_PROXY, service_update, NULL },
+ { BLUEZ_GATT_CHR_INTERFACE, MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY, chr_update, chr_clear },
+ { BLUEZ_GATT_DSC_INTERFACE, BLUEZ5_TYPE_GATT_DESCRIPTOR1_PROXY, NULL, NULL },
+ { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
+ { NULL, G_TYPE_INVALID, NULL, NULL }
+ };
+
+ SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);
+
+ dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
+ impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, NULL);
+}
+
+/*
+ * DBus GATT profile, to enable BlueZ autoconnect
+ */
+
+static gboolean profile_handle_release(Bluez5GattProfile1 *iface, GDBusMethodInvocation *invocation)
+{
+ bluez5_gatt_profile1_complete_release(iface, invocation);
+ return TRUE;
+}
+
+static int export_profile(struct impl *impl)
+{
+ static const char *uuids[] = { BT_MIDI_SERVICE_UUID, NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattProfile1 *iface = NULL;
+ int res = -ENOMEM;
+
+ iface = bluez5_gatt_profile1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ skeleton = g_dbus_object_skeleton_new(MIDI_PROFILE_PATH);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_profile1_set_uuids(iface, uuids);
+ g_signal_connect(iface, "handle-release", G_CALLBACK(profile_handle_release), NULL);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Profile exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+/*
+ * Monitor impl
+ */
+
+static int impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ GList *chrs;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ chrs = get_all_valid_chr(this);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ for (GList *p = g_list_first(chrs); p; p = p->next) {
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p->data);
+ Bluez5Device1 *device;
+ Bluez5GattService1 *service;
+
+ if (!chr->node_emitted)
+ continue;
+
+ lookup_chr_node(this, chr, &service, &device);
+ if (device)
+ emit_chr_node(this, chr, device);
+ }
+ g_list_free_full(chrs, g_object_unref);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ this = (struct impl *) handle;
+
+ dbus_monitor_clear(&this->monitor);
+ g_clear_object(&this->manager);
+ g_clear_object(&this->conn);
+
+ spa_zero(*this);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ GError *error = NULL;
+ int res = 0;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ if (this->log == NULL)
+ return -EINVAL;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
+ spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
+ SPA_KEY_API_GLIB_MAINLOOP);
+ return -EINVAL;
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
+ if (!this->conn) {
+ spa_log_error(this->log, "Creating GDBus connection failed: %s",
+ error->message);
+ g_error_free(error);
+ goto fail;
+ }
+
+ this->manager = g_dbus_object_manager_server_new(MIDI_OBJECT_PATH);
+ if (!this->manager){
+ spa_log_error(this->log, "Creating GDBus object manager failed");
+ goto fail;
+ }
+
+ if ((res = export_profile(this)) < 0)
+ goto fail;
+
+ g_dbus_object_manager_server_set_connection(this->manager, this->conn);
+
+ monitor_start(this);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ impl_clear(handle);
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_bluez5_midi_enum_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MIDI_ENUM,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c
new file mode 100644
index 0000000..c6a7e46
--- /dev/null
+++ b/spa/plugins/bluez5/midi-node.c
@@ -0,0 +1,2151 @@
+/* Spa MIDI node
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/utils/dll.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/monitor/device.h>
+#include <spa/control/control.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+
+#include "midi.h"
+
+#include "bluez5-interface-gen.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.node");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+#define DLL_BW 0.05
+
+#define DEFAULT_LATENCY_OFFSET (0 * SPA_NSEC_PER_MSEC)
+
+#define MAX_BUFFERS 32
+
+#define MIDI_RINGBUF_SIZE (8192*4)
+
+enum node_role {
+ NODE_SERVER,
+ NODE_CLIENT,
+};
+
+struct props {
+ char clock_name[64];
+ char device_name[512];
+ int64_t latency_offset;
+};
+
+struct midi_event_ringbuffer_entry {
+ uint64_t time;
+ unsigned int size;
+};
+
+struct midi_event_ringbuffer {
+ struct spa_ringbuffer rbuf;
+ uint8_t buf[MIDI_RINGBUF_SIZE];
+};
+
+struct buffer {
+ uint32_t id;
+ unsigned int outgoing:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct time_sync {
+ uint64_t prev_recv_time;
+ uint64_t recv_time;
+
+ uint16_t prev_device_timestamp;
+ uint16_t device_timestamp;
+
+ uint64_t device_time;
+
+ struct spa_dll dll;
+};
+
+struct port {
+ uint32_t id;
+ enum spa_direction direction;
+
+ struct spa_audio_info current_format;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ int fd;
+ uint16_t mtu;
+
+ struct buffer *buffer;
+ struct spa_pod_builder builder;
+ struct spa_pod_frame frame;
+
+ struct time_sync sync;
+
+ unsigned int acquired:1;
+ GCancellable *acquire_call;
+
+ struct spa_source source;
+
+ struct impl *impl;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ GDBusConnection *conn;
+ Bluez5GattCharacteristic1 *proxy;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+#define PORT_IN 0
+#define PORT_OUT 1
+#define N_PORTS 2
+ struct port ports[N_PORTS];
+
+ char *chr_path;
+
+ unsigned int started:1;
+ unsigned int following:1;
+
+ struct spa_source timer_source;
+
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint32_t duration;
+ uint32_t rate;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ struct midi_event_ringbuffer event_rbuf;
+
+ struct spa_bt_midi_parser parser;
+ struct spa_bt_midi_parser tmp_parser;
+ uint8_t read_buffer[MIDI_MAX_MTU];
+
+ struct spa_bt_midi_writer writer;
+
+ enum node_role role;
+
+ struct spa_bt_midi_server *server;
+};
+
+#define CHECK_PORT(this,d,p) ((p) == 0 && ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT))
+#define GET_PORT(this,d,p) (&(this)->ports[(d) == SPA_DIRECTION_OUTPUT ? PORT_OUT : PORT_IN])
+
+static void midi_event_ringbuffer_init(struct midi_event_ringbuffer *mbuf)
+{
+ spa_ringbuffer_init(&mbuf->rbuf);
+}
+
+static int midi_event_ringbuffer_push(struct midi_event_ringbuffer *mbuf,
+ uint64_t time, uint8_t *event, unsigned int size)
+{
+ const unsigned int bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt = {
+ .time = time,
+ .size = size
+ };
+
+ avail = spa_ringbuffer_get_write_index(&mbuf->rbuf, &index);
+ if (avail < 0 || avail + sizeof(evt) + size > bufsize)
+ return -ENOSPC;
+
+ spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+ index += sizeof(evt);
+ spa_ringbuffer_write_update(&mbuf->rbuf, index);
+ spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ event, size);
+ index += size;
+ spa_ringbuffer_write_update(&mbuf->rbuf, index);
+
+ return 0;
+}
+
+static int midi_event_ringbuffer_peek(struct midi_event_ringbuffer *mbuf, uint64_t *time, unsigned int *size)
+{
+ const unsigned bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt;
+
+ avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index);
+ if (avail < (int)sizeof(evt))
+ return -ENOENT;
+
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+
+ *time = evt.time;
+ *size = evt.size;
+ return 0;
+}
+
+static int midi_event_ringbuffer_pop(struct midi_event_ringbuffer *mbuf, uint8_t *data, size_t max_size)
+{
+ const unsigned bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt;
+
+ avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index);
+ if (avail < (int)sizeof(evt))
+ return -ENOENT;
+
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+ index += sizeof(evt);
+ avail -= sizeof(evt);
+ spa_ringbuffer_read_update(&mbuf->rbuf, index);
+
+ if ((uint32_t)avail < evt.size) {
+ /* corrupted ringbuffer: should never happen */
+ spa_assert_not_reached();
+ return -EINVAL;
+ }
+
+ if (evt.size <= max_size)
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ data, SPA_MIN(max_size, evt.size));
+ index += evt.size;
+ spa_ringbuffer_read_update(&mbuf->rbuf, index);
+
+ if (evt.size > max_size)
+ return -ENOSPC;
+
+ return 0;
+}
+
+static void reset_props(struct props *props)
+{
+ props->latency_offset = DEFAULT_LATENCY_OFFSET;
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+ props->device_name[0] = '\0';
+}
+
+static bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outgoing) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outgoing = false;
+ }
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ spa_list_append(&port->free, &b->link);
+ b->outgoing = false;
+ } else {
+ b->outgoing = true;
+ }
+ }
+}
+
+static struct buffer *peek_buffer(struct impl *this, struct port *port)
+{
+ if (spa_list_is_empty(&port->free))
+ return NULL;
+ return spa_list_first(&port->free, struct buffer, link);
+}
+
+static int prepare_buffer(struct impl *this, struct port *port)
+{
+ if (port->buffer != NULL)
+ return 0;
+ if ((port->buffer = peek_buffer(this, port)) == NULL)
+ return -EPIPE;
+
+ spa_pod_builder_init(&port->builder,
+ port->buffer->buf->datas[0].data,
+ port->buffer->buf->datas[0].maxsize);
+ spa_pod_builder_push_sequence(&port->builder, &port->frame, 0);
+
+ return 0;
+}
+
+static int finish_buffer(struct impl *this, struct port *port)
+{
+ if (port->buffer == NULL)
+ return 0;
+
+ spa_pod_builder_pop(&port->builder, &port->frame);
+
+ port->buffer->buf->datas[0].chunk->offset = 0;
+ port->buffer->buf->datas[0].chunk->size = port->builder.state.offset;
+
+ /* move buffer to ready queue */
+ spa_list_remove(&port->buffer->link);
+ spa_list_append(&port->ready, &port->buffer->link);
+ port->buffer = NULL;
+
+ return 0;
+}
+
+/* Replace value -> value + n*period, to minimize |value - target| */
+static int64_t unwrap_to_closest(int64_t value, int64_t target, int64_t period)
+{
+ if (value > target)
+ value -= SPA_ROUND_DOWN(value - target + period/2, period);
+ if (value < target)
+ value += SPA_ROUND_DOWN(target - value + period/2, period);
+ return value;
+}
+
+static int64_t time_diff(uint64_t a, uint64_t b)
+{
+ if (a >= b)
+ return a - b;
+ else
+ return -(int64_t)(b - a);
+}
+
+static void midi_event_get_last_timestamp(void *user_data, uint16_t timestamp, uint8_t *data, size_t size)
+{
+ int *last_timestamp = user_data;
+ *last_timestamp = timestamp;
+}
+
+static uint64_t midi_convert_time(struct time_sync *sync, uint16_t timestamp)
+{
+ int offset;
+
+ /*
+ * sync->device_timestamp is a device timestamp that corresponds to system
+ * clock time sync->device_time.
+ *
+ * It is the timestamp of the last MIDI event in the current packet, so we can
+ * assume here no event here has timestamp after it.
+ */
+ if (timestamp > sync->device_timestamp)
+ offset = sync->device_timestamp + MIDI_CLOCK_PERIOD_MSEC - timestamp;
+ else
+ offset = sync->device_timestamp - timestamp;
+
+ return sync->device_time - offset * SPA_NSEC_PER_MSEC;
+}
+
+static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data, size_t size)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->ports[PORT_OUT];
+ struct time_sync *sync = &port->sync;
+ uint64_t time;
+ int res;
+
+ spa_assert(size > 0);
+
+ time = midi_convert_time(sync, timestamp);
+
+ spa_log_trace(this->log, "%p: event:0x%x size:%d timestamp:%d time:%"PRIu64"",
+ this, (int)data[0], (int)size, (int)timestamp, (uint64_t)time);
+
+ res = midi_event_ringbuffer_push(&this->event_rbuf, time, data, size);
+ if (res < 0) {
+ midi_event_ringbuffer_init(&this->event_rbuf);
+ spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s",
+ this, spa_strerror(res));
+ }
+}
+
+static int unacquire_port(struct port *port)
+{
+ struct impl *this = port->impl;
+
+ if (!port->acquired)
+ return 0;
+
+ spa_log_debug(this->log, "%p: unacquire port:%d", this, port->direction);
+
+ shutdown(port->fd, SHUT_RDWR);
+ close(port->fd);
+ port->fd = -1;
+ port->acquired = false;
+
+ if (this->server)
+ spa_bt_midi_server_released(this->server,
+ (port->direction == SPA_DIRECTION_OUTPUT));
+
+ return 0;
+}
+
+static int do_unacquire_port(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct port *port = user_data;
+
+ /* in main thread */
+ unacquire_port(port);
+ return 0;
+}
+
+static void on_ready_read(struct spa_source *source)
+{
+ struct port *port = source->data;
+ struct impl *this = port->impl;
+ struct timespec now;
+ int res, size, last_timestamp;
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) ||
+ SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) {
+ spa_log_debug(this->log, "%p: port:%d ERR/HUP", this, port->direction);
+ goto stop;
+ }
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+
+ /* read data from socket */
+again:
+ size = recv(port->fd, this->read_buffer, sizeof(this->read_buffer), MSG_DONTWAIT | MSG_NOSIGNAL);
+ if (size == 0) {
+ return;
+ } else if (size < 0) {
+ if (errno == EINTR)
+ goto again;
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return;
+ goto stop;
+ }
+
+ spa_log_trace(this->log, "%p: port:%d recv data size:%d", this, port->direction, size);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->read_buffer, size);
+
+ if (port->direction != SPA_DIRECTION_OUTPUT) {
+ /* Just monitor errors for the input port */
+ spa_log_debug(this->log, "%p: port:%d is not RX port; ignoring data",
+ this, port->direction);
+ return;
+ }
+
+ /* prepare for producing events */
+ if (port->io == NULL || port->n_buffers == 0 || !this->started)
+ return;
+
+ /*
+ * Remote clock synchronization:
+ *
+ * Assume: Last timestamp in packet on average corresponds to packet send time.
+ * There is some unknown latency in between, but on average it is constant.
+ *
+ * The `device_time` computed below is the estimated wall-clock time
+ * corresponding to the timestamp `device_timestamp` of the last event
+ * in the packet. This timestamp is late by the average transmission latency,
+ * which is unknown.
+ *
+ * Packet reception jitter and any clock drift is smoothed over with DLL.
+ * The estimated timestamps are stable and preserve event intervals.
+ *
+ * To allow latency_offset to work better, we don't write the events
+ * to the output buffer here, but instead put them to a ringbuffer.
+ * This is because if the offset shifts events to later buffers,
+ * this is simpler to handle with the rbuf.
+ */
+ last_timestamp = -1;
+ spa_bt_midi_parser_dup(&this->parser, &this->tmp_parser, true);
+ res = spa_bt_midi_parser_parse(&this->tmp_parser, this->read_buffer, size, true,
+ midi_event_get_last_timestamp, &last_timestamp);
+ if (res >= 0 && last_timestamp >= 0) {
+ struct time_sync *sync = &port->sync;
+ int64_t clock_elapsed;
+ int64_t device_elapsed;
+ int64_t err_nsec;
+ double corr, tcorr;
+
+ sync->prev_recv_time = sync->recv_time;
+ sync->recv_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ sync->prev_device_timestamp = sync->device_timestamp;
+ sync->device_timestamp = last_timestamp;
+
+ if (port->sync.prev_recv_time == 0) {
+ sync->prev_recv_time = sync->recv_time;
+ sync->prev_device_timestamp = sync->device_timestamp;
+ spa_dll_init(&sync->dll);
+ }
+ if (SPA_UNLIKELY(sync->dll.bw == 0))
+ spa_dll_set_bw(&sync->dll, DLL_BW, 1024, 48000);
+
+ /* move device clock forward */
+ clock_elapsed = sync->recv_time - sync->prev_recv_time;
+
+ device_elapsed = (int)sync->device_timestamp - (int)sync->prev_device_timestamp;
+ device_elapsed *= SPA_NSEC_PER_MSEC;
+ device_elapsed = unwrap_to_closest(device_elapsed, clock_elapsed, MIDI_CLOCK_PERIOD_NSEC);
+ sync->device_time += device_elapsed;
+
+ /* smooth clock sync */
+ err_nsec = time_diff(sync->recv_time, sync->device_time);
+ corr = spa_dll_update(&sync->dll,
+ -SPA_CLAMP(err_nsec, -20*SPA_NSEC_PER_MSEC, 20*SPA_NSEC_PER_MSEC)
+ * this->rate / SPA_NSEC_PER_SEC);
+ tcorr = SPA_MIN(device_elapsed, SPA_NSEC_PER_SEC) * (corr - 1);
+ sync->device_time += tcorr;
+
+ /* reset if too much off */
+ if (err_nsec < -50 * SPA_NSEC_PER_MSEC ||
+ err_nsec > 200 * SPA_NSEC_PER_MSEC ||
+ SPA_ABS(tcorr) > 20*SPA_NSEC_PER_MSEC ||
+ device_elapsed < 0) {
+ spa_log_debug(this->log, "%p: device clock sync off too much: resync", this);
+ spa_dll_init(&sync->dll);
+ sync->device_time = sync->recv_time;
+ }
+
+ spa_log_debug(this->log,
+ "timestamp:%d dt:%d dt2:%d err:%.1f tcorr:%.2f (ms) corr:%f",
+ (int)sync->device_timestamp,
+ (int)(clock_elapsed/SPA_NSEC_PER_MSEC),
+ (int)(device_elapsed/SPA_NSEC_PER_MSEC),
+ (double)err_nsec / SPA_NSEC_PER_MSEC,
+ tcorr/SPA_NSEC_PER_MSEC,
+ corr);
+ }
+
+ /* put midi event data to the buffer */
+ res = spa_bt_midi_parser_parse(&this->parser, this->read_buffer, size, false,
+ midi_event_recv, this);
+ if (res < 0) {
+ /* bad data */
+ spa_bt_midi_parser_init(&this->parser);
+
+ spa_log_info(this->log, "BLE MIDI data packet parsing failed: %d", res);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 4, this->read_buffer, size);
+ }
+
+ return;
+
+stop:
+ spa_log_debug(this->log, "%p: port:%d stopping port", this, port->direction);
+
+ if (port->source.loop)
+ spa_loop_remove_source(this->data_loop, &port->source);
+
+ /* port->acquired is updated only from the main thread */
+ spa_loop_invoke(this->main_loop, do_unacquire_port, 0, NULL, 0, false, port);
+}
+
+static int process_output(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_OUT];
+ struct buffer *buffer;
+ struct spa_io_buffers *io = port->io;
+
+ /* Check if we are able to process */
+ if (io == NULL || !port->acquired)
+ return SPA_STATUS_OK;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Produce buffer */
+ if (prepare_buffer(this, port) >= 0) {
+ /*
+ * this->current_time is at the end time of the buffer, and offsets
+ * are recorded vs. the start of the buffer.
+ */
+ const uint64_t start_time = this->current_time
+ - this->duration * SPA_NSEC_PER_SEC / this->rate;
+ const uint64_t end_time = this->current_time;
+ uint64_t time;
+ uint32_t offset;
+ void *buf;
+ unsigned int size;
+ int res;
+
+ while (true) {
+ res = midi_event_ringbuffer_peek(&this->event_rbuf, &time, &size);
+ if (res < 0)
+ break;
+
+ time -= this->props.latency_offset;
+
+ if (time > end_time) {
+ break;
+ } else if (time + SPA_NSEC_PER_MSEC < start_time) {
+ /* Log events in the past by more than 1 ms, but don't
+ * do anything about them. The user can change the latency
+ * offset to choose whether to tradeoff latency for more
+ * accurate timestamps.
+ *
+ * TODO: maybe this information should be available in
+ * a more visible place, some latency property?
+ */
+ spa_log_debug(this->log, "%p: event in the past by %d ms",
+ this, (int)((start_time - time) / SPA_NSEC_PER_MSEC));
+ }
+
+ time = SPA_MAX(time, start_time) - start_time;
+ offset = time * this->rate / SPA_NSEC_PER_SEC;
+ offset = SPA_CLAMP(offset, 0u, this->duration - 1);
+
+ spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi);
+ buf = spa_pod_builder_reserve_bytes(&port->builder, size);
+ if (buf) {
+ midi_event_ringbuffer_pop(&this->event_rbuf, buf, size);
+
+ spa_log_trace(this->log, "%p: produce event:0x%x offset:%d time:%"PRIu64"",
+ this, (int)*(uint8_t*)buf, (int)offset,
+ (uint64_t)(start_time + offset * SPA_NSEC_PER_SEC / this->rate));
+ }
+ }
+
+ finish_buffer(this, port);
+ }
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outgoing = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int flush_packet(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_IN];
+ int res;
+
+ if (this->writer.size == 0)
+ return 0;
+
+ res = send(port->fd, this->writer.buf, this->writer.size,
+ MSG_DONTWAIT | MSG_NOSIGNAL);
+ if (res < 0)
+ return -errno;
+
+ spa_log_trace(this->log, "%p: send packet size:%d", this, this->writer.size);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->writer.buf, this->writer.size);
+
+ return 0;
+}
+
+static int write_data(struct impl *this, struct spa_data *d)
+{
+ struct port *port = &this->ports[PORT_IN];
+ struct spa_pod_sequence *pod;
+ struct spa_pod_control *c;
+ uint64_t time;
+ int res;
+
+ pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
+ if (pod == NULL) {
+ spa_log_warn(this->log, "%p: invalid sequence in buffer max:%u offset:%u size:%u",
+ this, d->maxsize, d->chunk->offset, d->chunk->size);
+ return -EINVAL;
+ }
+
+ spa_bt_midi_writer_init(&this->writer, port->mtu);
+ time = 0;
+
+ SPA_POD_SEQUENCE_FOREACH(pod, c) {
+ uint8_t *event;
+ size_t size;
+
+ if (c->type != SPA_CONTROL_Midi)
+ continue;
+
+ time = SPA_MAX(time, this->current_time + c->offset * SPA_NSEC_PER_SEC / this->rate);
+ event = SPA_POD_BODY(&c->value);
+ size = SPA_POD_BODY_SIZE(&c->value);
+
+ spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this,
+ (size > 0) ? event[0] : 0, time);
+
+ do {
+ res = spa_bt_midi_writer_write(&this->writer,
+ time, event, size);
+ if (res < 0) {
+ return res;
+ } else if (res) {
+ int res2;
+ if ((res2 = flush_packet(this)) < 0)
+ return res2;
+ }
+ } while (res);
+ }
+
+ if ((res = flush_packet(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int process_input(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_IN];
+ struct buffer *b;
+ struct spa_io_buffers *io = port->io;
+ int res;
+
+ /* Check if we are able to process */
+ if (io == NULL || !port->acquired)
+ return SPA_STATUS_OK;
+
+ if (io->status != SPA_STATUS_HAVE_DATA || io->buffer_id >= port->n_buffers)
+ return SPA_STATUS_OK;
+
+ b = &port->buffers[io->buffer_id];
+ if (!b->outgoing) {
+ spa_log_warn(this->log, "%p: buffer %u not outgoing", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ if ((res = write_data(this, &b->buf->datas[0])) < 0) {
+ spa_log_info(this->log, "%p: writing data failed: %s",
+ this, spa_strerror(res));
+ }
+
+ port->io->buffer_id = b->id;
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, io->buffer_id);
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static void update_position(struct impl *this)
+{
+ if (SPA_LIKELY(this->position)) {
+ this->duration = this->position->clock.duration;
+ this->rate = this->position->clock.rate.denom;
+ } else {
+ this->duration = 1024;
+ this->rate = 48000;
+ }
+}
+
+static void on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ uint64_t prev_time, now_time;
+ int status;
+
+ if (!this->started)
+ return;
+
+ if (spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
+ spa_log_warn(this->log, "%p: error reading timerfd: %s", this, strerror(errno));
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ update_position(this);
+
+ this->next_time = now_time + this->duration * SPA_NSEC_PER_SEC / this->rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += this->duration;
+ this->clock->duration = this->duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ status = process_output(this);
+ spa_log_trace(this->log, "%p: status:%d", this, status);
+
+ spa_node_call_ready(&this->callbacks, status | SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this);
+
+static int do_release(struct impl *this);
+
+static int do_stop(struct impl *this);
+
+static void acquire_reply(GObject *source_object, GAsyncResult *res, gpointer user_data, bool notify)
+{
+ struct port *port;
+ struct impl *this;
+ const char *method;
+ GError *err = NULL;
+ GUnixFDList *fd_list = NULL;
+ GVariant *fd_handle = NULL;
+ int fd;
+ guint16 mtu;
+
+ if (notify) {
+ bluez5_gatt_characteristic1_call_acquire_notify_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err);
+ } else {
+ bluez5_gatt_characteristic1_call_acquire_write_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err);
+ }
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now. */
+ g_error_free(err);
+ return;
+ }
+
+ port = user_data;
+ this = port->impl;
+ method = notify ? "AcquireNotify" : "AcquireWrite";
+ if (err) {
+ spa_log_error(this->log, "%s.%s() for %s failed: %s",
+ BLUEZ_GATT_CHR_INTERFACE, method,
+ this->chr_path, err->message);
+ goto fail;
+ }
+
+ fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(fd_handle), &err);
+ if (fd < 0) {
+ spa_log_error(this->log, "%s.%s() for %s failed to get fd: %s",
+ BLUEZ_GATT_CHR_INTERFACE, method,
+ this->chr_path, err->message);
+ goto fail;
+ }
+
+ spa_log_info(this->log, "%p: BLE MIDI %s %s success mtu:%d",
+ this, this->chr_path, method, mtu);
+ port->fd = fd;
+ port->mtu = mtu;
+ port->acquired = true;
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ spa_bt_midi_parser_init(&this->parser);
+
+ /* Start source */
+ port->source.data = port;
+ port->source.fd = port->fd;
+ port->source.func = on_ready_read;
+ port->source.mask = SPA_IO_IN | SPA_IO_HUP | SPA_IO_ERR;
+ port->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &port->source);
+ }
+ return;
+
+fail:
+ g_error_free(err);
+ g_clear_object(&fd_list);
+ g_clear_object(&fd_handle);
+ do_stop(this);
+ do_release(this);
+}
+
+static void acquire_notify_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ acquire_reply(source_object, res, user_data, true);
+}
+
+static void acquire_write_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ acquire_reply(source_object, res, user_data, false);
+}
+
+static int do_acquire(struct port *port)
+{
+ struct impl *this = port->impl;
+ const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ?
+ "AcquireNotify" : "AcquireWrite";
+ GVariant *options;
+ GVariantBuilder builder;
+
+ if (port->acquired)
+ return 0;
+ if (port->acquire_call)
+ return 0;
+
+ spa_log_info(this->log,
+ "%p: port %d: client %s for BLE MIDI device characteristic %s",
+ this, port->direction, method, this->chr_path);
+
+ port->acquire_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ bluez5_gatt_characteristic1_call_acquire_notify(
+ BLUEZ5_GATT_CHARACTERISTIC1(this->proxy),
+ options,
+ NULL,
+ port->acquire_call,
+ acquire_notify_reply,
+ port);
+ } else {
+ bluez5_gatt_characteristic1_call_acquire_write(
+ BLUEZ5_GATT_CHARACTERISTIC1(this->proxy),
+ options,
+ NULL,
+ port->acquire_call,
+ acquire_write_reply,
+ port);
+ }
+
+ return 0;
+}
+
+static int server_do_acquire(struct port *port, int fd, uint16_t mtu)
+{
+ struct impl *this = port->impl;
+ const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ?
+ "AcquireWrite" : "AcquireNotify";
+
+ spa_log_info(this->log,
+ "%p: port %d: server %s for BLE MIDI device characteristic %s",
+ this, port->direction, method, this->server->chr_path);
+
+ if (port->acquired) {
+ spa_log_info(this->log,
+ "%p: port %d: %s failed: already acquired",
+ this, port->direction, method);
+ return -EBUSY;
+ }
+
+ port->fd = fd;
+ port->mtu = mtu;
+
+ if (port->direction == SPA_DIRECTION_OUTPUT)
+ spa_bt_midi_parser_init(&this->parser);
+
+ /* Start source */
+ port->source.data = port;
+ port->source.fd = port->fd;
+ port->source.func = on_ready_read;
+ port->source.mask = SPA_IO_HUP | SPA_IO_ERR;
+ if (port->direction == SPA_DIRECTION_OUTPUT)
+ port->source.mask |= SPA_IO_IN;
+ port->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &port->source);
+
+ port->acquired = true;
+ return 0;
+}
+
+static int server_acquire_write(void *user_data, int fd, uint16_t mtu)
+{
+ struct impl *this = user_data;
+ return server_do_acquire(&this->ports[PORT_OUT], fd, mtu);
+}
+
+static int server_acquire_notify(void *user_data, int fd, uint16_t mtu)
+{
+ struct impl *this = user_data;
+ return server_do_acquire(&this->ports[PORT_IN], fd, mtu);
+}
+
+static int server_release(void *user_data)
+{
+ struct impl *this = user_data;
+ do_release(this);
+ return 0;
+}
+
+static const char *server_description(void *user_data)
+{
+ struct impl *this = user_data;
+ return this->props.device_name;
+}
+
+static int do_remove_port_source(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *this = user_data;
+ int i;
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ if (port->source.loop)
+ spa_loop_remove_source(this->data_loop, &port->source);
+ }
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ spa_log_debug(this->log, "%p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ return res;
+}
+
+static int do_release(struct impl *this)
+{
+ int res = 0;
+ size_t i;
+
+ spa_log_debug(this->log, "%p: release", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_port_source, 0, NULL, 0, true, this);
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ g_cancellable_cancel(port->acquire_call);
+ g_clear_object(&port->acquire_call);
+
+ unacquire_port(port);
+ }
+
+ return res;
+}
+
+static int do_start(struct impl *this)
+{
+ int res;
+ size_t i;
+
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+
+ update_position(this);
+
+ spa_log_debug(this->log, "%p: start following:%d",
+ this, this->following);
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ switch (this->role) {
+ case NODE_CLIENT:
+ /* Acquire Bluetooth I/O */
+ if ((res = do_acquire(port)) < 0) {
+ do_stop(this);
+ do_release(this);
+ return res;
+ }
+ break;
+ case NODE_SERVER:
+ /*
+ * In MIDI server role, the device/BlueZ invokes
+ * the acquire asynchronously as available/needed.
+ */
+ break;
+ default:
+ spa_assert_not_reached();
+ }
+
+ reset_buffers(port);
+ }
+
+ midi_event_ringbuffer_init(&this->event_rbuf);
+
+ this->started = true;
+
+ /* Start timer */
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ set_timers(this);
+
+ return 0;
+}
+
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+
+ set_timers(this);
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("Device name"),
+ SPA_PROP_INFO_type, SPA_POD_String(p->device_name));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset),
+ SPA_PROP_deviceName, SPA_POD_String(p->device_name));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full);
+
+static void set_latency(struct impl *this, bool emit_latency)
+{
+ struct port *port = &this->ports[PORT_OUT];
+
+ port->latency.min_ns = port->latency.max_ns = this->props.latency_offset;
+
+ if (emit_latency) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ }
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset),
+ SPA_PROP_deviceName, SPA_POD_OPT_Stringn(new_props.device_name,
+ sizeof(new_props.device_name)));
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+
+ if (changed)
+ set_latency(this, true);
+
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ int res, res2;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ res = do_stop(this);
+ if (this->role == NODE_CLIENT)
+ res2 = do_release(this);
+ else
+ res2 = 0;
+ if (res < 0)
+ return res;
+ if (res2 < 0)
+ return res2;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" },
+ };
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ size_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+
+ for (i = 0; i < N_PORTS; ++i)
+ emit_port_info(this, &this->ports[i], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ 4096, 4096, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_application ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, 1);
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: use buffers %d", this, n_buffers);
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ reset_buffers(port);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id);
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ int status = SPA_STATUS_OK;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (!this->started)
+ return SPA_STATUS_OK;
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now = { 0 };
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ update_position(this);
+
+ if (this->following)
+ status |= process_output(this);
+
+ status |= process_input(this);
+
+ return status;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static const struct spa_bt_midi_server_cb impl_server = {
+ .acquire_write = server_acquire_write,
+ .acquire_notify = server_acquire_notify,
+ .release = server_release,
+ .get_description = server_description,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ do_release(this);
+
+ free(this->chr_path);
+ if (this->timerfd > 0)
+ spa_system_close(this->data_system, this->timerfd);
+ if (this->server)
+ spa_bt_midi_server_destroy(this->server);
+ g_clear_object(&this->proxy);
+ g_clear_object(&this->conn);
+
+ spa_zero(*this);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ const char *device_name = "";
+ int res = 0;
+ GError *err = NULL;
+ size_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ if (this->log == NULL)
+ return -EINVAL;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
+ spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
+ SPA_KEY_API_GLIB_MAINLOOP);
+ return -EINVAL;
+ }
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->role = NODE_CLIENT;
+
+ if (info) {
+ const char *str;
+
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_PATH)) != NULL)
+ this->chr_path = strdup(str);
+
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_ROLE)) != NULL) {
+ if (spa_streq(str, "server"))
+ this->role = NODE_SERVER;
+ }
+
+ if ((str = spa_dict_lookup(info, "node.nick")) != NULL)
+ device_name = str;
+ else if ((str = spa_dict_lookup(info, "node.description")) != NULL)
+ device_name = str;
+ }
+
+ if (this->role == NODE_CLIENT && this->chr_path == NULL) {
+ spa_log_error(this->log, "missing MIDI service characteristic path");
+ res = -EINVAL;
+ goto fail;
+ }
+
+ this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err);
+ if (this->conn == NULL) {
+ spa_log_error(this->log, "failed to get dbus connection: %s",
+ err->message);
+ g_error_free(err);
+ res = -EIO;
+ goto fail;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ spa_scnprintf(this->props.device_name, sizeof(this->props.device_name),
+ "%s", device_name);
+
+ /* set the node info */
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+ static const struct spa_dict_item in_port_items[] = {
+ SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"),
+ };
+ static const struct spa_dict_item out_port_items[] = {
+ SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"),
+ };
+ static const struct spa_dict in_port_props = SPA_DICT_INIT_ARRAY(in_port_items);
+ static const struct spa_dict out_port_props = SPA_DICT_INIT_ARRAY(out_port_items);
+
+ spa_zero(*port);
+
+ port->impl = this;
+
+ port->id = 0;
+ port->direction = (i == PORT_OUT) ? SPA_DIRECTION_OUTPUT :
+ SPA_DIRECTION_INPUT;
+
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+ port->info.props = (i == PORT_OUT) ? &out_port_props : &in_port_props;
+
+ port->latency = SPA_LATENCY_INFO(port->direction);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+ }
+
+ this->duration = 1024;
+ this->rate = 48000;
+
+ set_latency(this, false);
+
+ if (this->role == NODE_SERVER) {
+ this->server = spa_bt_midi_server_new(&impl_server, this->conn, this->log, this);
+ if (this->server == NULL)
+ goto fail;
+ } else {
+ this->proxy = bluez5_gatt_characteristic1_proxy_new_sync(this->conn,
+ G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+ BLUEZ_SERVICE,
+ this->chr_path,
+ NULL,
+ &err);
+ if (this->proxy == NULL) {
+ spa_log_error(this->log,
+ "Failed to create BLE MIDI GATT proxy %s: %s",
+ this->chr_path, err->message);
+ g_error_free(err);
+ res = -EIO;
+ goto fail;
+ }
+ }
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ impl_clear(handle);
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_bluez5_midi_node_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MIDI_NODE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/midi-parser.c b/spa/plugins/bluez5/midi-parser.c
new file mode 100644
index 0000000..ba3cd32
--- /dev/null
+++ b/spa/plugins/bluez5/midi-parser.c
@@ -0,0 +1,295 @@
+/* BLE MIDI parser
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+
+#include "midi.h"
+
+enum midi_event_class {
+ MIDI_BASIC,
+ MIDI_SYSEX,
+ MIDI_SYSCOMMON,
+ MIDI_REALTIME,
+ MIDI_ERROR
+};
+
+static enum midi_event_class midi_event_info(uint8_t status, unsigned int *size)
+{
+ switch (status) {
+ case 0x80 ... 0x8f:
+ case 0x90 ... 0x9f:
+ case 0xa0 ... 0xaf:
+ case 0xb0 ... 0xbf:
+ case 0xe0 ... 0xef:
+ *size = 3;
+ return MIDI_BASIC;
+ case 0xc0 ... 0xcf:
+ case 0xd0 ... 0xdf:
+ *size = 2;
+ return MIDI_BASIC;
+ case 0xf0:
+ /* variable; count only status byte here */
+ *size = 1;
+ return MIDI_SYSEX;
+ case 0xf1:
+ case 0xf3:
+ *size = 2;
+ return MIDI_SYSCOMMON;
+ case 0xf2:
+ *size = 3;
+ return MIDI_SYSCOMMON;
+ case 0xf6:
+ case 0xf7:
+ *size = 1;
+ return MIDI_SYSCOMMON;
+ case 0xf8 ... 0xff:
+ *size = 1;
+ return MIDI_REALTIME;
+ case 0xf4:
+ case 0xf5:
+ default:
+ /* undefined MIDI status */
+ *size = 0;
+ return MIDI_ERROR;
+ }
+}
+
+static void timestamp_set_high(uint16_t *time, uint8_t byte)
+{
+ *time = (byte & 0x3f) << 7;
+}
+
+static void timestamp_set_low(uint16_t *time, uint8_t byte)
+{
+ if ((*time & 0x7f) > (byte & 0x7f))
+ *time += 0x80;
+
+ *time &= ~0x7f;
+ *time |= byte & 0x7f;
+}
+
+int spa_bt_midi_parser_parse(struct spa_bt_midi_parser *parser,
+ const uint8_t *src, size_t src_size, bool only_time,
+ void (*event)(void *user_data, uint16_t time, uint8_t *event, size_t event_size),
+ void *user_data)
+{
+ const uint8_t *src_end = src + src_size;
+ uint8_t running_status = 0;
+ uint16_t time;
+ uint8_t byte;
+
+#define NEXT() do { if (src == src_end) return -EINVAL; byte = *src++; } while (0)
+#define PUT(byte) do { if (only_time) { parser->size++; break; } \
+ if (parser->size == sizeof(parser->buf)) return -ENOSPC; \
+ parser->buf[parser->size++] = (byte); } while (0)
+
+ /* Header */
+ NEXT();
+ if (!(byte & 0x80))
+ return -EINVAL;
+ timestamp_set_high(&time, byte);
+
+ while (src < src_end) {
+ NEXT();
+
+ if (!parser->sysex) {
+ uint8_t status = 0;
+ unsigned int event_size;
+
+ if (byte & 0x80) {
+ /* Timestamp */
+ timestamp_set_low(&time, byte);
+ NEXT();
+
+ /* Status? */
+ if (byte & 0x80) {
+ parser->size = 0;
+ PUT(byte);
+ status = byte;
+ }
+ }
+
+ if (status == 0) {
+ /* Running status */
+ parser->size = 0;
+ PUT(running_status);
+ PUT(byte);
+ status = running_status;
+ }
+
+ switch (midi_event_info(status, &event_size)) {
+ case MIDI_BASIC:
+ running_status = (event_size > 1) ? status : 0;
+ break;
+ case MIDI_REALTIME:
+ case MIDI_SYSCOMMON:
+ /* keep previous running status */
+ break;
+ case MIDI_SYSEX:
+ parser->sysex = true;
+ /* XXX: not fully clear if SYSEX can be running status, assume no */
+ running_status = 0;
+ continue;
+ default:
+ goto malformed;
+ }
+
+ /* Event data */
+ while (parser->size < event_size) {
+ NEXT();
+ if (byte & 0x80) {
+ /* BLE MIDI allows no interleaved events */
+ goto malformed;
+ }
+ PUT(byte);
+ }
+
+ event(user_data, time, parser->buf, parser->size);
+ } else {
+ if (byte & 0x80) {
+ /* Timestamp */
+ timestamp_set_low(&time, byte);
+ NEXT();
+
+ if (byte == 0xf7) {
+ /* Sysex end */
+ PUT(byte);
+ event(user_data, time, parser->buf, parser->size);
+ parser->sysex = false;
+ } else {
+ /* Interleaved realtime event */
+ unsigned int event_size;
+
+ if (midi_event_info(byte, &event_size) != MIDI_REALTIME)
+ goto malformed;
+ spa_assert(event_size == 1);
+ event(user_data, time, &byte, 1);
+ }
+ } else {
+ PUT(byte);
+ }
+ }
+ }
+
+#undef NEXT
+#undef PUT
+
+ return 0;
+
+malformed:
+ /* Error (potentially recoverable) */
+ return -EINVAL;
+}
+
+
+int spa_bt_midi_writer_write(struct spa_bt_midi_writer *writer,
+ uint64_t time, const uint8_t *event, size_t event_size)
+{
+ /* BLE MIDI-1.0: maximum payload size is MTU - 3 */
+ const unsigned int max_size = writer->mtu - 3;
+ const uint64_t time_msec = (time / SPA_NSEC_PER_MSEC);
+ const uint16_t timestamp = time_msec & 0x1fff;
+
+#define PUT(byte) do { if (writer->size >= max_size) return -ENOSPC; \
+ writer->buf[writer->size++] = (byte); } while (0)
+
+ if (writer->mtu < 5+3)
+ return -ENOSPC; /* all events must fit */
+
+ spa_assert(max_size <= sizeof(writer->buf));
+ spa_assert(writer->size <= max_size);
+
+ if (event_size == 0)
+ return 0;
+
+ if (writer->flush) {
+ writer->flush = false;
+ writer->size = 0;
+ }
+
+ if (writer->size == max_size)
+ goto flush;
+
+ /* Packet header */
+ if (writer->size == 0) {
+ PUT(0x80 | (timestamp >> 7));
+ writer->running_status = 0;
+ writer->running_time_msec = time_msec;
+ }
+
+ /* Timestamp low bits can wrap around, but not multiple times */
+ if (time_msec > writer->running_time_msec + 0x7f)
+ goto flush;
+
+ spa_assert(writer->pos < event_size);
+
+ for (; writer->pos < event_size; ++writer->pos) {
+ const unsigned int unused = max_size - writer->size;
+ const uint8_t byte = event[writer->pos];
+
+ if (byte & 0x80) {
+ enum midi_event_class class;
+ unsigned int expected_size;
+
+ class = midi_event_info(event[0], &expected_size);
+
+ if (class == MIDI_BASIC && expected_size > 1 &&
+ writer->running_status == byte &&
+ writer->running_time_msec == time_msec) {
+ /* Running status: continue with data */
+ continue;
+ }
+
+ if (unused < expected_size + 1)
+ goto flush;
+
+ /* Timestamp before status */
+ PUT(0x80 | (timestamp & 0x7f));
+ writer->running_time_msec = time_msec;
+
+ if (class == MIDI_BASIC && expected_size > 1)
+ writer->running_status = byte;
+ else
+ writer->running_status = 0;
+ } else if (unused == 0) {
+ break;
+ }
+
+ PUT(byte);
+ }
+
+ if (writer->pos < event_size)
+ goto flush;
+
+ writer->pos = 0;
+ return 0;
+
+flush:
+ writer->flush = true;
+ return 1;
+
+#undef PUT
+}
diff --git a/spa/plugins/bluez5/midi-server.c b/spa/plugins/bluez5/midi-server.c
new file mode 100644
index 0000000..a1b2682
--- /dev/null
+++ b/spa/plugins/bluez5/midi-server.c
@@ -0,0 +1,581 @@
+/* Spa Bluez5 midi
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+
+#include "midi.h"
+
+#include "bluez5-interface-gen.h"
+#include "dbus-monitor.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT (&impl->log_topic)
+
+#define MIDI_SERVER_PATH "/midiserver%u"
+#define MIDI_SERVICE_PATH "/midiserver%u/service"
+#define MIDI_CHR_PATH "/midiserver%u/service/chr"
+#define MIDI_DSC_PATH "/midiserver%u/service/chr/dsc"
+
+#define BLE_DEFAULT_MTU 23
+
+struct impl
+{
+ struct spa_bt_midi_server this;
+
+ struct spa_log_topic log_topic;
+ struct spa_log *log;
+
+ const struct spa_bt_midi_server_cb *cb;
+
+ GDBusConnection *conn;
+ struct dbus_monitor monitor;
+ GDBusObjectManagerServer *manager;
+
+ Bluez5GattCharacteristic1 *chr;
+
+ void *user_data;
+
+ uint32_t server_id;
+
+ unsigned int write_acquired:1;
+ unsigned int notify_acquired:1;
+};
+
+struct _MidiServerManagerProxy
+{
+ Bluez5GattManager1Proxy parent_instance;
+
+ GCancellable *register_call;
+ unsigned int registered:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, MIDI_SERVER,
+ MANAGER_PROXY, Bluez5GattManager1Proxy)
+G_DEFINE_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
+#define MIDI_SERVER_TYPE_MANAGER_PROXY (midi_server_manager_proxy_get_type())
+
+
+/*
+ * Characteristic user descriptor: not in BLE MIDI standard, but we
+ * put a device name here in case we have multiple MIDI endpoints.
+ */
+
+static gboolean dsc_handle_read_value(Bluez5GattDescriptor1 *iface, GDBusMethodInvocation *invocation,
+ GVariant *arg_options, gpointer user_data)
+{
+ struct impl *impl = user_data;
+ const char *description = NULL;
+ uint16_t offset = 0;
+ int len;
+
+ g_variant_lookup(arg_options, "offset", "q", &offset);
+
+ if (impl->cb->get_description)
+ description = impl->cb->get_description(impl->user_data);
+ if (!description)
+ description = "";
+
+ len = strlen(description);
+ if (offset > len) {
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.freedesktop.DBus.Error.InvalidArgs",
+ "Invalid arguments");
+ return TRUE;
+ }
+
+ bluez5_gatt_descriptor1_complete_read_value(iface,
+ invocation, description + offset);
+ return TRUE;
+}
+
+static int export_dsc(struct impl *impl)
+{
+ static const char * const flags[] = { "encrypt-read", NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattDescriptor1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_descriptor1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_DSC_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_descriptor1_set_uuid(iface, BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID);
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ bluez5_gatt_descriptor1_set_characteristic(iface, path);
+ bluez5_gatt_descriptor1_set_flags(iface, flags);
+
+ g_signal_connect(iface, "handle-read-value", G_CALLBACK(dsc_handle_read_value), impl);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Descriptor exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * MIDI characteristic
+ */
+
+static gboolean chr_handle_read_value(Bluez5GattCharacteristic1 *iface,
+ GDBusMethodInvocation *invocation, GVariant *arg_options,
+ gpointer user_data)
+{
+ /* BLE MIDI-1.0: returns empty value */
+ bluez5_gatt_characteristic1_complete_read_value(iface, invocation, "");
+ return TRUE;
+}
+
+static void chr_change_acquired(struct impl *impl, bool write, bool enabled)
+{
+ if (write) {
+ impl->write_acquired = enabled;
+ bluez5_gatt_characteristic1_set_write_acquired(impl->chr, enabled);
+ } else {
+ impl->notify_acquired = enabled;
+ bluez5_gatt_characteristic1_set_notify_acquired(impl->chr, enabled);
+ }
+}
+
+static int create_socketpair(int fds[2])
+{
+ if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, fds) < 0)
+ return -errno;
+ return 0;
+}
+
+static gboolean chr_handle_acquire(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *dummy, GVariant *arg_options,
+ bool write, gpointer user_data)
+{
+ struct impl *impl = user_data;
+ const char *err_msg = "Failed";
+ uint16_t mtu = BLE_DEFAULT_MTU;
+ gint fds[2] = {-1, -1};
+ int res;
+ GUnixFDList *fd_list = NULL;
+ GVariant *fd_handle = NULL;
+ GError *err = NULL;
+
+ if ((write && (impl->cb->acquire_write == NULL)) ||
+ (!write && (impl->cb->acquire_notify == NULL))) {
+ err_msg = "Not supported";
+ goto fail;
+ }
+ if ((write && impl->write_acquired) ||
+ (!write && impl->notify_acquired)) {
+ err_msg = "Already acquired";
+ goto fail;
+ }
+
+ g_variant_lookup(arg_options, "mtu", "q", &mtu);
+
+ if (create_socketpair(fds) < 0) {
+ err_msg = "Socketpair creation failed";
+ goto fail;
+ }
+
+ if (write)
+ res = impl->cb->acquire_write(impl->user_data, fds[0], mtu);
+ else
+ res = impl->cb->acquire_notify(impl->user_data, fds[0], mtu);
+ if (res < 0) {
+ err_msg = "Acquiring failed";
+ goto fail;
+ }
+ fds[0] = -1;
+
+ fd_handle = g_variant_new_handle(0);
+ fd_list = g_unix_fd_list_new_from_array(&fds[1], 1);
+ fds[1] = -1;
+
+ chr_change_acquired(impl, write, true);
+
+ if (write) {
+ bluez5_gatt_characteristic1_complete_acquire_write(
+ object, invocation, fd_list, fd_handle, mtu);
+ } else {
+ bluez5_gatt_characteristic1_complete_acquire_notify(
+ object, invocation, fd_list, fd_handle, mtu);
+ }
+
+ g_clear_object(&fd_list);
+ return TRUE;
+
+fail:
+ if (fds[0] >= 0)
+ close(fds[0]);
+ if (fds[1] >= 0)
+ close(fds[1]);
+
+ if (err)
+ g_error_free(err);
+ g_clear_pointer(&fd_handle, g_variant_unref);
+ g_clear_object(&fd_list);
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.freedesktop.DBus.Error.Failed", err_msg);
+ return TRUE;
+
+}
+
+static gboolean chr_handle_acquire_write(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *fd_list, GVariant *arg_options,
+ gpointer user_data)
+{
+ return chr_handle_acquire(object, invocation, fd_list, arg_options, true, user_data);
+}
+
+static gboolean chr_handle_acquire_notify(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *fd_list, GVariant *arg_options,
+ gpointer user_data)
+{
+ return chr_handle_acquire(object, invocation, fd_list, arg_options, false, user_data);
+}
+
+static int export_chr(struct impl *impl)
+{
+ static const char * const flags[] = { "encrypt-read", "write-without-response",
+ "encrypt-write", "encrypt-notify", NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattCharacteristic1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_characteristic1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_characteristic1_set_uuid(iface, BT_MIDI_CHR_UUID);
+ spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id);
+ bluez5_gatt_characteristic1_set_service(iface, path);
+ bluez5_gatt_characteristic1_set_write_acquired(iface, FALSE);
+ bluez5_gatt_characteristic1_set_notify_acquired(iface, FALSE);
+ bluez5_gatt_characteristic1_set_flags(iface, flags);
+
+ g_signal_connect(iface, "handle-read-value", G_CALLBACK(chr_handle_read_value), impl);
+ g_signal_connect(iface, "handle-acquire-write", G_CALLBACK(chr_handle_acquire_write), impl);
+ g_signal_connect(iface, "handle-acquire-notify", G_CALLBACK(chr_handle_acquire_notify), impl);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ impl->chr = g_object_ref(iface);
+
+ spa_log_debug(impl->log, "MIDI GATT Characteristic exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * MIDI service
+ */
+
+static int export_service(struct impl *impl)
+{
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattService1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_service1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_service1_set_uuid(iface, BT_MIDI_SERVICE_UUID);
+ bluez5_gatt_service1_set_primary(iface, TRUE);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Service exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * Registration on all GattManagers
+ */
+
+static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(source_object);
+ struct impl *impl = user_data;
+ GError *err = NULL;
+
+ bluez5_gatt_manager1_call_register_application_finish(
+ BLUEZ5_GATT_MANAGER1(source_object), res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ manager->registered = true;
+
+done:
+ g_clear_object(&manager->register_call);
+}
+
+static int manager_register_application(struct impl *impl, MidiServerManagerProxy *manager)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ if (manager->registered)
+ return 0;
+ if (manager->register_call)
+ return -EBUSY;
+
+ spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));
+
+ manager->register_call = g_cancellable_new();
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+ bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ options,
+ manager->register_call,
+ manager_register_application_reply,
+ impl);
+
+ return 0;
+}
+
+static void midi_server_manager_proxy_init(MidiServerManagerProxy *manager)
+{
+}
+
+static void midi_server_manager_proxy_finalize(GObject *object)
+{
+ MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(object);
+
+ g_cancellable_cancel(manager->register_call);
+ g_clear_object(&manager->register_call);
+}
+
+static void midi_server_manager_proxy_class_init(MidiServerManagerProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_server_manager_proxy_finalize;
+}
+
+static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ manager_register_application(impl, MIDI_SERVER_MANAGER_PROXY(iface));
+}
+
+static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_server_manager_proxy_finalize(G_OBJECT(iface));
+}
+
+static void on_name_owner_change(struct dbus_monitor *monitor)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ /*
+ * BlueZ disappeared/appeared. It does not appear to close the sockets
+ * it quits, so we should force the chr release now.
+ */
+ if (impl->cb->release)
+ impl->cb->release(impl->user_data);
+ chr_change_acquired(impl, true, false);
+ chr_change_acquired(impl, false, false);
+}
+
+static void monitor_start(struct impl *impl)
+{
+ struct dbus_monitor_proxy_type proxy_types[] = {
+ { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_SERVER_TYPE_MANAGER_PROXY, manager_update, manager_clear },
+ { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
+ { NULL, G_TYPE_INVALID, NULL, NULL },
+ };
+
+ SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);
+
+ dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
+ impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types,
+ on_name_owner_change);
+}
+
+
+/*
+ * Object registration
+ */
+
+static int export_objects(struct impl *impl)
+{
+ int res = 0;
+ char path[128];
+
+ spa_scnprintf(path, sizeof(path), MIDI_SERVER_PATH, impl->server_id);
+ impl->manager = g_dbus_object_manager_server_new(path);
+ if (!impl->manager){
+ spa_log_error(impl->log, "Creating GDBus object manager failed");
+ goto fail;
+ }
+
+ if ((res = export_service(impl)) < 0)
+ goto fail;
+
+ if ((res = export_chr(impl)) < 0)
+ goto fail;
+
+ if ((res = export_dsc(impl)) < 0)
+ goto fail;
+
+ g_dbus_object_manager_server_set_connection(impl->manager, impl->conn);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+
+ spa_log_error(impl->log, "Failed to register BLE MIDI services in DBus: %s",
+ spa_strerror(res));
+
+ g_clear_object(&impl->manager);
+
+ return res;
+}
+
+struct spa_bt_midi_server *spa_bt_midi_server_new(const struct spa_bt_midi_server_cb *cb,
+ GDBusConnection *conn, struct spa_log *log, void *user_data)
+{
+ static unsigned int server_id = 0;
+ struct impl *impl;
+ char path[128];
+ int res = 0;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto fail;
+
+ impl->server_id = server_id++;
+ impl->user_data = user_data;
+ impl->cb = cb;
+ impl->log = log;
+ impl->log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.server");
+ impl->conn = conn;
+ spa_log_topic_init(impl->log, &impl->log_topic);
+
+ if ((res = export_objects(impl)) < 0)
+ goto fail;
+
+ monitor_start(impl);
+
+ g_object_ref(impl->conn);
+
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ impl->this.chr_path = strdup(path);
+
+ return &impl->this;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ free(impl);
+ errno = res;
+ return NULL;
+}
+
+void spa_bt_midi_server_destroy(struct spa_bt_midi_server *server)
+{
+ struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this);
+
+ free((void *)impl->this.chr_path);
+ g_clear_object(&impl->chr);
+ dbus_monitor_clear(&impl->monitor);
+ g_clear_object(&impl->manager);
+ g_clear_object(&impl->conn);
+
+ free(impl);
+}
+
+void spa_bt_midi_server_released(struct spa_bt_midi_server *server, bool write)
+{
+ struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this);
+
+ chr_change_acquired(impl, write, false);
+}
diff --git a/spa/plugins/bluez5/midi.h b/spa/plugins/bluez5/midi.h
new file mode 100644
index 0000000..fbf2702
--- /dev/null
+++ b/spa/plugins/bluez5/midi.h
@@ -0,0 +1,142 @@
+/* Spa V4l2 dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BT_MIDI_H_
+#define SPA_BT_MIDI_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1"
+#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1"
+#define BLUEZ_GATT_MANAGER_INTERFACE BLUEZ_SERVICE ".GattManager1"
+#define BLUEZ_GATT_PROFILE_INTERFACE BLUEZ_SERVICE ".GattProfile1"
+#define BLUEZ_GATT_SERVICE_INTERFACE BLUEZ_SERVICE ".GattService1"
+#define BLUEZ_GATT_CHR_INTERFACE BLUEZ_SERVICE ".GattCharacteristic1"
+#define BLUEZ_GATT_DSC_INTERFACE BLUEZ_SERVICE ".GattDescriptor1"
+
+#define BT_MIDI_SERVICE_UUID "03b80e5a-ede8-4b33-a751-6ce34ec4c700"
+#define BT_MIDI_CHR_UUID "7772e5db-3868-4112-a1a9-f2669d106bf3"
+#define BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID "00002901-0000-1000-8000-00805f9b34fb"
+
+#define MIDI_BUF_SIZE 8192
+#define MIDI_MAX_MTU 8192
+
+#define MIDI_CLOCK_PERIOD_MSEC 0x2000
+#define MIDI_CLOCK_PERIOD_NSEC (MIDI_CLOCK_PERIOD_MSEC * SPA_NSEC_PER_MSEC)
+
+struct spa_bt_midi_server
+{
+ const char *chr_path;
+};
+
+struct spa_bt_midi_parser {
+ unsigned int size;
+ unsigned int sysex:1;
+ uint8_t buf[MIDI_BUF_SIZE];
+};
+
+struct spa_bt_midi_writer {
+ unsigned int size;
+ unsigned int mtu;
+ unsigned int pos;
+ uint8_t running_status;
+ uint64_t running_time_msec;
+ unsigned int flush:1;
+ uint8_t buf[MIDI_MAX_MTU];
+};
+
+struct spa_bt_midi_server_cb
+{
+ int (*acquire_notify)(void *user_data, int fd, uint16_t mtu);
+ int (*acquire_write)(void *user_data, int fd, uint16_t mtu);
+ int (*release)(void *user_data);
+ const char *(*get_description)(void *user_data);
+};
+
+static inline void spa_bt_midi_parser_init(struct spa_bt_midi_parser *parser)
+{
+ parser->size = 0;
+ parser->sysex = 0;
+}
+
+static inline void spa_bt_midi_parser_dup(struct spa_bt_midi_parser *src, struct spa_bt_midi_parser *dst, bool only_time)
+{
+ dst->size = src->size;
+ dst->sysex = src->sysex;
+ if (!only_time)
+ memcpy(dst->buf, src->buf, src->size);
+}
+
+/**
+ * Parse a single BLE MIDI data packet to normalized MIDI events.
+ */
+int spa_bt_midi_parser_parse(struct spa_bt_midi_parser *parser,
+ const uint8_t *src, size_t src_size,
+ bool only_time,
+ void (*event)(void *user_data, uint16_t time, uint8_t *event, size_t event_size),
+ void *user_data);
+
+static inline void spa_bt_midi_writer_init(struct spa_bt_midi_writer *writer, unsigned int mtu)
+{
+ writer->size = 0;
+ writer->mtu = SPA_MIN(mtu, (unsigned int)MIDI_MAX_MTU);
+ writer->pos = 0;
+ writer->running_status = 0;
+ writer->running_time_msec = 0;
+ writer->flush = 0;
+}
+
+/**
+ * Add a new event to midi writer buffer.
+ *
+ * spa_bt_midi_writer_init(&writer, mtu);
+ * for (time, event, size) in midi events {
+ * do {
+ * res = spa_bt_midi_writer_write(&writer, time, event, size);
+ * if (res < 0) {
+ * fail with error
+ * } else if (res) {
+ * send_packet(writer->buf, writer->size);
+ * }
+ * } while (res);
+ * }
+ * if (writer.size > 0)
+ * send_packet(writer->buf, writer->size);
+ */
+int spa_bt_midi_writer_write(struct spa_bt_midi_writer *writer,
+ uint64_t time, const uint8_t *event, size_t event_size);
+
+struct spa_bt_midi_server *spa_bt_midi_server_new(const struct spa_bt_midi_server_cb *cb,
+ GDBusConnection *conn, struct spa_log *log, void *user_data);
+void spa_bt_midi_server_released(struct spa_bt_midi_server *server, bool write);
+void spa_bt_midi_server_destroy(struct spa_bt_midi_server *server);
+
+#endif
diff --git a/spa/plugins/bluez5/modemmanager.c b/spa/plugins/bluez5/modemmanager.c
new file mode 100644
index 0000000..d9df95a
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.c
@@ -0,0 +1,1249 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <spa/utils/string.h>
+
+#include <ModemManager.h>
+
+#include "modemmanager.h"
+
+#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+struct modem {
+ char *path;
+ bool network_has_service;
+ unsigned int signal_strength;
+};
+
+struct impl {
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ DBusConnection *conn;
+
+ char *allowed_modem_device;
+ bool filters_added;
+ DBusPendingCall *pending;
+ DBusPendingCall *voice_pending;
+
+ const struct mm_ops *ops;
+ void *user_data;
+
+ struct modem modem;
+ struct spa_list call_list;
+};
+
+struct dbus_cmd_data {
+ struct impl *this;
+ struct call *call;
+ void *user_data;
+};
+
+static bool mm_dbus_connection_send_with_reply(struct impl *this, DBusMessage *m, DBusPendingCall **pending_return,
+ DBusPendingCallNotifyFunction function, void *user_data)
+{
+ dbus_bool_t dbus_ret;
+
+ spa_assert(*pending_return == NULL);
+
+ dbus_ret = dbus_connection_send_with_reply(this->conn, m, pending_return, -1);
+ if (!dbus_ret || *pending_return == NULL) {
+ spa_log_debug(this->log, "dbus call failure");
+ return false;
+ }
+
+ dbus_ret = dbus_pending_call_set_notify(*pending_return, function, user_data, NULL);
+ if (!dbus_ret) {
+ spa_log_debug(this->log, "dbus set notify failure");
+ dbus_pending_call_cancel(*pending_return);
+ dbus_pending_call_unref(*pending_return);
+ *pending_return = NULL;
+ return false;
+ }
+
+ return true;
+}
+
+static int mm_state_to_clcc(struct impl *this, MMCallState state)
+{
+ switch (state) {
+ case MM_CALL_STATE_DIALING:
+ return CLCC_DIALING;
+ case MM_CALL_STATE_RINGING_OUT:
+ return CLCC_ALERTING;
+ case MM_CALL_STATE_RINGING_IN:
+ return CLCC_INCOMING;
+ case MM_CALL_STATE_ACTIVE:
+ return CLCC_ACTIVE;
+ case MM_CALL_STATE_HELD:
+ return CLCC_HELD;
+ case MM_CALL_STATE_WAITING:
+ return CLCC_WAITING;
+ case MM_CALL_STATE_TERMINATED:
+ case MM_CALL_STATE_UNKNOWN:
+ default:
+ return -1;
+ }
+}
+
+static void mm_call_state_changed(struct impl *this)
+{
+ struct call *call;
+ bool call_indicator = false;
+ enum call_setup call_setup_indicator = CIND_CALLSETUP_NONE;
+
+ spa_list_for_each(call, &this->call_list, link) {
+ call_indicator |= (call->state == CLCC_ACTIVE);
+
+ if (call->state == CLCC_INCOMING && call_setup_indicator < CIND_CALLSETUP_INCOMING)
+ call_setup_indicator = CIND_CALLSETUP_INCOMING;
+ else if (call->state == CLCC_DIALING && call_setup_indicator < CIND_CALLSETUP_DIALING)
+ call_setup_indicator = CIND_CALLSETUP_DIALING;
+ else if (call->state == CLCC_ALERTING && call_setup_indicator < CIND_CALLSETUP_ALERTING)
+ call_setup_indicator = CIND_CALLSETUP_ALERTING;
+ }
+
+ if (this->ops->set_call_active)
+ this->ops->set_call_active(call_indicator, this->user_data);
+
+ if (this->ops->set_call_setup)
+ this->ops->set_call_setup(call_setup_indicator, this->user_data);
+}
+
+static void mm_get_call_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct call *call = user_data;
+ struct impl *this = call->this;
+ DBusMessage *r;
+ DBusMessageIter arg_i, element_i;
+ MMCallDirection direction;
+ MMCallState state;
+
+ spa_assert(call->pending == pending);
+ dbus_pending_call_unref(pending);
+ call->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus Call not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &arg_i) || !spa_streq(dbus_message_get_signature(r), "a{sv}")) {
+ spa_log_error(this->log, "Invalid arguments in GetAll() reply");
+ goto finish;
+ }
+
+ spa_log_debug(this->log, "Call path: %s", call->path);
+
+ dbus_message_iter_recurse(&arg_i, &element_i);
+ while (dbus_message_iter_get_arg_type(&element_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_CALL_PROPERTY_DIRECTION)) {
+ dbus_message_iter_get_basic(&value_i, &direction);
+ spa_log_debug(this->log, "Call direction: %u", direction);
+ call->direction = (direction == MM_CALL_DIRECTION_INCOMING) ? CALL_INCOMING : CALL_OUTGOING;
+ } else if (spa_streq(key, MM_CALL_PROPERTY_NUMBER)) {
+ char *number;
+
+ dbus_message_iter_get_basic(&value_i, &number);
+ spa_log_debug(this->log, "Call number: %s", number);
+ if (call->number)
+ free(call->number);
+ call->number = strdup(number);
+ } else if (spa_streq(key, MM_CALL_PROPERTY_STATE)) {
+ int clcc_state;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Call state: %u", state);
+ clcc_state = mm_state_to_clcc(this, state);
+ if (clcc_state < 0) {
+ spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+ } else {
+ call->state = clcc_state;
+ mm_call_state_changed(this);
+ }
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static DBusHandlerResult mm_parse_voice_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i, element_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) {
+ spa_log_debug(this->log, "Voice properties");
+ dbus_message_iter_recurse(&value_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) {
+ const char *call_object;
+
+ dbus_message_iter_get_basic(&element_i, &call_object);
+ spa_log_debug(this->log, " Call: %s", call_object);
+
+ dbus_message_iter_next(&element_i);
+ }
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem3gpp_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) {
+ char *operator_name;
+
+ dbus_message_iter_get_basic(&value_i, &operator_name);
+ spa_log_debug(this->log, "Network operator code: %s", operator_name);
+ if (this->ops->set_modem_operator_name)
+ this->ops->set_modem_operator_name(operator_name, this->user_data);
+ } else if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) {
+ MMModem3gppRegistrationState state;
+ bool is_roaming;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Registration state: %d", state);
+
+ if (state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||
+ state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED ||
+ state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY)
+ is_roaming = true;
+ else
+ is_roaming = false;
+
+ if (this->ops->set_modem_roaming)
+ this->ops->set_modem_roaming(is_roaming, this->user_data);
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if(spa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) {
+ char *imei;
+
+ dbus_message_iter_get_basic(&value_i, &imei);
+ spa_log_debug(this->log, "Modem IMEI: %s", imei);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) {
+ char *manufacturer;
+
+ dbus_message_iter_get_basic(&value_i, &manufacturer);
+ spa_log_debug(this->log, "Modem manufacturer: %s", manufacturer);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_MODEL)) {
+ char *model;
+
+ dbus_message_iter_get_basic(&value_i, &model);
+ spa_log_debug(this->log, "Modem model: %s", model);
+ } else if (spa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) {
+ char *number;
+ DBusMessageIter array_i;
+
+ dbus_message_iter_recurse(&value_i, &array_i);
+ if (dbus_message_iter_get_arg_type(&array_i) == DBUS_TYPE_STRING) {
+ dbus_message_iter_get_basic(&array_i, &number);
+ spa_log_debug(this->log, "Modem own number: %s", number);
+ if (this->ops->set_modem_own_number)
+ this->ops->set_modem_own_number(number, this->user_data);
+ }
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_REVISION)) {
+ char *revision;
+
+ dbus_message_iter_get_basic(&value_i, &revision);
+ spa_log_debug(this->log, "Modem revision: %s", revision);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) {
+ unsigned int percentage, signal_strength;
+ DBusMessageIter struct_i;
+
+ dbus_message_iter_recurse(&value_i, &struct_i);
+ if (dbus_message_iter_get_arg_type(&struct_i) == DBUS_TYPE_UINT32) {
+ dbus_message_iter_get_basic(&struct_i, &percentage);
+ signal_strength = (unsigned int) round(percentage / 20.0);
+ spa_log_debug(this->log, "Network signal strength: %d (%d)", percentage, signal_strength);
+ if(this->ops->set_modem_signal_strength)
+ this->ops->set_modem_signal_strength(signal_strength, this->user_data);
+ }
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_STATE)) {
+ MMModemState state;
+ bool has_service;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Network state: %d", state);
+
+ has_service = (state >= MM_MODEM_STATE_REGISTERED);
+ if (this->ops->set_modem_service)
+ this->ops->set_modem_service(has_service, this->user_data);
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_interfaces(struct impl *this, DBusMessageIter *dict_i)
+{
+ DBusMessageIter element_i, props_i;
+ const char *path;
+
+ spa_assert(this);
+ spa_assert(dict_i);
+
+ dbus_message_iter_get_basic(dict_i, &path);
+ dbus_message_iter_next(dict_i);
+ dbus_message_iter_recurse(dict_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter iface_i;
+ const char *interface;
+
+ dbus_message_iter_recurse(&element_i, &iface_i);
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Found Modem interface %s, path %s", interface, path);
+ if (this->modem.path == NULL) {
+ if (this->allowed_modem_device) {
+ DBusMessageIter i;
+
+ dbus_message_iter_recurse(&iface_i, &i);
+ while (dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter key_i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&i, &key_i);
+
+ dbus_message_iter_get_basic(&key_i, &key);
+ dbus_message_iter_next(&key_i);
+ dbus_message_iter_recurse(&key_i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_PROPERTY_DEVICE)) {
+ char *device;
+
+ dbus_message_iter_get_basic(&value_i, &device);
+ if (!spa_streq(this->allowed_modem_device, device)) {
+ spa_log_debug(this->log, "Modem not allowed: %s", device);
+ goto next;
+ }
+ }
+ dbus_message_iter_next(&i);
+ }
+ }
+ this->modem.path = strdup(path);
+ } else if (!spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "A modem is already registered");
+ goto next;
+ }
+ mm_parse_modem_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ if (spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "Found Modem3GPP interface %s, path %s", interface, path);
+ mm_parse_modem3gpp_properties(this, &props_i);
+ }
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ if (spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "Found Voice interface %s, path %s", interface, path);
+ mm_parse_voice_properties(this, &props_i);
+ }
+ }
+
+next:
+ dbus_message_iter_next(&element_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void mm_get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i;
+
+ spa_assert(this->pending == pending);
+ dbus_pending_call_unref(pending);
+ this->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "Failed to get a list of endpoints from ModemManager: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) {
+ spa_log_error(this->log, "Invalid arguments in GetManagedObjects() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter dict_i;
+
+ dbus_message_iter_recurse(&array_i, &dict_i);
+ mm_parse_interfaces(this, &dict_i);
+ dbus_message_iter_next(&array_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void call_free(struct call *call) {
+ spa_list_remove(&call->link);
+
+ if (call->pending != NULL) {
+ dbus_pending_call_cancel(call->pending);
+ dbus_pending_call_unref(call->pending);
+ }
+
+ if (call->number)
+ free(call->number);
+ if (call->path)
+ free(call->path);
+ free(call);
+}
+
+static void mm_clean_voice(struct impl *this)
+{
+ struct call *call;
+
+ spa_list_consume(call, &this->call_list, link)
+ call_free(call);
+
+ if (this->voice_pending != NULL) {
+ dbus_pending_call_cancel(this->voice_pending);
+ dbus_pending_call_unref(this->voice_pending);
+ }
+
+ if (this->ops->set_call_setup)
+ this->ops->set_call_setup(CIND_CALLSETUP_NONE, this->user_data);
+ if (this->ops->set_call_active)
+ this->ops->set_call_active(false, this->user_data);
+}
+
+static void mm_clean_modem3gpp(struct impl *this)
+{
+ if (this->ops->set_modem_operator_name)
+ this->ops->set_modem_operator_name(NULL, this->user_data);
+ if (this->ops->set_modem_roaming)
+ this->ops->set_modem_roaming(false, this->user_data);
+}
+
+static void mm_clean_modem(struct impl *this)
+{
+ if (this->modem.path) {
+ free(this->modem.path);
+ this->modem.path = NULL;
+ }
+ if(this->ops->set_modem_signal_strength)
+ this->ops->set_modem_signal_strength(0, this->user_data);
+ if (this->ops->set_modem_service)
+ this->ops->set_modem_service(false, this->user_data);
+ this->modem.network_has_service = false;
+}
+
+static DBusHandlerResult mm_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto finish;
+ }
+
+ if (spa_streq(name, MM_DBUS_SERVICE)) {
+ if (old_owner && *old_owner) {
+ spa_log_debug(this->log, "ModemManager daemon disappeared (%s)", old_owner);
+ mm_clean_voice(this);
+ mm_clean_modem3gpp(this);
+ mm_clean_modem(this);
+ }
+
+ if (new_owner && *new_owner)
+ spa_log_debug(this->log, "ModemManager daemon appeared (%s)", new_owner);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_ADDED)) {
+ DBusMessageIter arg_i;
+
+ spa_log_warn(this->log, "sender: %s", dbus_message_get_sender(m));
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) {
+ spa_log_error(this->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ mm_parse_interfaces(this, &arg_i);
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_REMOVED)) {
+ const char *path;
+ DBusMessageIter arg_i, element_i;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oas")) {
+ spa_log_error(this->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &path);
+ if (!spa_streq(this->modem.path, path))
+ goto finish;
+
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_recurse(&arg_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) {
+ const char *iface;
+
+ dbus_message_iter_get_basic(&element_i, &iface);
+
+ spa_log_debug(this->log, "Interface removed %s", path);
+ if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Modem interface %s removed, path %s", iface, path);
+ mm_clean_modem(this);
+ } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ spa_log_debug(this->log, "Modem3GPP interface %s removed, path %s", iface, path);
+ mm_clean_modem3gpp(this);
+ } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ spa_log_debug(this->log, "Voice interface %s removed, path %s", iface, path);
+ mm_clean_voice(this);
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i, props_i;
+ const char *interface;
+
+ path = dbus_message_get_path(m);
+ if (!spa_streq(this->modem.path, path))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_modem_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_modem3gpp_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_voice_properties(this, &props_i);
+ }
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) {
+ DBusMessageIter iface_i;
+ const char *path;
+ struct call *call_object;
+ const char *mm_call_interface = MM_DBUS_INTERFACE_CALL;
+
+ if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLADDED);
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &path);
+ spa_log_debug(this->log, "New call: %s", path);
+
+ call_object = calloc(1, sizeof(struct call));
+ if (call_object == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ call_object->this = this;
+ call_object->path = strdup(path);
+ spa_list_append(&this->call_list, &call_object->link);
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll");
+ if (m == NULL)
+ goto finish;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID);
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_properties_reply, call_object)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ goto finish;
+ }
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) {
+ const char *path;
+ DBusMessageIter iface_i;
+ struct call *call, *call_tmp;
+
+ if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLDELETED);
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &path);
+ spa_log_debug(this->log, "Call ended: %s", path);
+
+ spa_list_for_each_safe(call, call_tmp, &this->call_list, link) {
+ if (spa_streq(call->path, path))
+ call_free(call);
+ }
+ mm_call_state_changed(this);
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i;
+ MMCallState old, new;
+ MMCallStateReason reason;
+ struct call *call = NULL, *call_tmp;
+ int clcc_state;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "iiu")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_CALL_SIGNAL_STATECHANGED);
+ goto finish;
+ }
+
+ path = dbus_message_get_path(m);
+
+ dbus_message_iter_get_basic(&iface_i, &old);
+ dbus_message_iter_next(&iface_i);
+ dbus_message_iter_get_basic(&iface_i, &new);
+ dbus_message_iter_next(&iface_i);
+ dbus_message_iter_get_basic(&iface_i, &reason);
+
+ spa_log_debug(this->log, "Call state %s changed to %d (old = %d, reason = %u)", path, new, old, reason);
+
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (spa_streq(call_tmp->path, path)) {
+ call = call_tmp;
+ break;
+ }
+ }
+
+ if (call == NULL) {
+ spa_log_warn(this->log, "No call reference for %s", path);
+ goto finish;
+ }
+
+ clcc_state = mm_state_to_clcc(this, new);
+ if (clcc_state < 0) {
+ spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+ } else {
+ call->state = clcc_state;
+ mm_call_state_changed(this);
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" MM_DBUS_SERVICE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_ADDED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_REMOVED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", &err);
+
+ this->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+ "org.freedesktop.DBus", "NameHasOwner");
+ if (m == NULL)
+ return false;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(this->log, "NameHasOwner failed for %s", service);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_BOOLEAN, &success,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+
+ return success;
+}
+
+bool mm_is_available(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+
+ if (this == NULL)
+ return false;
+
+ return this->modem.path != NULL;
+}
+
+unsigned int mm_supported_features()
+{
+ return SPA_BT_HFP_AG_FEATURE_REJECT_CALL | SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS;
+}
+
+static void mm_get_call_simple_reply(DBusPendingCall *pending, void *data)
+{
+ struct dbus_cmd_data *dbus_cmd_data = data;
+ struct impl *this = dbus_cmd_data->this;
+ struct call *call = dbus_cmd_data->call;
+ void *user_data = dbus_cmd_data->user_data;
+ DBusMessage *r;
+
+ free(data);
+
+ spa_assert(call->pending == pending);
+ dbus_pending_call_unref(pending);
+ call->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus method not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ this->ops->send_cmd_result(true, 0, user_data);
+ return;
+
+finish:
+ this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+static void mm_get_call_create_reply(DBusPendingCall *pending, void *data)
+{
+ struct dbus_cmd_data *dbus_cmd_data = data;
+ struct impl *this = dbus_cmd_data->this;
+ void *user_data = dbus_cmd_data->user_data;
+ DBusMessage *r;
+
+ free(data);
+
+ spa_assert(this->voice_pending == pending);
+ dbus_pending_call_unref(pending);
+ this->voice_pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus method not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ this->ops->send_cmd_result(true, 0, user_data);
+ return;
+
+finish:
+ this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No ringing in call");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_ACTIVE) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_DIALING ||
+ call_tmp->state == CLCC_ALERTING ||
+ call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No call to reject or hang up");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
+ DBusMessageIter dict_entry_it, variant_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_append_basic(&variant_it, variant_type_int, variant);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static inline bool is_valid_dial_string_char(char c)
+{
+ return ('0' <= c && c <= '9')
+ || ('A' <= c && c <= 'C')
+ || c == '*'
+ || c == '#'
+ || c == '+';
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+ DBusMessageIter iter, dict;
+
+ for (size_t i = 0; number[i]; i++) {
+ if (!is_valid_dial_string_char(number[i])) {
+ spa_log_warn(this->log, "Call creation canceled, invalid character found in dial string: %c", number[i]);
+ if (error)
+ *error = CMEE_INVALID_CHARACTERS_DIAL_STRING;
+ return false;
+ }
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, this->modem.path, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+ append_basic_variant_dict_entry(&dict, "number", DBUS_TYPE_STRING, "s", &number);
+ dbus_message_iter_close_container(&iter, &dict);
+ if (!mm_dbus_connection_send_with_reply(this, m, &this->voice_pending, mm_get_call_create_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_ACTIVE) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No active call");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ /* Allowed dtmf characters: 0-9, *, #, A-D */
+ if (!((dtmf[0] >= '0' && dtmf[0] <= '9')
+ || (dtmf[0] == '*')
+ || (dtmf[0] == '#')
+ || (dtmf[0] >= 'A' && dtmf[0] <= 'D'))) {
+ spa_log_debug(this->log, "Invalid DTMF character: %s", dtmf);
+ if (error)
+ *error = CMEE_INVALID_CHARACTERS_TEXT_STRING;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &dtmf, DBUS_TYPE_INVALID);
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No ringing in call");
+ return NULL;
+ }
+
+ return call_object->number;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+
+ return &this->call_list;
+}
+
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data)
+{
+ struct impl *this;
+ const char *modem_device_str = NULL;
+ bool modem_device_found = false;
+
+ spa_assert(log);
+ spa_assert(dbus_connection);
+
+ if (info) {
+ if ((modem_device_str = spa_dict_lookup(info, "bluez5.hfphsp-backend-native-modem")) != NULL) {
+ if (!spa_streq(modem_device_str, "none"))
+ modem_device_found = true;
+ }
+ }
+ if (!modem_device_found) {
+ spa_log_info(log, "No modem allowed, doesn't link to ModemManager");
+ return NULL;
+ }
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+ this->conn = dbus_connection;
+ this->ops = ops;
+ this->user_data = user_data;
+ if (modem_device_str && !spa_streq(modem_device_str, "any"))
+ this->allowed_modem_device = strdup(modem_device_str);
+ spa_list_init(&this->call_list);
+
+ if (add_filters(this) < 0) {
+ goto fail;
+ }
+
+ if (is_dbus_service_available(this, MM_DBUS_SERVICE)) {
+ DBusMessage *m;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, "/org/freedesktop/ModemManager1",
+ DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects");
+ if (m == NULL)
+ goto fail;
+
+ if (!mm_dbus_connection_send_with_reply(this, m, &this->pending, mm_get_managed_objects_reply, this)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ goto fail;
+ }
+ }
+
+ return this;
+
+fail:
+ free(this);
+ return NULL;
+}
+
+void mm_unregister(void *data)
+{
+ struct impl *this = data;
+
+ if (this->pending != NULL) {
+ dbus_pending_call_cancel(this->pending);
+ dbus_pending_call_unref(this->pending);
+ }
+
+ mm_clean_voice(this);
+ mm_clean_modem3gpp(this);
+ mm_clean_modem(this);
+
+ if (this->filters_added) {
+ dbus_connection_remove_filter(this->conn, mm_filter_cb, this);
+ this->filters_added = false;
+ }
+
+ if (this->allowed_modem_device)
+ free(this->allowed_modem_device);
+
+ free(this);
+}
diff --git a/spa/plugins/bluez5/modemmanager.h b/spa/plugins/bluez5/modemmanager.h
new file mode 100644
index 0000000..a239b2a
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.h
@@ -0,0 +1,161 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_MODEMMANAGER_H_
+#define SPA_BLUEZ5_MODEMMANAGER_H_
+
+#include <spa/utils/list.h>
+
+#include "defs.h"
+
+enum cmee_error {
+ CMEE_AG_FAILURE = 0,
+ CMEE_NO_CONNECTION_TO_PHONE = 1,
+ CMEE_OPERATION_NOT_ALLOWED = 3,
+ CMEE_OPERATION_NOT_SUPPORTED = 4,
+ CMEE_INVALID_CHARACTERS_TEXT_STRING = 25,
+ CMEE_INVALID_CHARACTERS_DIAL_STRING = 27,
+ CMEE_NO_NETWORK_SERVICE = 30
+};
+
+enum call_setup {
+ CIND_CALLSETUP_NONE = 0,
+ CIND_CALLSETUP_INCOMING,
+ CIND_CALLSETUP_DIALING,
+ CIND_CALLSETUP_ALERTING
+};
+
+enum call_direction {
+ CALL_OUTGOING,
+ CALL_INCOMING
+};
+
+enum call_state {
+ CLCC_ACTIVE,
+ CLCC_HELD,
+ CLCC_DIALING,
+ CLCC_ALERTING,
+ CLCC_INCOMING,
+ CLCC_WAITING,
+ CLCC_RESPONSE_AND_HOLD
+};
+
+struct call {
+ struct spa_list link;
+ unsigned int index;
+ struct impl *this;
+ DBusPendingCall *pending;
+
+ char *path;
+ char *number;
+ bool call_indicator;
+ enum call_direction direction;
+ enum call_state state;
+ bool multiparty;
+};
+
+struct mm_ops {
+ void (*send_cmd_result)(bool success, enum cmee_error error, void *user_data);
+ void (*set_modem_service)(bool available, void *user_data);
+ void (*set_modem_signal_strength)(unsigned int strength, void *user_data);
+ void (*set_modem_operator_name)(const char *name, void *user_data);
+ void (*set_modem_own_number)(const char *number, void *user_data);
+ void (*set_modem_roaming)(bool is_roaming, void *user_data);
+ void (*set_call_active)(bool active, void *user_data);
+ void (*set_call_setup)(enum call_setup value, void *user_data);
+};
+
+#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE_MM
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data);
+void mm_unregister(void *data);
+bool mm_is_available(void *modemmanager);
+unsigned int mm_supported_features();
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error);
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error);
+const char *mm_get_incoming_call_number(void *modemmanager);
+struct spa_list *mm_get_calls(void *modemmanager);
+#else
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data)
+{
+ return NULL;
+}
+
+void mm_unregister(void *data)
+{
+}
+
+bool mm_is_available(void *modemmanager)
+{
+ return false;
+}
+
+unsigned int mm_supported_features(void)
+{
+ return 0;
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+ return NULL;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+ return NULL;
+}
+#endif
+
+#endif
diff --git a/spa/plugins/bluez5/org.bluez.xml b/spa/plugins/bluez5/org.bluez.xml
new file mode 100644
index 0000000..dee131e
--- /dev/null
+++ b/spa/plugins/bluez5/org.bluez.xml
@@ -0,0 +1,71 @@
+<node>
+ <interface name="org.bluez.Adapter1">
+ <method name="RegisterApplication">
+ <arg direction="in" type="o" name="path"/>
+ <arg direction="in" type="a{sv}" name="options"/>
+ </method>
+ </interface>
+
+ <interface name="org.bluez.Device1">
+ <property name="Adapter" type="o" access="read"/>
+ <property name="Connected" type="b" access="read"/>
+ <property name="ServicesResolved" type="b" access="read"/>
+ <property name="Name" type="s" access="read"/>
+ <property name="Alias" type="s" access="read"/>
+ <property name="Address" type="s" access="read"/>
+ <property name="Icon" type="s" access="read"/>
+ <property name="Class" type="u" access="read"/>
+ <property name="Appearance" type="q" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattManager1">
+ <method name="RegisterApplication">
+ <arg direction="in" type="o" name="path"/>
+ <arg direction="in" type="a{sv}" name="options"/>
+ </method>
+ </interface>
+
+ <interface name="org.bluez.GattProfile1">
+ <method name="Release">
+ </method>
+ <property name="UUIDs" type="as" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattService1">
+ <property name="UUID" type="s" access="read"/>
+ <property name="Primary" type="b" access="read"/>
+ <property name="Device" type="o" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattCharacteristic1">
+ <method name="ReadValue">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="ay" name="value"/>
+ </method>
+ <method name="AcquireNotify">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="h" name="fd"/>
+ <arg direction="out" type="q" name="mtu"/>
+ </method>
+ <method name="AcquireWrite">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="h" name="fd"/>
+ <arg direction="out" type="q" name="mtu"/>
+ </method>
+ <property name="UUID" type="s" access="read"/>
+ <property name="Service" type="o" access="read"/>
+ <property name="WriteAcquired" type="b" access="read"/>
+ <property name="NotifyAcquired" type="b" access="read"/>
+ <property name="Flags" type="as" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattDescriptor1">
+ <method name="ReadValue">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="ay" name="value"/>
+ </method>
+ <property name="UUID" type="s" access="read"/>
+ <property name="Characteristic" type="o" access="read"/>
+ <property name="Flags" type="as" access="read"/>
+ </interface>
+</node>
diff --git a/spa/plugins/bluez5/player.c b/spa/plugins/bluez5/player.c
new file mode 100644
index 0000000..a77ca25
--- /dev/null
+++ b/spa/plugins/bluez5/player.c
@@ -0,0 +1,428 @@
+/* Spa Bluez5 AVRCP Player
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <dbus/dbus.h>
+
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "player.h"
+
+#define PLAYER_OBJECT_PATH_BASE "/media_player"
+
+#define PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
+
+#define PLAYER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name='" PLAYER_INTERFACE "'>" \
+ " <property name='PlaybackStatus' type='s' access='read'/>" \
+ " </interface>" \
+ " <interface name='" DBUS_INTERFACE_PROPERTIES "'>" \
+ " <method name='Get'>" \
+ " <arg name='interface' type='s' direction='in' />" \
+ " <arg name='name' type='s' direction='in' />" \
+ " <arg name='value' type='v' direction='out' />" \
+ " </method>" \
+ " <method name='Set'>" \
+ " <arg name='interface' type='s' direction='in' />" \
+ " <arg name='name' type='s' direction='in' />" \
+ " <arg name='value' type='v' direction='in' />" \
+ " </method>" \
+ " <method name='GetAll'>" \
+ " <arg name='interface' type='s' direction='in' />" \
+ " <arg name='properties' type='a{sv}' direction='out' />" \
+ " </method>" \
+ " <signal name='PropertiesChanged'>" \
+ " <arg name='interface' type='s' />" \
+ " <arg name='changed_properties' type='a{sv}' />" \
+ " <arg name='invalidated_properties' type='as' />" \
+ " </signal>" \
+ " </interface>" \
+ " <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>" \
+ " <method name='Introspect'>" \
+ " <arg name='xml' type='s' direction='out'/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define MAX_PROPERTIES 1
+
+struct impl {
+ struct spa_bt_player this;
+ DBusConnection *conn;
+ char *path;
+ struct spa_log *log;
+ struct spa_dict_item properties_items[MAX_PROPERTIES];
+ struct spa_dict properties;
+ unsigned int playing_count;
+};
+
+static size_t instance_counter = 0;
+
+static DBusMessage *properties_get(struct impl *impl, DBusMessage *m)
+{
+ const char *iface, *name;
+ size_t j;
+
+ if (!dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &iface,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ return NULL;
+
+ if (!spa_streq(iface, PLAYER_INTERFACE))
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such interface");
+
+ for (j = 0; j < impl->properties.n_items; ++j) {
+ const struct spa_dict_item *item = &impl->properties.items[j];
+ if (spa_streq(item->key, name)) {
+ DBusMessage *r;
+ DBusMessageIter i, v;
+
+ r = dbus_message_new_method_return(m);
+ if (r == NULL)
+ return NULL;
+
+ dbus_message_iter_init_append(r, &i);
+ dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+ "s", &v);
+ dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
+ &item->value);
+ dbus_message_iter_close_container(&i, &v);
+ return r;
+ }
+ }
+
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such property");
+}
+
+static void append_properties(struct impl *impl, DBusMessageIter *i)
+{
+ DBusMessageIter d, e, v;
+ size_t j;
+
+ dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY,
+ DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+ DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
+ DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d);
+
+ for (j = 0; j < impl->properties.n_items; ++j) {
+ const struct spa_dict_item *item = &impl->properties.items[j];
+
+ spa_log_debug(impl->log, "player %s: %s=%s", impl->path,
+ item->key, item->value);
+
+ dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e);
+ dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key);
+ dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v);
+ dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value);
+ dbus_message_iter_close_container(&e, &v);
+ dbus_message_iter_close_container(&d, &e);
+ }
+
+ dbus_message_iter_close_container(i, &d);
+}
+
+static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m)
+{
+ const char *iface, *name;
+ DBusMessage *r;
+ DBusMessageIter i;
+
+ if (!dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &iface,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ return NULL;
+
+ if (!spa_streq(iface, PLAYER_INTERFACE))
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such interface");
+
+ r = dbus_message_new_method_return(m);
+ if (r == NULL)
+ return NULL;
+
+ dbus_message_iter_init_append(r, &i);
+ append_properties(impl, &i);
+ return r;
+}
+
+static DBusMessage *properties_set(struct impl *impl, DBusMessage *m)
+{
+ return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
+ "Property not writable");
+}
+
+static DBusMessage *introspect(struct impl *impl, DBusMessage *m)
+{
+ const char *xml = PLAYER_INTROSPECT_XML;
+ DBusMessage *r;
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return NULL;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return NULL;
+ return r;
+}
+
+static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *impl = userdata;
+ DBusMessage *r;
+
+ if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
+ r = introspect(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
+ r = properties_get(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
+ r = properties_get_all(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
+ r = properties_set(impl, m);
+ } else {
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(impl->conn, r, NULL)) {
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static int send_update_signal(struct impl *impl)
+{
+ DBusMessage *m;
+ const char *iface = PLAYER_INTERFACE;
+ DBusMessageIter i, a;
+ int res = 0;
+
+ m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface);
+
+ append_properties(impl, &i);
+
+ dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_STRING_AS_STRING, &a);
+ dbus_message_iter_close_container(&i, &a);
+
+ if (!dbus_connection_send(impl->conn, m, NULL))
+ res = -EIO;
+
+ dbus_message_unref(m);
+
+ return res;
+}
+
+static void update_properties(struct impl *impl, bool send_signal)
+{
+ int nitems = 0;
+
+ switch (impl->this.state) {
+ case SPA_BT_PLAYER_PLAYING:
+ impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing");
+ break;
+ case SPA_BT_PLAYER_STOPPED:
+ impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped");
+ break;
+ }
+ impl->properties = SPA_DICT_INIT(impl->properties_items, nitems);
+
+ if (!send_signal)
+ return;
+
+ send_update_signal(impl);
+}
+
+struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log)
+{
+ struct impl *impl;
+ const DBusObjectPathVTable vtable = {
+ .message_function = player_handler,
+ };
+
+ spa_log_topic_init(log, &log_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->this.state = SPA_BT_PLAYER_STOPPED;
+ impl->conn = dbus_connection;
+ impl->log = log;
+ impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++);
+ if (impl->path == NULL) {
+ free(impl);
+ return NULL;
+ }
+
+ dbus_connection_ref(impl->conn);
+
+ update_properties(impl, false);
+
+ if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) {
+ spa_bt_player_destroy(&impl->this);
+ errno = EIO;
+ return NULL;
+ }
+
+ return &impl->this;
+}
+
+void spa_bt_player_destroy(struct spa_bt_player *player)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ /*
+ * We unregister only the object path, but don't unregister it from
+ * BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is
+ * that the DBus connection is terminated immediately after.
+ */
+ dbus_connection_unregister_object_path(impl->conn, impl->path);
+
+ dbus_connection_unref(impl->conn);
+ free(impl->path);
+ free(impl);
+}
+
+int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ switch (state) {
+ case SPA_BT_PLAYER_PLAYING:
+ if (impl->playing_count++ > 0)
+ return 0;
+ break;
+ case SPA_BT_PLAYER_STOPPED:
+ if (impl->playing_count == 0)
+ return -EINVAL;
+ if (--impl->playing_count > 0)
+ return 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ impl->this.state = state;
+ update_properties(impl, true);
+
+ return 0;
+}
+
+int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ DBusError err;
+ DBusMessageIter i;
+ DBusMessage *m, *r;
+ int res = 0;
+
+ spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s",
+ impl->path, adapter_path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
+ BLUEZ_MEDIA_INTERFACE, "RegisterPlayer");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
+ append_properties(impl, &i);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(impl->log, "RegisterPlayer() failed");
+ res = -EIO;
+ }
+
+ dbus_message_unref(r);
+
+ return res;
+}
+
+int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ DBusError err;
+ DBusMessageIter i;
+ DBusMessage *m, *r;
+ int res = 0;
+
+ spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s",
+ impl->path, adapter_path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
+ BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(impl->log, "UnregisterPlayer() failed");
+ res = -EIO;
+ }
+
+ dbus_message_unref(r);
+
+ return res;
+}
diff --git a/spa/plugins/bluez5/player.h b/spa/plugins/bluez5/player.h
new file mode 100644
index 0000000..b50eb6b
--- /dev/null
+++ b/spa/plugins/bluez5/player.h
@@ -0,0 +1,51 @@
+/* Spa Bluez5 AVRCP Player
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_PLAYER_H_
+#define SPA_BLUEZ5_PLAYER_H_
+
+enum spa_bt_player_state {
+ SPA_BT_PLAYER_STOPPED,
+ SPA_BT_PLAYER_PLAYING,
+};
+
+/**
+ * Dummy AVRCP player.
+ *
+ * Some headsets require an AVRCP player to be present, before their
+ * AVRCP volume synchronization works. To work around this, we
+ * register a dummy player that does nothing.
+ */
+struct spa_bt_player {
+ enum spa_bt_player_state state;
+};
+
+struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log);
+void spa_bt_player_destroy(struct spa_bt_player *player);
+int spa_bt_player_set_state(struct spa_bt_player *player,
+ enum spa_bt_player_state state);
+int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path);
+int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path);
+
+#endif
diff --git a/spa/plugins/bluez5/plugin.c b/spa/plugins/bluez5/plugin.c
new file mode 100644
index 0000000..7b7f862
--- /dev/null
+++ b/spa/plugins/bluez5/plugin.c
@@ -0,0 +1,83 @@
+/* Spa Volume plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_bluez5_dbus_factory;
+extern const struct spa_handle_factory spa_bluez5_device_factory;
+extern const struct spa_handle_factory spa_media_sink_factory;
+extern const struct spa_handle_factory spa_media_source_factory;
+extern const struct spa_handle_factory spa_sco_sink_factory;
+extern const struct spa_handle_factory spa_sco_source_factory;
+extern const struct spa_handle_factory spa_a2dp_sink_factory;
+extern const struct spa_handle_factory spa_a2dp_source_factory;
+extern const struct spa_handle_factory spa_bluez5_midi_enum_factory;
+extern const struct spa_handle_factory spa_bluez5_midi_node_factory;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_bluez5_dbus_factory;
+ break;
+ case 1:
+ *factory = &spa_bluez5_device_factory;
+ break;
+ case 2:
+ *factory = &spa_media_sink_factory;
+ break;
+ case 3:
+ *factory = &spa_media_source_factory;
+ break;
+ case 4:
+ *factory = &spa_sco_sink_factory;
+ break;
+ case 5:
+ *factory = &spa_sco_source_factory;
+ break;
+ case 6:
+ *factory = &spa_a2dp_sink_factory;
+ break;
+ case 7:
+ *factory = &spa_a2dp_source_factory;
+ break;
+ case 8:
+ *factory = &spa_bluez5_midi_enum_factory;
+ break;
+ case 9:
+ *factory = &spa_bluez5_midi_node_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c
new file mode 100644
index 0000000..8a7f926
--- /dev/null
+++ b/spa/plugins/bluez5/quirks.c
@@ -0,0 +1,406 @@
+/* Device/adapter/kernel quirk table
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <limits.h>
+#include <sys/utsname.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <bluetooth/bluetooth.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.quirks");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct spa_bt_quirks {
+ struct spa_log *log;
+
+ int force_msbc;
+ int force_hw_volume;
+ int force_sbc_xq;
+ int force_faststream;
+ int force_a2dp_duplex;
+
+ char *device_rules;
+ char *adapter_rules;
+ char *kernel_rules;
+};
+
+static enum spa_bt_feature parse_feature(const char *str)
+{
+ static const struct { const char *key; enum spa_bt_feature value; } feature_keys[] = {
+ { "msbc", SPA_BT_FEATURE_MSBC },
+ { "msbc-alt1", SPA_BT_FEATURE_MSBC_ALT1 },
+ { "msbc-alt1-rtl", SPA_BT_FEATURE_MSBC_ALT1_RTL },
+ { "hw-volume", SPA_BT_FEATURE_HW_VOLUME },
+ { "hw-volume-mic", SPA_BT_FEATURE_HW_VOLUME_MIC },
+ { "sbc-xq", SPA_BT_FEATURE_SBC_XQ },
+ { "faststream", SPA_BT_FEATURE_FASTSTREAM },
+ { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX },
+ };
+ SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) {
+ if (spa_streq(str, f->key))
+ return f->value;
+ }
+ return 0;
+}
+
+static int do_match(const char *rules, struct spa_dict *dict, uint32_t *no_features)
+{
+ struct spa_json rules_json = SPA_JSON_INIT(rules, strlen(rules));
+ struct spa_json rules_arr, it[2];
+
+ if (spa_json_enter_array(&rules_json, &rules_arr) <= 0)
+ return 1;
+
+ while (spa_json_enter_object(&rules_arr, &it[0]) > 0) {
+ char key[256];
+ int match = true;
+ uint32_t no_features_cur = 0;
+
+ while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+ char val[4096];
+ const char *str, *value;
+ int len;
+ bool success = false;
+
+ if (spa_streq(key, "no-features")) {
+ if (spa_json_enter_array(&it[0], &it[1]) > 0) {
+ while (spa_json_get_string(&it[1], val, sizeof(val)) > 0)
+ no_features_cur |= parse_feature(val);
+ }
+ continue;
+ }
+
+ if ((len = spa_json_next(&it[0], &value)) <= 0)
+ break;
+
+ if (spa_json_is_null(value, len)) {
+ value = NULL;
+ } else {
+ if (spa_json_parse_stringn(value, len, val, sizeof(val)) < 0)
+ continue;
+ value = val;
+ }
+
+ str = spa_dict_lookup(dict, key);
+ if (value == NULL) {
+ success = str == NULL;
+ } else if (str != NULL) {
+ if (value[0] == '~') {
+ regex_t r;
+ if (regcomp(&r, value+1, REG_EXTENDED | REG_NOSUB) == 0) {
+ if (regexec(&r, str, 0, NULL, 0) == 0)
+ success = true;
+ regfree(&r);
+ }
+ } else if (spa_streq(str, value)) {
+ success = true;
+ }
+ }
+
+ if (!success) {
+ match = false;
+ break;
+ }
+ }
+
+ if (match) {
+ *no_features = no_features_cur;
+ return 0;
+ }
+ }
+ return 0;
+}
+
+static int parse_force_flag(const struct spa_dict *info, const char *key)
+{
+ const char *str;
+ str = spa_dict_lookup(info, key);
+ if (str == NULL)
+ return -1;
+ else
+ return (strcmp(str, "true") == 0 || atoi(str)) ? 1 : 0;
+}
+
+static void load_quirks(struct spa_bt_quirks *this, const char *str, size_t len)
+{
+ struct spa_json data = SPA_JSON_INIT(str, len);
+ struct spa_json rules;
+ char key[1024];
+
+ if (spa_json_enter_object(&data, &rules) <= 0)
+ spa_json_init(&rules, str, len);
+
+ while (spa_json_get_string(&rules, key, sizeof(key)) > 0) {
+ int sz;
+ const char *value;
+
+ if ((sz = spa_json_next(&rules, &value)) <= 0)
+ break;
+
+ if (!spa_json_is_container(value, sz))
+ continue;
+
+ sz = spa_json_container_len(&rules, value, sz);
+
+ if (spa_streq(key, "bluez5.features.kernel") && !this->kernel_rules)
+ this->kernel_rules = strndup(value, sz);
+ else if (spa_streq(key, "bluez5.features.adapter") && !this->adapter_rules)
+ this->adapter_rules = strndup(value, sz);
+ else if (spa_streq(key, "bluez5.features.device") && !this->device_rules)
+ this->device_rules = strndup(value, sz);
+ }
+}
+
+static int load_conf(struct spa_bt_quirks *this, const char *path)
+{
+ char *data;
+ struct stat sbuf;
+ int fd = -1;
+
+ spa_log_debug(this->log, "loading %s", path);
+
+ if ((fd = open(path, O_CLOEXEC | O_RDONLY)) < 0)
+ goto fail;
+ if (fstat(fd, &sbuf) < 0)
+ goto fail;
+ if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
+ goto fail;
+ close(fd);
+
+ load_quirks(this, data, sbuf.st_size);
+ munmap(data, sbuf.st_size);
+
+ return 0;
+
+fail:
+ if (fd >= 0)
+ close(fd);
+ return -errno;
+}
+
+struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log)
+{
+ struct spa_bt_quirks *this;
+ const char *str;
+
+ if (!info) {
+ errno = -EINVAL;
+ return NULL;
+ }
+
+ this = calloc(1, sizeof(struct spa_bt_quirks));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ this->force_sbc_xq = parse_force_flag(info, "bluez5.enable-sbc-xq");
+ this->force_msbc = parse_force_flag(info, "bluez5.enable-msbc");
+ this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume");
+ this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream");
+ this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex");
+
+ if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) {
+ spa_log_debug(this->log, "loading session manager provided data");
+ load_quirks(this, str, strlen(str));
+ } else {
+ char path[PATH_MAX];
+ const char *dir = getenv("SPA_DATA_DIR");
+ int res;
+
+ if (dir == NULL)
+ dir = SPADATADIR;
+
+ if (spa_scnprintf(path, sizeof(path), "%s/bluez5/bluez-hardware.conf", dir) >= 0)
+ if ((res = load_conf(this, path)) < 0)
+ spa_log_warn(this->log, "failed to load '%s': %s", path,
+ spa_strerror(res));
+ }
+ if (!(this->kernel_rules && this->adapter_rules && this->device_rules))
+ spa_log_warn(this->log, "failed to load bluez-hardware.conf");
+
+ return this;
+}
+
+void spa_bt_quirks_destroy(struct spa_bt_quirks *this)
+{
+ free(this->kernel_rules);
+ free(this->adapter_rules);
+ free(this->device_rules);
+ free(this);
+}
+
+static void log_props(struct spa_log *log, const struct spa_dict *dict)
+{
+ const struct spa_dict_item *item;
+ spa_dict_for_each(item, dict)
+ spa_log_debug(log, "quirk property %s=%s", item->key, item->value);
+}
+
+static void strtolower(char *src, char *dst, int maxsize)
+{
+ while (maxsize > 1 && *src != '\0') {
+ *dst = (*src >= 'A' && *src <= 'Z') ? ('a' + (*src - 'A')) : *src;
+ ++src;
+ ++dst;
+ --maxsize;
+ }
+ if (maxsize > 0)
+ *dst = '\0';
+}
+
+int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
+ const struct spa_bt_adapter *adapter,
+ const struct spa_bt_device *device,
+ uint32_t *features)
+{
+ struct spa_dict props;
+ struct spa_dict_item items[5];
+ int res;
+
+ *features = ~(uint32_t)0;
+
+ /* Kernel */
+ if (this->kernel_rules) {
+ uint32_t no_features = 0;
+ int nitems = 0;
+ struct utsname name;
+ if ((res = uname(&name)) < 0)
+ return res;
+ items[nitems++] = SPA_DICT_ITEM_INIT("sysname", name.sysname);
+ items[nitems++] = SPA_DICT_ITEM_INIT("release", name.release);
+ items[nitems++] = SPA_DICT_ITEM_INIT("version", name.version);
+ props = SPA_DICT_INIT(items, nitems);
+ log_props(this->log, &props);
+ do_match(this->kernel_rules, &props, &no_features);
+ spa_log_debug(this->log, "kernel quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Adapter */
+ if (this->adapter_rules && adapter) {
+ uint32_t no_features = 0;
+ int nitems = 0;
+ char vendor_id[64], product_id[64], address[64];
+
+ if (spa_bt_format_vendor_product_id(
+ adapter->source_id, adapter->vendor_id, adapter->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id);
+ }
+ items[nitems++] = SPA_DICT_ITEM_INIT("bus-type",
+ (adapter->bus_type == BUS_TYPE_USB) ? "usb" : "other");
+ if (adapter->address) {
+ strtolower(adapter->address, address, sizeof(address));
+ items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
+ }
+ props = SPA_DICT_INIT(items, nitems);
+ log_props(this->log, &props);
+ do_match(this->adapter_rules, &props, &no_features);
+ spa_log_debug(this->log, "adapter quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Device */
+ if (this->device_rules && device) {
+ uint32_t no_features = 0;
+ int nitems = 0;
+ char vendor_id[64], product_id[64], version_id[64], address[64];
+ if (spa_bt_format_vendor_product_id(
+ device->source_id, device->vendor_id, device->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ snprintf(version_id, sizeof(version_id), "%04x",
+ (unsigned int)device->version_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("version-id", version_id);
+ }
+ if (device->name)
+ items[nitems++] = SPA_DICT_ITEM_INIT("name", device->name);
+ if (device->address) {
+ strtolower(device->address, address, sizeof(address));
+ items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
+ }
+ props = SPA_DICT_INIT(items, nitems);
+ log_props(this->log, &props);
+ do_match(this->device_rules, &props, &no_features);
+ spa_log_debug(this->log, "device quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Force flags */
+ if (this->force_msbc != -1) {
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC, this->force_msbc);
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1, this->force_msbc);
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1_RTL, this->force_msbc);
+ }
+
+ if (this->force_hw_volume != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_VOLUME, this->force_hw_volume);
+
+ if (this->force_sbc_xq != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_SBC_XQ, this->force_sbc_xq);
+
+ if (this->force_faststream != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_FASTSTREAM, this->force_faststream);
+
+ if (this->force_a2dp_duplex != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex);
+
+ return 0;
+}
diff --git a/spa/plugins/bluez5/rtp.h b/spa/plugins/bluez5/rtp.h
new file mode 100644
index 0000000..20694c1
--- /dev/null
+++ b/spa/plugins/bluez5/rtp.h
@@ -0,0 +1,74 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ *
+ *
+ * This library 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.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+struct rtp_header {
+ unsigned cc:4;
+ unsigned x:1;
+ unsigned p:1;
+ unsigned v:2;
+
+ unsigned pt:7;
+ unsigned m:1;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+ unsigned frame_count:4;
+ unsigned rfa0:1;
+ unsigned is_last_fragment:1;
+ unsigned is_first_fragment:1;
+ unsigned is_fragmented:1;
+} __attribute__ ((packed));
+
+#elif __BYTE_ORDER == __BIG_ENDIAN
+
+struct rtp_header {
+ unsigned v:2;
+ unsigned p:1;
+ unsigned x:1;
+ unsigned cc:4;
+
+ unsigned m:1;
+ unsigned pt:7;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+ unsigned is_fragmented:1;
+ unsigned is_first_fragment:1;
+ unsigned is_last_fragment:1;
+ unsigned rfa0:1;
+ unsigned frame_count:4;
+} __attribute__ ((packed));
+
+#else
+#error "Unknown byte order"
+#endif
diff --git a/spa/plugins/bluez5/sco-io.c b/spa/plugins/bluez5/sco-io.c
new file mode 100644
index 0000000..0657750
--- /dev/null
+++ b/spa/plugins/bluez5/sco-io.c
@@ -0,0 +1,289 @@
+/* Spa SCO I/O
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+
+/* We'll use the read rx data size to find the correct packet size for writing,
+ * since kernel might not report it as the socket MTU, see
+ * https://lore.kernel.org/linux-bluetooth/20201210003528.3pmaxvubiwegxmhl@pali/T/
+ *
+ * We continue reading also when there's no source connected, to keep socket
+ * flushed.
+ *
+ * XXX: when the kernel/backends start giving the right values, the heuristic
+ * XXX: can be removed
+ */
+#define MAX_MTU 1024
+
+
+struct spa_bt_sco_io {
+ bool started;
+
+ uint8_t read_buffer[MAX_MTU];
+ uint32_t read_size;
+
+ int fd;
+ uint16_t read_mtu;
+ uint16_t write_mtu;
+
+ struct spa_loop *data_loop;
+ struct spa_source source;
+
+ int (*source_cb)(void *userdata, uint8_t *data, int size);
+ void *source_userdata;
+
+ int (*sink_cb)(void *userdata);
+ void *sink_userdata;
+};
+
+
+static void update_source(struct spa_bt_sco_io *io)
+{
+ int enabled;
+ int changed = 0;
+
+ enabled = io->sink_cb != NULL;
+ if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_OUT) != enabled) {
+ SPA_FLAG_UPDATE(io->source.mask, SPA_IO_OUT, enabled);
+ changed = 1;
+ }
+
+ if (changed) {
+ spa_loop_update_source(io->data_loop, &io->source);
+ }
+}
+
+static void sco_io_on_ready(struct spa_source *source)
+{
+ struct spa_bt_sco_io *io = source->data;
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_IN)) {
+ int res;
+
+ read_again:
+ res = read(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU));
+ if (res <= 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ goto read_again;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* no data: try it next time */
+ goto read_done;
+ }
+
+ /* error */
+ goto stop;
+ }
+
+ io->read_size = res;
+
+ if (io->source_cb) {
+ int res;
+ res = io->source_cb(io->source_userdata, io->read_buffer, io->read_size);
+ if (res) {
+ io->source_cb = NULL;
+ }
+ }
+ }
+
+read_done:
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_OUT)) {
+ if (io->sink_cb) {
+ int res;
+ res = io->sink_cb(io->sink_userdata);
+ if (res) {
+ io->sink_cb = NULL;
+ }
+ }
+ }
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) || SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) {
+ goto stop;
+ }
+
+ /* Poll socket in/out only if necessary */
+ update_source(io);
+
+ return;
+
+stop:
+ if (io->source.loop) {
+ spa_loop_remove_source(io->data_loop, &io->source);
+ io->started = false;
+ }
+}
+
+/*
+ * Write data to socket in correctly sized blocks.
+ * Returns the number of bytes written, 0 when data cannot be written now or
+ * there is too little of it to write, and <0 on write error.
+ */
+int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *buf, int size)
+{
+ uint16_t packet_size;
+ uint8_t *buf_start = buf;
+
+ if (io->read_size == 0) {
+ /* The proper write packet size is not known yet */
+ return 0;
+ }
+
+ packet_size = SPA_MIN(io->write_mtu, io->read_size);
+
+ if (size < packet_size) {
+ return 0;
+ }
+
+ do {
+ int written;
+
+ written = write(io->fd, buf, packet_size);
+ if (written < 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ continue;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* Don't continue writing */
+ break;
+ }
+ return -errno;
+ }
+
+ buf += written;
+ size -= written;
+ } while (size >= packet_size);
+
+ return buf - buf_start;
+}
+
+
+struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop,
+ int fd,
+ uint16_t read_mtu,
+ uint16_t write_mtu)
+{
+ struct spa_bt_sco_io *io;
+
+ io = calloc(1, sizeof(struct spa_bt_sco_io));
+ if (io == NULL)
+ return io;
+
+ io->fd = fd;
+ io->read_mtu = read_mtu;
+ io->write_mtu = write_mtu;
+ io->data_loop = data_loop;
+
+ io->read_size = 0;
+
+ /* Add the ready callback */
+ io->source.data = io;
+ io->source.fd = io->fd;
+ io->source.func = sco_io_on_ready;
+ io->source.mask = SPA_IO_IN | SPA_IO_OUT | SPA_IO_ERR | SPA_IO_HUP;
+ io->source.rmask = 0;
+ spa_loop_add_source(io->data_loop, &io->source);
+
+ io->started = true;
+
+ return io;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_bt_sco_io *io = user_data;
+
+ if (io->source.loop)
+ spa_loop_remove_source(io->data_loop, &io->source);
+
+ return 0;
+}
+
+void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io)
+{
+ if (io->started)
+ spa_loop_invoke(io->data_loop, do_remove_source, 0, NULL, 0, true, io);
+
+ io->started = false;
+ free(io);
+}
+
+/* Set source callback.
+ * This function should only be called from the data thread.
+ * Callback is called (in data loop) with data just read from the socket.
+ */
+void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *, uint8_t *, int), void *userdata)
+{
+ io->source_cb = source_cb;
+ io->source_userdata = userdata;
+
+ if (io->started) {
+ update_source(io);
+ }
+}
+
+/* Set sink callback.
+ * This function should only be called from the data thread.
+ * Callback is called (in data loop) when socket can be written to.
+ */
+void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *), void *userdata)
+{
+ io->sink_cb = sink_cb;
+ io->sink_userdata = userdata;
+
+ if (io->started) {
+ update_source(io);
+ }
+}
diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c
new file mode 100644
index 0000000..18a34f6
--- /dev/null
+++ b/spa/plugins/bluez5/sco-sink.c
@@ -0,0 +1,1517 @@
+/* Spa SCO Sink
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/result.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.sco");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ int frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list ready;
+
+ struct buffer *current_buffer;
+ uint32_t ready_offset;
+ uint8_t write_buffer[4096];
+ uint32_t write_buffer_size;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ /* Support */
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ /* Hooks and callbacks */
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ /* Info */
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define N_NODE_PARAMS 2
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ uint32_t quantum_limit;
+
+ /* Transport */
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ /* Port */
+ struct port port;
+
+ /* Flags */
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int flush_pending:1;
+
+ /* Sources */
+ struct spa_source source;
+ struct spa_source flush_timer_source;
+
+ /* Timer */
+ int timerfd;
+ int flush_timerfd;
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t process_time;
+ uint64_t prev_flush_time;
+ uint64_t next_flush_time;
+
+ /* mSBC */
+ sbc_t msbc;
+ uint8_t *buffer;
+ uint8_t *buffer_head;
+ uint8_t *buffer_next;
+ int buffer_size;
+ int msbc_seq;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static const char sntable[4] = { 0x08, 0x38, 0xC8, 0xF8 };
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL);
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void enable_flush_timer(struct impl *this, bool enabled)
+{
+ struct itimerspec ts;
+
+ if (!enabled)
+ this->next_flush_time = 0;
+
+ ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system,
+ this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+ this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t bytes = 0;
+ struct buffer *b;
+
+ spa_list_for_each(b, &port->ready, link) {
+ struct spa_data *d = b->buf->datas;
+
+ bytes += d[0].chunk->size;
+ }
+
+ if (bytes > port->ready_offset)
+ bytes -= port->ready_offset;
+ else
+ bytes = 0;
+
+ return bytes / port->frame_size;
+}
+
+static void flush_data(struct impl *this)
+{
+ struct port *port = &this->port;
+ const uint32_t min_in_size =
+ (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
+ MSBC_DECODED_SIZE : this->transport->write_mtu;
+ uint8_t * const packet =
+ (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
+ this->buffer_head : port->write_buffer;
+ const uint32_t packet_samples = min_in_size / port->frame_size;
+ const uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate;
+ int processed = 0;
+ int written;
+
+ if (this->transport == NULL || this->transport->sco_io == NULL)
+ return;
+
+ while (!spa_list_is_empty(&port->ready) && port->write_buffer_size < min_in_size) {
+ struct spa_data *datas;
+
+ /* get buffer */
+ if (!port->current_buffer) {
+ spa_return_if_fail(!spa_list_is_empty(&port->ready));
+ port->current_buffer = spa_list_first(&port->ready, struct buffer, link);
+ port->ready_offset = 0;
+ }
+ datas = port->current_buffer->buf->datas;
+
+ /* if buffer has data, copy it into the write buffer */
+ if (datas[0].chunk->size - port->ready_offset > 0) {
+ const uint32_t avail =
+ SPA_MIN(min_in_size, datas[0].chunk->size - port->ready_offset);
+ const uint32_t size =
+ (avail + port->write_buffer_size) > min_in_size ?
+ min_in_size - port->write_buffer_size : avail;
+ memcpy(port->write_buffer + port->write_buffer_size,
+ (uint8_t *)datas[0].data + port->ready_offset,
+ size);
+ port->write_buffer_size += size;
+ port->ready_offset += size;
+ } else {
+ struct buffer *b;
+
+ b = port->current_buffer;
+ port->current_buffer = NULL;
+
+ /* reuse buffer */
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+ spa_log_trace(this->log, "sco-sink %p: reuse buffer %u", this, b->id);
+ port->io->buffer_id = b->id;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ }
+ }
+
+ if (this->flush_pending) {
+ spa_log_trace(this->log, "%p: wait for flush timer", this);
+ return;
+ }
+
+ if (port->write_buffer_size < min_in_size) {
+ /* wait for more data */
+ spa_log_trace(this->log, "%p: skip flush", this);
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ ssize_t out_encoded;
+
+ /* Encode */
+ if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+ /* Buffer overrun; shouldn't usually happen. Drop data and reset. */
+ this->buffer_head = this->buffer_next = this->buffer;
+ spa_log_warn(this->log, "sco-sink: mSBC buffer overrun, dropping data");
+ }
+ this->buffer_next[0] = 0x01;
+ this->buffer_next[1] = sntable[this->msbc_seq % 4];
+ this->buffer_next[59] = 0x00;
+ this->msbc_seq = (this->msbc_seq + 1) % 4;
+ processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size,
+ this->buffer_next + 2, MSBC_ENCODED_SIZE - 3, &out_encoded);
+ if (processed < 0) {
+ spa_log_warn(this->log, "sbc_encode failed: %d", processed);
+ return;
+ }
+ this->buffer_next += out_encoded + 3;
+ port->write_buffer_size = 0;
+
+ /* Write */
+ written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+ this->buffer_next - this->buffer_head);
+ if (written < 0) {
+ spa_log_warn(this->log, "failed to write data: %d (%s)",
+ written, spa_strerror(written));
+ goto stop;
+ }
+
+ this->buffer_head += written;
+
+ if (this->buffer_head == this->buffer_next)
+ this->buffer_head = this->buffer_next = this->buffer;
+ else if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+ /* Written bytes is not necessarily commensurate
+ * with MSBC_ENCODED_SIZE. If this occurs, copy data.
+ */
+ int size = this->buffer_next - this->buffer_head;
+ spa_memmove(this->buffer, this->buffer_head, size);
+ this->buffer_next = this->buffer + size;
+ this->buffer_head = this->buffer;
+ }
+ } else {
+ written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+ port->write_buffer_size);
+ if (written < 0) {
+ spa_log_warn(this->log, "sco-sink: write failure: %d (%s)",
+ written, spa_strerror(written));
+ goto stop;
+ } else if (written == 0) {
+ /* EAGAIN or similar, just skip ahead */
+ written = SPA_MIN(port->write_buffer_size, (uint32_t)48);
+ }
+
+ processed = written;
+ port->write_buffer_size -= written;
+
+ if (port->write_buffer_size > 0 && written > 0) {
+ spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size);
+ }
+ }
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec ts;
+ uint64_t now;
+ uint64_t dt;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ dt = now - this->prev_flush_time;
+ this->prev_flush_time = now;
+
+ spa_log_trace(this->log,
+ "%p: send wrote:%d dt:%"PRIu64,
+ this, written, dt);
+ }
+
+ spa_log_trace(this->log, "write socket data %d", written);
+
+ if (SPA_LIKELY(this->position)) {
+ uint32_t frames = get_queued_frames(this);
+ uint64_t duration_ns;
+
+ /*
+ * Flush at the time position of the next buffered sample.
+ */
+ duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+ / this->position->clock.rate.denom);
+ this->next_flush_time = this->process_time + duration_ns
+ - ((uint64_t)frames * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate);
+
+ /*
+ * We could delay the output by one packet to avoid waiting
+ * for the next buffer and so make send intervals more regular.
+ * However, this appears not needed in practice, and it's better
+ * to not add latency if not needed.
+ */
+#if 0
+ this->next_flush_time += SPA_MIN(packet_time,
+ duration_ns * (port->n_buffers - 1));
+#endif
+ } else {
+ if (this->next_flush_time == 0)
+ this->next_flush_time = this->process_time;
+ this->next_flush_time += packet_time;
+ }
+
+ enable_flush_timer(this, true);
+ return;
+
+stop:
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+ enable_flush_timer(this, false);
+}
+
+static void sco_on_flush_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ spa_log_trace(this->log, "%p: flush on timeout", this);
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ if (this->transport == NULL) {
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ while (exp-- > 0) {
+ this->flush_pending = false;
+ flush_data(this);
+ }
+}
+
+static void sco_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ struct spa_io_buffers *io = port->io;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+ this->clock->delay = 0;
+ }
+
+ spa_log_trace(this->log, "%p: %d", this, io->status);
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+/* greater common divider */
+static int gcd(int a, int b) {
+ while(b) {
+ int c = b;
+ b = a % b;
+ a = c;
+ }
+ return a;
+}
+/* least common multiple */
+static int lcm(int a, int b) {
+ return (a*b)/gcd(a,b);
+}
+
+static int do_start(struct impl *this)
+{
+ bool do_accept;
+ int res;
+
+ /* Don't do anything if the node has already started */
+ if (this->started)
+ return 0;
+
+ /* Make sure the transport is valid */
+ spa_return_val_if_fail(this->transport != NULL, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d", this, this->following);
+
+ /* Do accept if Gateway; otherwise do connect for Head Unit */
+ do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ /* acquire the socket fd (false -> connect | true -> accept) */
+ if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0)
+ return res;
+
+ /* Init mSBC if needed */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ sbc_init_msbc(&this->msbc, 0);
+ /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */
+ this->msbc.endian = SBC_LE;
+
+ /* write_mtu might not be correct at this point, so we'll throw
+ * in some common ones, at the cost of a potentially larger
+ * allocation (size <= 120 * write_mtu). If it still fails to be
+ * commensurate, we may end up doing memmoves, but nothing worse
+ * is going to happen.
+ */
+ this->buffer_size = lcm(24, lcm(60, lcm(this->transport->write_mtu, 2 * MSBC_ENCODED_SIZE)));
+ this->buffer = calloc(this->buffer_size, sizeof(uint8_t));
+ this->buffer_head = this->buffer_next = this->buffer;
+ if (this->buffer == NULL) {
+ res = -errno;
+ goto fail;
+ }
+ }
+
+ spa_return_val_if_fail(this->transport->write_mtu <= sizeof(this->port.write_buffer), -EINVAL);
+
+ /* start socket i/o */
+ if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0)
+ goto fail;
+
+ /* Add the timeout callback */
+ this->source.data = this;
+ this->source.fd = this->timerfd;
+ this->source.func = sco_on_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->flush_timer_source.data = this;
+ this->flush_timer_source.fd = this->flush_timerfd;
+ this->flush_timer_source.func = sco_on_flush_timeout;
+ this->flush_timer_source.mask = SPA_IO_IN;
+ this->flush_timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_timer_source);
+
+ /* start processing */
+ this->flush_pending = false;
+ set_timers(this);
+
+ /* Set the started flag */
+ this->started = true;
+
+ return 0;
+
+fail:
+ free(this->buffer);
+ this->buffer = NULL;
+ spa_bt_transport_release(this->transport);
+ return res;
+}
+
+/* Drop any buffered data remaining in the port */
+static void drop_port_output(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ port->write_buffer_size = 0;
+ port->current_buffer = NULL;
+ port->ready_offset = 0;
+
+ while (!spa_list_is_empty(&port->ready)) {
+ struct buffer *b;
+ b = spa_list_first(&port->ready, struct buffer, link);
+
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+ port->io->buffer_id = b->id;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ }
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ set_timeout(this, 0);
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+
+ if (this->flush_timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL);
+
+ /* Drop buffered data in the ready queue. Ideally there shouldn't be any. */
+ drop_port_output(this);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_trace(this->log, "sco-sink %p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->buffer) {
+ free(this->buffer);
+ this->buffer = NULL;
+ this->buffer_head = this->buffer_next = this->buffer;
+ }
+
+ if (this->transport) {
+ /* Release the transport; it is responsible for closing the fd */
+ res = spa_bt_transport_release(this->transport);
+ }
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ static const struct spa_dict_item hu_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+ };
+
+ const struct spa_dict_item ag_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : "HSP/HFP") },
+ { SPA_KEY_MEDIA_ROLE, "Communication" },
+ };
+ bool is_ag = this->transport &&
+ (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = is_ag ?
+ &SPA_DICT_INIT_ARRAY(ag_node_info_items) :
+ &SPA_DICT_INIT_ARRAY(hu_node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (result.index > 0)
+ return 0;
+ if (this->transport == NULL)
+ return -EIO;
+
+ /* set the info structure */
+ struct spa_audio_info_raw info = { 0, };
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ /* CVSD format has a rate of 8kHz
+ * MSBC format has a rate of 16kHz */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC)
+ info.rate = 16000;
+ else
+ info.rate = 8000;
+
+ /* build the param */
+ param = spa_format_audio_raw_build(&b, id, &info);
+
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels != 1)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels * 2;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->outstanding = true;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (buffers[i]->datas[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+
+ if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!b->outstanding) {
+ spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id);
+
+ spa_list_append(&port->ready, &b->link);
+ b->outstanding = false;
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_OK;
+ }
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now;
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ this->process_time = this->current_time;
+
+ if (!spa_list_is_empty(&port->ready)) {
+ spa_log_trace(this->log, "%p: flush on process", this);
+ flush_data(this);
+ }
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_system_close(this->data_system, this->flush_timerfd);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS |
+ SPA_NODE_CHANGE_MASK_PROPS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ spa_list_init(&port->ready);
+
+ this->quantum_limit = 8192;
+
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ this->flush_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play bluetooth audio with hsp/hfp" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_sco_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_SCO_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
new file mode 100644
index 0000000..6f5fe08
--- /dev/null
+++ b/spa/plugins/bluez5/sco-source.c
@@ -0,0 +1,1592 @@
+/* Spa SCO Source
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/result.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.sco");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#include "decode-buffer.h"
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ int frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct spa_bt_decode_buffer buffer;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resampling:1;
+
+ struct spa_source timer_source;
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ /* mSBC */
+ sbc_t msbc;
+ bool msbc_seq_initialized;
+ uint8_t msbc_seq;
+
+ /* mSBC frame parsing */
+ uint8_t msbc_buffer[MSBC_ENCODED_SIZE];
+ uint8_t msbc_buffer_pos;
+
+ struct timespec now;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->port;
+
+ set_timers(this);
+ spa_bt_decode_buffer_recover(&port->buffer);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ /* noop */
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outstanding) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+/* Append data to msbc buffer, syncing buffer start to frame headers */
+static void msbc_buffer_append_byte(struct impl *this, uint8_t byte)
+{
+ /* Parse mSBC frame header */
+ if (this->msbc_buffer_pos == 0) {
+ if (byte != 0x01) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 1) {
+ if (!((byte & 0x0F) == 0x08 &&
+ ((byte >> 4) & 1) == ((byte >> 5) & 1) &&
+ ((byte >> 6) & 1) == ((byte >> 7) & 1))) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 2) {
+ /* .. and beginning of MSBC frame: SYNCWORD + 2 nul bytes */
+ if (byte != 0xAD) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 3) {
+ if (byte != 0x00) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 4) {
+ if (byte != 0x00) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos >= MSBC_ENCODED_SIZE) {
+ /* Packet completed. Reset. */
+ this->msbc_buffer_pos = 0;
+ msbc_buffer_append_byte(this, byte);
+ return;
+ }
+ this->msbc_buffer[this->msbc_buffer_pos] = byte;
+ ++this->msbc_buffer_pos;
+}
+
+/* Helper function for debugging */
+static SPA_UNUSED void hexdump_to_log(struct impl *this, uint8_t *data, size_t size)
+{
+ char buf[2048];
+ size_t i, col = 0, pos = 0;
+ buf[0] = '\0';
+ for (i = 0; i < size; ++i) {
+ int res;
+ res = spa_scnprintf(buf + pos, sizeof(buf) - pos, "%s%02x",
+ (col == 0) ? "\n\t" : " ", data[i]);
+ if (res < 0)
+ break;
+ pos += res;
+ col = (col + 1) % 16;
+ }
+ spa_log_trace(this->log, "hexdump (%d bytes):%s", (int)size, buf);
+}
+
+/* helper function to detect if a packet consists only of zeros */
+static bool is_zero_packet(uint8_t *data, int size)
+{
+ for (int i = 0; i < size; ++i) {
+ if (data[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static uint32_t preprocess_and_decode_msbc_data(void *userdata, uint8_t *read_data, int size_read)
+{
+ struct impl *this = userdata;
+ struct port *port = &this->port;
+ uint32_t decoded = 0;
+ int i;
+
+ spa_log_trace(this->log, "handling mSBC data");
+
+ /*
+ * Check if the packet contains only zeros - if so ignore the packet.
+ * This is necessary, because some kernels insert bogus "all-zero" packets
+ * into the datastream.
+ * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549
+ */
+ if (is_zero_packet(read_data, size_read))
+ return 0;
+
+ for (i = 0; i < size_read; ++i) {
+ void *buf;
+ uint32_t avail;
+ int seq, processed;
+ size_t written;
+
+ msbc_buffer_append_byte(this, read_data[i]);
+
+ if (this->msbc_buffer_pos != MSBC_ENCODED_SIZE)
+ continue;
+
+ /*
+ * Handle found mSBC packet
+ */
+
+ buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+
+ /* Check sequence number */
+ seq = ((this->msbc_buffer[1] >> 4) & 1) |
+ ((this->msbc_buffer[1] >> 6) & 2);
+
+ spa_log_trace(this->log, "mSBC packet seq=%u", seq);
+ if (!this->msbc_seq_initialized) {
+ this->msbc_seq_initialized = true;
+ this->msbc_seq = seq;
+ } else if (seq != this->msbc_seq) {
+ /* TODO: PLC (too late to insert data now) */
+ spa_log_info(this->log,
+ "missing mSBC packet: %u != %u", seq, this->msbc_seq);
+ this->msbc_seq = seq;
+ }
+
+ this->msbc_seq = (this->msbc_seq + 1) % 4;
+
+ if (avail < MSBC_DECODED_SIZE)
+ spa_log_warn(this->log, "Output buffer full, dropping msbc data");
+
+ /* decode frame */
+ processed = sbc_decode(
+ &this->msbc, this->msbc_buffer + 2, MSBC_ENCODED_SIZE - 3,
+ buf, avail, &written);
+
+ if (processed < 0) {
+ spa_log_warn(this->log, "sbc_decode failed: %d", processed);
+ /* TODO: manage errors */
+ continue;
+ }
+
+ spa_bt_decode_buffer_write_packet(&port->buffer, written);
+ decoded += written;
+ }
+
+ return decoded;
+}
+
+static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
+{
+ struct impl *this = userdata;
+ struct port *port = &this->port;
+ uint32_t decoded;
+ uint64_t dt;
+
+ if (this->transport == NULL) {
+ spa_log_debug(this->log, "no transport, stop reading");
+ goto stop;
+ }
+
+ /* update the current pts */
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now);
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now);
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
+
+ /* handle data read from socket */
+#if 0
+ hexdump_to_log(this, read_data, size_read);
+#endif
+
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ decoded = preprocess_and_decode_msbc_data(userdata, read_data, size_read);
+ } else {
+ uint32_t avail;
+ uint8_t *packet;
+
+ if (size_read != 48 && is_zero_packet(read_data, size_read)) {
+ /* Adapter is returning non-standard CVSD stream. For example
+ * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021
+ * on kernel 5.13.19 produces such data.
+ */
+ return 0;
+ }
+
+ if (size_read % port->frame_size != 0) {
+ /* Unaligned data: reception or adapter problem.
+ * Consider the whole packet lost and report.
+ */
+ spa_log_debug(this->log,
+ "received bad Bluetooth SCO CVSD packet");
+ return 0;
+ }
+
+ packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+ avail = SPA_MIN(avail, (uint32_t)size_read);
+ spa_memmove(packet, read_data, avail);
+ spa_bt_decode_buffer_write_packet(&port->buffer, avail);
+
+ decoded = avail;
+ }
+
+ spa_log_trace(this->log, "read socket data size:%d decoded frames:%d dt:%d dms",
+ size_read, decoded / port->frame_size,
+ (int)(dt / 100000));
+
+ return 0;
+
+stop:
+ return 1;
+}
+
+static int setup_matching(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ if (this->position && port->rate_match) {
+ port->rate_match->rate = 1 / port->buffer.corr;
+
+ this->matching = this->following;
+ this->resampling = this->matching ||
+ (port->current_format.info.raw.rate != this->position->clock.rate.denom);
+ } else {
+ this->matching = false;
+ this->resampling = false;
+ }
+
+ if (port->rate_match)
+ SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
+
+ return 0;
+}
+
+static int produce_buffer(struct impl *this);
+
+static void sco_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s",
+ spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ setup_matching(this);
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = port->buffer.corr;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ if (port->io) {
+ int status = produce_buffer(this);
+ spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+ }
+
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_add_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+
+ spa_bt_sco_io_set_source_cb(this->transport->sco_io, sco_source_cb, this);
+
+ return 0;
+}
+
+static int do_start(struct impl *this)
+{
+ struct port *port = &this->port;
+ bool do_accept;
+ int res;
+
+ /* Don't do anything if the node has already started */
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d",
+ this, this->following);
+
+ /* Make sure the transport is valid */
+ spa_return_val_if_fail (this->transport != NULL, -EIO);
+
+ /* Do accept if Gateway; otherwise do connect for Head Unit */
+ do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ /* acquire the socket fd (false -> connect | true -> accept) */
+ if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0)
+ return res;
+
+ /* Reset the buffers and sample count */
+ reset_buffers(port);
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+ if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+ port->frame_size, port->current_format.info.raw.rate,
+ this->quantum_limit, this->quantum_limit)) < 0)
+ return res;
+
+ /* Init mSBC if needed */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ sbc_init_msbc(&this->msbc, 0);
+ /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */
+ this->msbc.endian = SBC_LE;
+ this->msbc_seq_initialized = false;
+
+ this->msbc_buffer_pos = 0;
+ }
+
+ /* Start socket i/o */
+ if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0)
+ goto fail;
+ spa_loop_invoke(this->data_loop, do_add_source, 0, NULL, 0, true, this);
+
+ /* Start timer */
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = sco_on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ setup_matching(this);
+ set_timers(this);
+
+ /* Set the started flag */
+ this->started = true;
+
+ return 0;
+
+fail:
+ spa_bt_transport_release(this->transport);
+ return res;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->transport && this->transport->sco_io)
+ spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL);
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ struct port *port = &this->port;
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_debug(this->log, "sco-source %p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->transport) {
+ /* Release the transport; it is responsible for closing the fd */
+ res = spa_bt_transport_release(this->transport);
+ }
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ static const struct spa_dict_item hu_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Source" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+ };
+ const struct spa_dict_item ag_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Stream/Output/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : "HSP/HFP") },
+ { SPA_KEY_MEDIA_ROLE, "Communication" },
+ };
+ bool is_ag = this->transport && (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = is_ag ?
+ &SPA_DICT_INIT_ARRAY(ag_node_info_items) :
+ &SPA_DICT_INIT_ARRAY(hu_node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (result.index > 0)
+ return 0;
+ if (this->transport == NULL)
+ return -EIO;
+
+ /* set the info structure */
+ struct spa_audio_info_raw info = { 0, };
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ /* CVSD format has a rate of 8kHz
+ * MSBC format has a rate of 16kHz */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC)
+ info.rate = 16000;
+ else
+ info.rate = 8000;
+
+ /* build the param */
+ param = spa_format_audio_raw_build(&b, id, &info);
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels != 1)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels * 2;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+ port = &this->port;
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+ struct port *port = &this->port;
+ uint32_t samples;
+
+ if (SPA_LIKELY(port->rate_match) && this->resampling) {
+ samples = port->rate_match->size;
+ } else {
+ if (SPA_LIKELY(this->position))
+ samples = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else
+ samples = 1024;
+ }
+
+ if (SPA_LIKELY(this->position))
+ *duration = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else if (SPA_LIKELY(this->clock))
+ *duration = this->clock->duration * port->current_format.info.raw.rate
+ / this->clock->rate.denom;
+ else
+ *duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+ return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t duration;
+ const uint32_t samples = get_samples(this, &duration);
+ void *buf;
+ uint32_t avail;
+
+ spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+ setup_matching(this);
+
+ buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+ /* copy data to buffers */
+ if (!spa_list_is_empty(&port->free) && avail > 0) {
+ struct buffer *buffer;
+ struct spa_data *datas;
+ uint32_t data_size;
+
+ data_size = samples * port->frame_size;
+
+ avail = SPA_MIN(avail, data_size);
+
+ spa_bt_decode_buffer_read(&port->buffer, avail);
+
+ buffer = spa_list_first(&port->free, struct buffer, link);
+ spa_list_remove(&buffer->link);
+
+ spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+ datas = buffer->buf->datas;
+
+ spa_assert(datas[0].maxsize >= data_size);
+
+ datas[0].chunk->offset = 0;
+ datas[0].chunk->size = avail;
+ datas[0].chunk->stride = port->frame_size;
+ memcpy(datas[0].data, buf, avail);
+
+ /* ready buffer if full */
+ spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+ spa_list_append(&port->ready, &buffer->link);
+ }
+}
+
+static int produce_buffer(struct impl *this)
+{
+ struct buffer *buffer;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+
+ if (io == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Handle buffering */
+ process_buffering(this);
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outstanding = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Follower produces buffers here, driver in timeout */
+ if (this->following)
+ return produce_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_bt_decode_buffer_clear(&this->port.buffer);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ /* set the node info */
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+
+ this->quantum_limit = 8192;
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with hsp/hfp" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_sco_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_SCO_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/test-midi.c b/spa/plugins/bluez5/test-midi.c
new file mode 100644
index 0000000..8e517aa
--- /dev/null
+++ b/spa/plugins/bluez5/test-midi.c
@@ -0,0 +1,299 @@
+#include <spa/utils/defs.h>
+
+#include "midi.h"
+
+#define TIME_HI(v) (0x80 | ((v >> 7) & 0x3f))
+#define TIME_LO(v) (0x80 | (v & 0x7f))
+
+struct event {
+ uint16_t time_msec;
+ size_t size;
+ const uint8_t *data;
+};
+
+struct packet {
+ size_t size;
+ const uint8_t *data;
+};
+
+struct test_info {
+ const struct packet *packets;
+ const struct event *events;
+ unsigned int i;
+};
+
+static const struct packet midi_1_packets[] = {
+ {
+ .size = 27,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ /* event 1 */
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ /* event 2: running status */
+ 0x03, 0x04,
+ /* event 3: running status with timestamp */
+ TIME_LO(0x1235), 0x05, 0x06,
+ /* event 4 */
+ TIME_LO(0x1236), 0xf8,
+ /* event 5: sysex */
+ TIME_LO(0x1237), 0xf0, 0x0a, 0x0b, 0x0c,
+ /* event 6: realtime event inside sysex */
+ TIME_LO(0x1238), 0xff,
+ /* event 5 continues */
+ 0x0d, 0x0e, TIME_LO(0x1239), 0xf7,
+ /* event 6: sysex */
+ TIME_LO(0x1240), 0xf0, 0x10, 0x11,
+ /* packet end in middle of sysex */
+ },
+ },
+ {
+ .size = 7,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1241),
+ /* event 6: continued from previous packet */
+ 0x12, TIME_LO(0x1241), 0xf7,
+ /* event 7 */
+ TIME_LO(0x1242), 0xf1, 0x13,
+ }
+ },
+ {0}
+};
+
+static const struct event midi_1_events[] = {
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x01, 0x02 } },
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x03, 0x04 } },
+ { 0x1235, 3, (uint8_t[]) { 0xa0, 0x05, 0x06 } },
+ { 0x1236, 1, (uint8_t[]) { 0xf8 } },
+ /* realtime event inside sysex come before it */
+ { 0x1238, 1, (uint8_t[]) { 0xff } },
+ /* sysex timestamp indicates the end time; sysex contains the end marker */
+ { 0x1239, 7, (uint8_t[]) { 0xf0, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf7 } },
+ { 0x1241, 5, (uint8_t[]) { 0xf0, 0x10, 0x11, 0x12, 0xf7 } },
+ { 0x1242, 2, (uint8_t[]) { 0xf1, 0x13 } },
+ {0}
+};
+
+static const struct packet midi_1_packets_mtu14[] = {
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ 0x03, 0x04,
+ /* output Apple-style BLE; running status only for coincident time */
+ TIME_LO(0x1235), 0xa0, 0x05, 0x06,
+ },
+ },
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1236),
+ TIME_LO(0x1236), 0xf8,
+ TIME_LO(0x1238), 0xff,
+ TIME_LO(0x1239), 0xf0, 0x0a, 0x0b, 0x0c, 0x0d,
+ },
+ },
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1239),
+ 0x0e, TIME_LO(0x1239), 0xf7,
+ TIME_LO(0x1241), 0xf0, 0x10, 0x11, 0x12, TIME_LO(0x1241), 0xf7
+ },
+ },
+ {
+ .size = 4,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1242),
+ TIME_LO(0x1242), 0xf1, 0x13
+ },
+ },
+ {0}
+};
+
+static const struct packet midi_2_packets[] = {
+ {
+ .size = 9,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ /* event 1 */
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ /* event 2: timestamp low bits rollover */
+ TIME_LO(0x12b3), 0xa0, 0x03, 0x04,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x18b3),
+ /* event 3: timestamp high bits jump */
+ TIME_LO(0x18b3), 0xa0, 0x05, 0x06,
+ },
+ },
+ {0}
+};
+
+static const struct event midi_2_events[] = {
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x01, 0x02 } },
+ { 0x12b3, 3, (uint8_t[]) { 0xa0, 0x03, 0x04 } },
+ { 0x18b3, 3, (uint8_t[]) { 0xa0, 0x05, 0x06 } },
+ {0}
+};
+
+static const struct packet midi_2_packets_mtu11[] = {
+ /* Small MTU: only room for one event per packet */
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234), TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x12b3), TIME_LO(0x12b3), 0xa0, 0x03, 0x04,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x18b3), TIME_LO(0x18b3), 0xa0, 0x05, 0x06,
+ },
+ },
+ {0}
+};
+
+
+static void check_event(void *user_data, uint16_t time, uint8_t *event, size_t event_size)
+{
+ struct test_info *info = user_data;
+ const struct event *ev = &info->events[info->i];
+
+ spa_assert_se(ev->size > 0);
+ spa_assert_se(ev->time_msec == time);
+ spa_assert_se(ev->size == event_size);
+ spa_assert_se(memcmp(event, ev->data, ev->size) == 0);
+
+ ++info->i;
+}
+
+static void check_parser(struct test_info *info)
+{
+ struct spa_bt_midi_parser parser;
+ int res;
+ int i;
+
+ info->i = 0;
+
+ spa_bt_midi_parser_init(&parser);
+ for (i = 0; info->packets[i].size > 0; ++i) {
+ res = spa_bt_midi_parser_parse(&parser,
+ info->packets[i].data, info->packets[i].size,
+ false, check_event, info);
+ spa_assert_se(res == 0);
+ }
+ spa_assert_se(info->events[info->i].size == 0);
+}
+
+static void check_writer(struct test_info *info, unsigned int mtu)
+{
+ struct spa_bt_midi_writer writer;
+ struct spa_bt_midi_parser parser;
+ unsigned int i, packet;
+ void SPA_UNUSED *buf = writer.buf;
+
+ spa_bt_midi_parser_init(&parser);
+ spa_bt_midi_writer_init(&writer, mtu);
+
+ packet = 0;
+ info->i = 0;
+
+ for (i = 0; info->events[i].size > 0; ++i) {
+ const struct event *ev = &info->events[i];
+ bool last = (info->events[i+1].size == 0);
+ int res;
+
+ do {
+ res = spa_bt_midi_writer_write(&writer,
+ ev->time_msec * SPA_NSEC_PER_MSEC, ev->data, ev->size);
+ spa_assert_se(res >= 0);
+ if (res || last) {
+ int r;
+
+ spa_assert_se(info->packets[packet].size > 0);
+ spa_assert_se(writer.size == info->packets[packet].size);
+ spa_assert_se(memcmp(writer.buf, info->packets[packet].data, writer.size) == 0);
+ ++packet;
+
+ /* Test roundtrip */
+ r = spa_bt_midi_parser_parse(&parser, writer.buf, writer.size,
+ false, check_event, info);
+ spa_assert_se(r == 0);
+ }
+ } while (res);
+ }
+
+ spa_assert_se(info->packets[packet].size == 0);
+ spa_assert_se(info->events[info->i].size == 0);
+}
+
+static void test_midi_parser_1(void)
+{
+ struct test_info info = {
+ .packets = midi_1_packets,
+ .events = midi_1_events,
+ };
+
+ check_parser(&info);
+}
+
+static void test_midi_parser_2(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets,
+ .events = midi_2_events,
+ };
+
+ check_parser(&info);
+}
+
+static void test_midi_writer_1(void)
+{
+ struct test_info info = {
+ .packets = midi_1_packets_mtu14,
+ .events = midi_1_events,
+ };
+
+ check_writer(&info, 14);
+}
+
+static void test_midi_writer_2(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets,
+ .events = midi_2_events,
+ };
+
+ check_writer(&info, 23);
+ check_writer(&info, 12);
+}
+
+static void test_midi_writer_3(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets_mtu11,
+ .events = midi_2_events,
+ };
+
+ check_writer(&info, 11);
+}
+
+int main(void)
+{
+ test_midi_parser_1();
+ test_midi_parser_2();
+ test_midi_writer_1();
+ test_midi_writer_2();
+ test_midi_writer_3();
+ return 0;
+}
diff --git a/spa/plugins/bluez5/upower.c b/spa/plugins/bluez5/upower.c
new file mode 100644
index 0000000..23a637a
--- /dev/null
+++ b/spa/plugins/bluez5/upower.c
@@ -0,0 +1,311 @@
+/* Spa Bluez5 UPower proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <spa/utils/string.h>
+
+#include "upower.h"
+
+#define UPOWER_SERVICE "org.freedesktop.UPower"
+#define UPOWER_DEVICE_INTERFACE UPOWER_SERVICE ".Device"
+#define UPOWER_DISPLAY_DEVICE_OBJECT "/org/freedesktop/UPower/devices/DisplayDevice"
+
+struct impl {
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ DBusConnection *conn;
+
+ bool filters_added;
+
+ void *user_data;
+ void (*set_battery_level)(unsigned int level, void *user_data);
+};
+
+static DBusHandlerResult upower_parse_percentage(struct impl *this, DBusMessageIter *variant_i)
+{
+ double percentage;
+ unsigned int battery_level;
+
+ dbus_message_iter_get_basic(variant_i, &percentage);
+ spa_log_debug(this->log, "Battery level: %f %%", percentage);
+
+ battery_level = (unsigned int) round(percentage / 20.0);
+ this->set_battery_level(battery_level, this->user_data);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void upower_get_percentage_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, variant_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get percentage from UPower: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "v")) {
+ spa_log_error(backend->log, "Invalid arguments in Get() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &variant_i);
+ upower_parse_percentage(backend, &variant_i);
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void upower_clean(struct impl *this)
+{
+ this->set_battery_level(0, this->user_data);
+}
+
+static DBusHandlerResult upower_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto finish;
+ }
+
+ if (spa_streq(name, UPOWER_SERVICE)) {
+ if (old_owner && *old_owner) {
+ spa_log_debug(this->log, "UPower daemon disappeared (%s)", old_owner);
+ upower_clean(this);
+ }
+
+ if (new_owner && *new_owner) {
+ DBusPendingCall *call;
+ static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+ static const char* percentage_property = "Percentage";
+
+ spa_log_debug(this->log, "UPower daemon appeared (%s)", new_owner);
+
+ m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+ if (m == NULL)
+ goto finish;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+ DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+ dbus_connection_send_with_reply(this->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+ dbus_message_unref(m);
+ }
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i, props_i;
+ const char *interface;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ path = dbus_message_get_path(m);
+
+ if (spa_streq(interface, UPOWER_DEVICE_INTERFACE)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+
+ while (dbus_message_iter_get_arg_type(&props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if(spa_streq(key, "Percentage"))
+ upower_parse_percentage(this, &value_i);
+
+ dbus_message_iter_next(&props_i);
+ }
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, upower_filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" UPOWER_SERVICE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" UPOWER_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "',"
+ "path='" UPOWER_DISPLAY_DEVICE_OBJECT "',arg0='" UPOWER_DEVICE_INTERFACE "'", &err);
+
+ this->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+ "org.freedesktop.DBus", "NameHasOwner");
+ if (m == NULL)
+ return false;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(this->log, "NameHasOwner failed for %s", service);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_BOOLEAN, &success,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+
+ return success;
+}
+
+void *upower_register(struct spa_log *log,
+ void *dbus_connection,
+ void (*set_battery_level)(unsigned int level, void *user_data),
+ void *user_data)
+{
+ struct impl *this;
+
+ spa_assert(log);
+ spa_assert(dbus_connection);
+ spa_assert(set_battery_level);
+ spa_assert(user_data);
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+ this->conn = dbus_connection;
+ this->set_battery_level = set_battery_level;
+ this->user_data = user_data;
+
+ if (add_filters(this) < 0) {
+ goto fail4;
+ }
+
+ if (is_dbus_service_available(this, UPOWER_SERVICE)) {
+ DBusMessage *m;
+ DBusPendingCall *call;
+ static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+ static const char* percentage_property = "Percentage";
+
+ m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+ if (m == NULL)
+ goto fail4;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+ DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+ dbus_connection_send_with_reply(this->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+ dbus_message_unref(m);
+ }
+
+ return this;
+
+fail4:
+ free(this);
+ return NULL;
+}
+
+void upower_unregister(void *data)
+{
+ struct impl *this = data;
+
+ if (this->filters_added) {
+ dbus_connection_remove_filter(this->conn, upower_filter_cb, this);
+ this->filters_added = false;
+ }
+ free(this);
+}
diff --git a/spa/plugins/bluez5/upower.h b/spa/plugins/bluez5/upower.h
new file mode 100644
index 0000000..9ebd751
--- /dev/null
+++ b/spa/plugins/bluez5/upower.h
@@ -0,0 +1,36 @@
+/* Spa Bluez5 UPower proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_UPOWER_H_
+#define SPA_BLUEZ5_UPOWER_H_
+
+#include "defs.h"
+
+void *upower_register(struct spa_log *log,
+ void *dbus_connection,
+ void (*set_battery_level)(unsigned int level, void *user_data),
+ void *user_data);
+void upower_unregister(void *data);
+
+#endif \ No newline at end of file