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
|
|
@ -150,6 +150,10 @@ struct spa_bt_a2dp_codec_switch {
|
|||
#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);
|
||||
|
||||
|
|
@ -409,6 +413,17 @@ static const struct a2dp_codec *a2dp_endpoint_to_codec(const char *endpoint)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static int a2dp_endpoint_to_profile(const char *endpoint)
|
||||
{
|
||||
|
||||
if (strstr(endpoint, A2DP_SINK_ENDPOINT "/") == endpoint)
|
||||
return SPA_BT_PROFILE_A2DP_SOURCE;
|
||||
else if (strstr(endpoint, A2DP_SOURCE_ENDPOINT "/") == endpoint)
|
||||
return SPA_BT_PROFILE_A2DP_SINK;
|
||||
else
|
||||
return SPA_BT_PROFILE_NULL;
|
||||
}
|
||||
|
||||
static bool is_a2dp_codec_enabled(struct spa_bt_monitor *monitor, const struct a2dp_codec *codec)
|
||||
{
|
||||
if (!monitor->enable_sbc_xq && codec->feature_flag != NULL &&
|
||||
|
|
@ -1449,6 +1464,13 @@ struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor,
|
|||
return t;
|
||||
}
|
||||
|
||||
static void transport_sync_volume(struct spa_bt_transport *transport)
|
||||
{
|
||||
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;
|
||||
|
|
@ -1459,6 +1481,8 @@ void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1474,6 +1498,7 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
|
|||
|
||||
spa_bt_transport_emit_destroy(transport);
|
||||
|
||||
spa_bt_transport_stop_volume_timer(transport);
|
||||
spa_bt_transport_stop_release_timer(transport);
|
||||
|
||||
if (transport->sco_io) {
|
||||
|
|
@ -1574,6 +1599,46 @@ int spa_bt_device_release_transports(struct spa_bt_device *device)
|
|||
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;
|
||||
|
|
@ -1593,45 +1658,65 @@ static void spa_bt_transport_release_timer_event(struct spa_source *source)
|
|||
|
||||
static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport)
|
||||
{
|
||||
struct spa_bt_monitor *monitor = transport->monitor;
|
||||
struct itimerspec ts;
|
||||
|
||||
if (transport->release_timer.data == NULL) {
|
||||
transport->release_timer.data = transport;
|
||||
transport->release_timer.func = spa_bt_transport_release_timer_event;
|
||||
transport->release_timer.fd = spa_system_timerfd_create(
|
||||
monitor->main_system, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
|
||||
transport->release_timer.mask = SPA_IO_IN;
|
||||
transport->release_timer.rmask = 0;
|
||||
spa_loop_add_source(monitor->main_loop, &transport->release_timer);
|
||||
}
|
||||
ts.it_value.tv_sec = SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC / SPA_MSEC_PER_SEC;
|
||||
ts.it_value.tv_nsec = (SCO_TRANSPORT_RELEASE_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, transport->release_timer.fd, 0, &ts, NULL);
|
||||
return 0;
|
||||
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)
|
||||
{
|
||||
struct spa_bt_monitor *monitor = transport->monitor;
|
||||
struct itimerspec ts;
|
||||
|
||||
if (transport->release_timer.data == NULL)
|
||||
return 0;
|
||||
|
||||
spa_loop_remove_source(monitor->main_loop, &transport->release_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, transport->release_timer.fd, 0, &ts, NULL);
|
||||
spa_system_close(monitor->main_system, transport->release_timer.fd);
|
||||
transport->release_timer.data = NULL;
|
||||
return 0;
|
||||
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;
|
||||
|
||||
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
|
||||
return;
|
||||
|
||||
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);
|
||||
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) {
|
||||
|
|
@ -1762,6 +1847,29 @@ static int transport_update_props(struct spa_bt_transport *transport,
|
|||
}
|
||||
}
|
||||
else if (strcmp(key, "Volume") == 0) {
|
||||
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 (strcmp(key, "Delay") == 0) {
|
||||
uint16_t value;
|
||||
|
|
@ -1780,6 +1888,79 @@ static int transport_update_props(struct spa_bt_transport *transport,
|
|||
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, NAME": 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)
|
||||
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;
|
||||
|
|
@ -1834,6 +2015,8 @@ static int transport_acquire(void *data, bool optional)
|
|||
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);
|
||||
|
||||
transport_sync_volume(transport);
|
||||
|
||||
finish:
|
||||
dbus_message_unref(r);
|
||||
return ret;
|
||||
|
|
@ -1894,6 +2077,7 @@ 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 append_basic_array_variant_dict_entry(DBusMessageIter *dict, int key_type_int, void* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
|
||||
|
|
@ -2296,6 +2480,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
|||
struct spa_bt_transport *transport;
|
||||
bool is_new = false;
|
||||
const struct a2dp_codec *codec;
|
||||
int profile;
|
||||
|
||||
if (!dbus_message_has_signature(m, "oa{sv}")) {
|
||||
spa_log_warn(monitor->log, "invalid SetConfiguration() signature");
|
||||
|
|
@ -2303,6 +2488,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
|||
}
|
||||
endpoint = dbus_message_get_path(m);
|
||||
|
||||
profile = a2dp_endpoint_to_profile(endpoint);
|
||||
codec = a2dp_endpoint_to_codec(endpoint);
|
||||
if (codec == NULL) {
|
||||
spa_log_warn(monitor->log, "unknown SetConfiguration() codec");
|
||||
|
|
@ -2323,7 +2509,25 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* PW is the rendering device so it's responsible for reporting hardware volume. */
|
||||
if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
|
||||
transport->volumes[SPA_BT_VOLUME_ID_RX].active = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
transport->a2dp_codec = codec;
|
||||
transport_update_props(transport, &it[1], NULL);
|
||||
|
||||
|
|
@ -2355,6 +2559,9 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
|||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include <spa/support/dbus.h>
|
||||
#include <spa/support/log.h>
|
||||
#include <spa/support/loop.h>
|
||||
|
|
@ -461,6 +463,14 @@ void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void
|
|||
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,
|
||||
|
|
@ -474,6 +484,7 @@ struct spa_bt_transport_events {
|
|||
void (*destroy) (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 {
|
||||
|
|
@ -482,9 +493,20 @@ struct spa_bt_transport_implementation {
|
|||
|
||||
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;
|
||||
|
|
@ -502,18 +524,24 @@ struct spa_bt_transport {
|
|||
uint32_t n_channels;
|
||||
uint32_t channels[64];
|
||||
|
||||
struct spa_bt_transport_volume volumes[SPA_BT_VOLUME_ID_TERM];
|
||||
|
||||
int acquire_refcount;
|
||||
int fd;
|
||||
uint16_t read_mtu;
|
||||
uint16_t write_mtu;
|
||||
uint16_t delay;
|
||||
void *user_data;
|
||||
|
||||
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);
|
||||
|
|
@ -534,6 +562,7 @@ int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *
|
|||
m, v, ##__VA_ARGS__)
|
||||
#define spa_bt_transport_emit_destroy(t) spa_bt_transport_emit(t, destroy, 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)
|
||||
|
|
@ -550,7 +579,8 @@ int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *
|
|||
res; \
|
||||
})
|
||||
|
||||
#define spa_bt_transport_destroy(t) spa_bt_transport_impl(t, destroy, 0)
|
||||
#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)
|
||||
{
|
||||
|
|
@ -564,6 +594,49 @@ static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(con
|
|||
return SPA_BT_TRANSPORT_STATE_IDLE;
|
||||
}
|
||||
|
||||
#define DEFAULT_AG_VOLUME 1.0f
|
||||
#define DEFAULT_RX_VOLUME 1.0f
|
||||
#define DEFAULT_TX_VOLUME 0.064f /* pa_sw_volume_to_linear(0.4 * PA_VOLUME_NORM) */
|
||||
|
||||
#define PA_VOLUME_MUTED ((uint32_t) 0u)
|
||||
#define PA_VOLUME_NORM ((uint32_t) 0x10000u)
|
||||
#define PA_VOLUME_MAX ((uint32_t) UINT32_MAX/2)
|
||||
|
||||
static inline uint32_t pa_sw_volume_from_linear(double v)
|
||||
{
|
||||
if (v <= 0.0)
|
||||
return PA_VOLUME_MUTED;
|
||||
return SPA_CLAMP(
|
||||
(uint64_t) lround(cbrt(v) * PA_VOLUME_NORM),
|
||||
PA_VOLUME_MUTED, PA_VOLUME_MAX);
|
||||
}
|
||||
|
||||
static inline double pa_sw_volume_to_linear(uint32_t v)
|
||||
{
|
||||
double f;
|
||||
if (v <= PA_VOLUME_MUTED)
|
||||
return 0.0;
|
||||
if (v == PA_VOLUME_NORM)
|
||||
return 1.0;
|
||||
f = ((double) v / PA_VOLUME_NORM);
|
||||
return f*f*f;
|
||||
}
|
||||
|
||||
/* AVRCP/HSP volume is considered as percentage, so map it to pulseaudio volume. */
|
||||
static inline uint32_t spa_bt_volume_linear_to_hw(double v, uint32_t hw_volume_max)
|
||||
{
|
||||
return SPA_CLAMP(
|
||||
pa_sw_volume_from_linear(v) * hw_volume_max / PA_VOLUME_NORM,
|
||||
0u, hw_volume_max);
|
||||
}
|
||||
|
||||
static inline double spa_bt_volume_hw_to_linear(uint32_t v, uint32_t hw_volume_max)
|
||||
{
|
||||
return SPA_CLAMP(
|
||||
pa_sw_volume_to_linear(v * PA_VOLUME_NORM / hw_volume_max),
|
||||
0.0, 1.0);
|
||||
}
|
||||
|
||||
struct spa_bt_backend_implementation {
|
||||
#define SPA_VERSION_BT_BACKEND_IMPLEMENTATION 0
|
||||
uint32_t version;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
bluez5_deps = [ dbus_dep, sbc_dep, bluez_dep ]
|
||||
bluez5_deps = [ mathlib, dbus_dep, sbc_dep, bluez_dep ]
|
||||
foreach dep: bluez5_deps
|
||||
if not dep.found()
|
||||
subdir_done()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue