mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
bluez5: add hardware volume support
Add necessary apis to bluez transport. Add A2DP(AVRCP) absolute volume support. Source volume can only update to adapter node but not from due to AG nodes don't have route. Since A2DP/HSP/HFP volume is already percentage itself, it has been mapped to pulseaudio volume then converting to linear volume.
This commit is contained in:
parent
f27ad659f8
commit
80f6ddf526
4 changed files with 508 additions and 64 deletions
|
|
@ -81,7 +81,12 @@ static void reset_props(struct props *props)
|
|||
props->codec = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -90,9 +95,9 @@ struct node {
|
|||
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 impl;
|
||||
struct dynamic_node
|
||||
{
|
||||
struct impl *impl;
|
||||
|
|
@ -226,6 +231,101 @@ static const char *get_codec_name(struct spa_bt_transport *t)
|
|||
return get_hfp_codec_name(t->codec);
|
||||
}
|
||||
|
||||
static void transport_destroy(void *userdata)
|
||||
{
|
||||
struct node *node = userdata;
|
||||
node->transport = NULL;
|
||||
}
|
||||
|
||||
static void emit_volume(struct impl *this, struct node *node)
|
||||
{
|
||||
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->soft_volumes),
|
||||
SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
|
||||
SPA_TYPE_Id, node->n_channels, node->channels));
|
||||
event = spa_pod_builder_pop(&b, &f[0]);
|
||||
|
||||
spa_device_emit_event(&this->hooks, event);
|
||||
}
|
||||
|
||||
static void emit_info(struct impl *this, bool full);
|
||||
|
||||
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 void volume_changed(void *userdata)
|
||||
{
|
||||
struct node *node = userdata;
|
||||
struct impl *impl = node->impl;
|
||||
struct spa_bt_transport_volume *t_volume;
|
||||
float prev_hw_volume;
|
||||
|
||||
if (!node->transport)
|
||||
return;
|
||||
|
||||
/* PW is the controller for remote device. */
|
||||
if (impl->profile != DEVICE_PROFILE_A2DP
|
||||
&& impl->profile != DEVICE_PROFILE_HSP_HFP)
|
||||
return;
|
||||
|
||||
t_volume = &node->transport->volumes[node->id];
|
||||
|
||||
if (!t_volume->active)
|
||||
return;
|
||||
|
||||
prev_hw_volume = node_get_hw_volume(node);
|
||||
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;
|
||||
}
|
||||
|
||||
node_update_soft_volumes(node, t_volume->volume);
|
||||
|
||||
impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
||||
impl->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
||||
emit_info(impl, false);
|
||||
|
||||
/* It sometimes flips volume to over 100% in pavucontrol silder
|
||||
* if volume is emited before route info emitting while node
|
||||
* volumes are not identical to route volumes. Not sure why. */
|
||||
emit_volume(impl, node);
|
||||
}
|
||||
|
||||
static const struct spa_bt_transport_events transport_events = {
|
||||
SPA_VERSION_BT_DEVICE_EVENTS,
|
||||
.destroy = transport_destroy,
|
||||
.volume_changed = volume_changed,
|
||||
};
|
||||
|
||||
static void emit_node(struct impl *this, struct spa_bt_transport *t,
|
||||
uint32_t id, const char *factory_name)
|
||||
{
|
||||
|
|
@ -270,10 +370,15 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
|
|||
this->nodes[id].volumes[i] = this->nodes[id].volumes[i % this->nodes[id].n_channels];
|
||||
}
|
||||
|
||||
this->nodes[id].impl = this;
|
||||
this->nodes[id].active = true;
|
||||
this->nodes[id].n_channels = t->n_channels;
|
||||
memcpy(this->nodes[id].channels, t->channels,
|
||||
t->n_channels * sizeof(uint32_t));
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +407,7 @@ 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,
|
||||
|
|
@ -327,10 +433,55 @@ static void dynamic_node_transport_state_changed(void *data,
|
|||
}
|
||||
}
|
||||
|
||||
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 = SPA_FLAG_CLEAR(node->id, DYNAMIC_NODE_ID_FLAG), volume_id;
|
||||
|
||||
/* Remote device is the controller */
|
||||
if (!node->transport || impl->profile != DEVICE_PROFILE_AG)
|
||||
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,
|
||||
|
|
@ -452,16 +603,19 @@ static void emit_info(struct impl *this, bool full)
|
|||
|
||||
static void emit_remove_nodes(struct impl *this)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
remove_dynamic_node (&this->dyn_a2dp_source);
|
||||
remove_dynamic_node (&this->dyn_sco_source);
|
||||
remove_dynamic_node (&this->dyn_sco_sink);
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (this->nodes[i].active) {
|
||||
for (uint32_t i = 0; i < 2; i++) {
|
||||
struct node * node = &this->nodes[i];
|
||||
if (node->transport) {
|
||||
spa_hook_remove(&node->transport_listener);
|
||||
node->transport = NULL;
|
||||
}
|
||||
if (node->active) {
|
||||
spa_device_emit_object_info(&this->hooks, i, NULL);
|
||||
this->nodes[i].active = false;
|
||||
node->active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1086,6 +1240,11 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
|
|||
|
||||
if (dev != SPA_ID_INVALID) {
|
||||
struct node *node = &this->nodes[dev];
|
||||
struct spa_bt_transport_volume *t_volume;
|
||||
|
||||
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);
|
||||
|
|
@ -1096,10 +1255,16 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
|
|||
spa_pod_builder_prop(b, SPA_PROP_mute, 0);
|
||||
spa_pod_builder_bool(b, node->mute);
|
||||
|
||||
spa_pod_builder_prop(b, SPA_PROP_channelVolumes, 0);
|
||||
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);
|
||||
|
|
@ -1331,39 +1496,34 @@ static int impl_enum_params(void *object, int seq,
|
|||
|
||||
static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes)
|
||||
{
|
||||
struct spa_event *event;
|
||||
uint8_t buffer[4096];
|
||||
struct spa_pod_builder b = { 0 };
|
||||
struct spa_pod_frame f[1];
|
||||
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]);
|
||||
spa_log_debug(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])
|
||||
++changed;
|
||||
if (node->volumes[i] == volumes[i % n_volumes])
|
||||
continue;
|
||||
++changed;
|
||||
node->volumes[i] = volumes[i % n_volumes];
|
||||
}
|
||||
|
||||
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_channelMap, SPA_POD_Array(sizeof(uint32_t),
|
||||
SPA_TYPE_Id, node->n_channels, node->channels));
|
||||
event = spa_pod_builder_pop(&b, &f[0]);
|
||||
t_volume = node->transport ? &node->transport->volumes[node->id]: NULL;
|
||||
|
||||
spa_device_emit_event(&this->hooks, event);
|
||||
if (t_volume && t_volume->active) {
|
||||
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 {
|
||||
for (uint32_t i = 0; i < node->n_channels; ++i)
|
||||
node->soft_volumes[i] = node->volumes[i];
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
|
@ -1551,6 +1711,8 @@ static int impl_set_param(void *object,
|
|||
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
||||
}
|
||||
emit_info(this, false);
|
||||
/* See volume_changed(void *) */
|
||||
emit_volume(this, node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -1629,6 +1791,8 @@ 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue