diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c index 6fa1afca7..c318ebb20 100644 --- a/spa/plugins/bluez5/backend-hsphfpd.c +++ b/spa/plugins/bluez5/backend-hsphfpd.c @@ -51,6 +51,8 @@ struct impl { struct spa_dbus *dbus; DBusConnection *conn; + const struct spa_bt_quirks *quirks; + struct spa_list endpoint_list; bool endpoints_listed; @@ -1485,6 +1487,7 @@ static const struct spa_bt_backend_implementation backend_impl = { 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) { @@ -1504,11 +1507,12 @@ struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor, spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); 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.msbc-support"))) + if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc"))) backend->msbc_supported = spa_atob(str); else backend->msbc_supported = false; diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 4a97b29c7..61508bf88 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -73,6 +73,8 @@ struct impl { struct spa_source sco; + const struct spa_bt_quirks *quirks; + struct spa_list rfcomm_list; unsigned int defer_setup_enabled:1; }; @@ -1925,6 +1927,7 @@ static const struct spa_bt_backend_implementation backend_impl = { 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) { @@ -1941,6 +1944,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); 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); diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c index f2678ca60..7c6644583 100644 --- a/spa/plugins/bluez5/backend-ofono.c +++ b/spa/plugins/bluez5/backend-ofono.c @@ -54,6 +54,8 @@ struct impl { struct spa_dbus *dbus; DBusConnection *conn; + const struct spa_bt_quirks *quirks; + unsigned int filters_added:1; unsigned int msbc_supported:1; }; @@ -769,6 +771,7 @@ static const struct spa_bt_backend_implementation backend_impl = { 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) { @@ -785,11 +788,12 @@ struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor, spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); 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.msbc-support"))) + if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc"))) backend->msbc_supported = spa_atob(str); else backend->msbc_supported = false; diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 24faf98f5..69f4f9e33 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -88,6 +88,9 @@ struct spa_bt_monitor { struct spa_dict enabled_codecs; unsigned int connection_info_supported:1; + + struct spa_bt_quirks *quirks; + unsigned int enable_sbc_xq:1; unsigned int backend_native_registered:1; unsigned int backend_ofono_registered:1; @@ -3842,6 +3845,8 @@ static int impl_clear(struct spa_handle *handle) monitor->backend_ofono_registered = false; monitor->backend_hsphfpd_registered = false; + spa_bt_quirks_destroy(monitor->quirks); + return 0; } @@ -3986,6 +3991,12 @@ impl_init(const struct spa_handle_factory *factory, return -EINVAL; } + this->quirks = spa_bt_quirks_create(info, this->log); + if (this->quirks == NULL) { + spa_log_error(this->log, NAME ": failed to parse quirk table"); + return -EINVAL; + } + 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"); @@ -4027,16 +4038,16 @@ impl_init(const struct spa_handle_factory *factory, ((tmp = atoi(str)) > 0)) this->default_audio_info.channels = tmp; - if ((str = spa_dict_lookup(info, "bluez5.sbc-xq-support")) != NULL && + if ((str = spa_dict_lookup(info, "bluez5.enable-sbc-xq")) != NULL && spa_atob(str)) this->enable_sbc_xq = true; } register_media_application(this); - this->backend_native = backend_native_new(this, this->conn, info, support, n_support); - this->backend_ofono = backend_ofono_new(this, this->conn, info, support, n_support); - this->backend_hsphfpd = backend_hsphfpd_new(this, this->conn, info, support, n_support); + this->backend_native = backend_native_new(this, this->conn, info, this->quirks, support, n_support); + this->backend_ofono = backend_ofono_new(this, this->conn, info, this->quirks, support, n_support); + this->backend_hsphfpd = backend_hsphfpd_new(this, this->conn, info, this->quirks, support, n_support); if (this->backend_ofono && spa_bt_backend_register_profiles(this->backend_ofono) == 0) this->backend_ofono_registered = true; diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 87e3cadfd..6a5a47cbd 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -666,6 +666,23 @@ static inline double spa_bt_volume_hw_to_linear(uint32_t v, uint32_t hw_volume_m 0.0, 1.0); } +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_SBC_XQ = (1 << 4), +}; + +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); + struct spa_bt_backend_implementation { #define SPA_VERSION_BT_BACKEND_IMPLEMENTATION 0 uint32_t version; @@ -707,6 +724,7 @@ struct spa_bt_backend { 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) { @@ -717,6 +735,7 @@ static inline struct spa_bt_backend *dummy_backend_new(struct spa_bt_monitor *mo 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 @@ -728,6 +747,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, 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 @@ -739,6 +759,7 @@ struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor, 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 diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build index ae7cf9414..5a5df437d 100644 --- a/spa/plugins/bluez5/meson.build +++ b/spa/plugins/bluez5/meson.build @@ -28,6 +28,7 @@ bluez5_sources = ['plugin.c', 'sco-sink.c', 'sco-source.c', 'sco-io.c', + 'quirks.c', 'bluez5-device.c', 'bluez5-dbus.c'] diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c new file mode 100644 index 000000000..bcae542c8 --- /dev/null +++ b/spa/plugins/bluez5/quirks.c @@ -0,0 +1,319 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "a2dp-codecs.h" +#include "defs.h" + +#define NAME "bluez5-quirks" + +struct spa_bt_quirks { + struct spa_log *log; + + int force_msbc; + int force_hw_volume; + int force_sbc_xq; + + 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 }, + { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, + }; + size_t i; + for (i = 0; i < SPA_N_ELEMENTS(feature_keys); ++i) { + if (spa_streq(str, feature_keys[i].key)) + return feature_keys[i].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)-1) > 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)-1) > 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 { + spa_json_parse_string(value, SPA_MIN(len, (int)sizeof(val)-1), val); + 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; +} + +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; + + 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"); + + str = spa_dict_lookup(info, "bluez5.features.kernel"); + this->kernel_rules = str ? strdup(str) : NULL; + str = spa_dict_lookup(info, "bluez5.features.adapter"); + this->adapter_rules = str ? strdup(str) : NULL; + str = spa_dict_lookup(info, "bluez5.features.device"); + this->device_rules = str ? strdup(str) : NULL; + + if (!(this->kernel_rules && this->adapter_rules && this->device_rules)) + spa_log_info(this->log, NAME " failed to find data from 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, NAME ": kernel quirks:%08x", no_features); + *features &= ~no_features; + } + + /* Adapter */ + if (this->adapter_rules) { + 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, NAME ": adapter quirks:%08x", no_features); + *features &= ~no_features; + } + + /* Device */ + if (this->device_rules) { + 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, NAME ": 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); + + return 0; +} diff --git a/src/daemon/media-session.d/bluez-monitor.conf b/src/daemon/media-session.d/bluez-monitor.conf index e68dd45a1..06ba5eb17 100644 --- a/src/daemon/media-session.d/bluez-monitor.conf +++ b/src/daemon/media-session.d/bluez-monitor.conf @@ -7,10 +7,11 @@ properties = { # These features do not work on all headsets, so they are enabled # by default based on the hardware database. They can also be - # forced on/off by the following options: + # forced on/off for all devices by the following options: - #bluez5.force-sbc-xq = true - #bluez5.force-msbc = true + #bluez5.enable-sbc-xq = true + #bluez5.enable-msbc = true + #bluez5.enable-hw-volume = true # See bluez-hardware.conf for the hardware database. @@ -62,10 +63,7 @@ rules = [ #] bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink ] - # Overload mSBC support for native backend and a specific device. - #bluez5.msbc-support = false - - # Hardware volume control (default: [ hfp_ag hsp_ag a2dp_source ]) + # Hardware volume control (default: all) #bluez5.hw-volume = [ # hfp_hf # hsp_hs diff --git a/src/examples/media-session/bluez-monitor.c b/src/examples/media-session/bluez-monitor.c index 57eed8ce2..a0e83d3ca 100644 --- a/src/examples/media-session/bluez-monitor.c +++ b/src/examples/media-session/bluez-monitor.c @@ -49,6 +49,7 @@ #define NAME "bluez5-monitor" #define SESSION_CONF "bluez-monitor.conf" +#define FEATURES_CONF "bluez-hardware.conf" struct device; @@ -715,7 +716,9 @@ int sm_bluez5_monitor_start(struct sm_media_session *session) { int res; struct impl *impl; - const char *str; + const char *key, *str; + struct pw_properties *hw_features = NULL; + void *state = NULL; impl = calloc(1, sizeof(struct impl)); if (impl == NULL) { @@ -731,9 +734,16 @@ int sm_bluez5_monitor_start(struct sm_media_session *session) res = -errno; goto out_free; } + if ((hw_features = pw_properties_new(NULL, NULL)) == NULL) { + res = -errno; + goto out_free; + } if ((res = sm_media_session_load_conf(impl->session, SESSION_CONF, impl->conf)) < 0) pw_log_info("can't load "SESSION_CONF" config: %s", spa_strerror(res)); + if ((res = sm_media_session_load_conf(impl->session, + FEATURES_CONF, hw_features)) < 0) + pw_log_info("can't load "FEATURES_CONF" config: %s", spa_strerror(res)); if ((impl->props = pw_properties_new(NULL, NULL)) == NULL) { res = -errno; @@ -744,14 +754,23 @@ int sm_bluez5_monitor_start(struct sm_media_session *session) pw_properties_set(impl->props, "api.bluez5.connection-info", "true"); + while ((key = pw_properties_iterate(hw_features, &state)) != NULL) { + if (strncmp(key, "bluez5.features.", strlen("bluez5.features.")) != 0) + continue; + if ((str = pw_properties_get(hw_features, key)) != NULL) + pw_properties_set(impl->props, key, str); + } + + pw_properties_free(hw_features); + sm_media_session_add_listener(session, &impl->session_listener, &session_events, impl); - return 0; out_free: pw_properties_free(impl->conf); pw_properties_free(impl->props); + pw_properties_free(hw_features); free(impl); out: return res;