diff options
Diffstat (limited to '')
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, ¶m)) != 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, ¶m)) != 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, ¶m)) != 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, ¶m)) != 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 |