mirror of
https://gitlab.freedesktop.org/pipewire/pipewire
synced 2024-10-14 20:02:38 +00:00
bluez5: add support for HSP/HFP hardware volume control
Only native backend is fulfilled.
This commit is contained in:
parent
80f6ddf526
commit
387f7e327f
|
@ -77,6 +77,7 @@ struct impl {
|
|||
};
|
||||
|
||||
struct transport_data {
|
||||
struct rfcomm *rfcomm;
|
||||
struct spa_source sco;
|
||||
};
|
||||
|
||||
|
@ -86,10 +87,25 @@ enum hfp_hf_state {
|
|||
hfp_hf_cind1,
|
||||
hfp_hf_cind2,
|
||||
hfp_hf_cmer,
|
||||
hfp_hf_slc,
|
||||
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;
|
||||
|
@ -101,6 +117,8 @@ struct rfcomm {
|
|||
enum spa_bt_profile profile;
|
||||
struct spa_source timer;
|
||||
char* path;
|
||||
bool has_volume;
|
||||
struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM];
|
||||
#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
|
||||
unsigned int slc_configured:1;
|
||||
unsigned int codec_negotiation_supported:1;
|
||||
|
@ -109,6 +127,7 @@ struct rfcomm {
|
|||
unsigned int hfp_ag_switching_codec:1;
|
||||
unsigned int hfp_ag_initial_codec_setup:2;
|
||||
enum hfp_hf_state hf_state;
|
||||
enum hsp_hs_state hs_state;
|
||||
unsigned int codec;
|
||||
#endif
|
||||
};
|
||||
|
@ -148,6 +167,7 @@ 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)
|
||||
|
@ -165,6 +185,23 @@ static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm)
|
|||
t->n_channels = 1;
|
||||
t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
|
||||
|
||||
td = t->user_data;
|
||||
td->rfcomm = rfcomm;
|
||||
|
||||
for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) {
|
||||
rfcomm->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID;
|
||||
t->volumes[i].active = rfcomm->volumes[i].active;
|
||||
t->volumes[i].hw_volume_max = SPA_BT_VOLUME_HS_MAX;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);
|
||||
|
||||
finish:
|
||||
|
@ -214,7 +251,7 @@ static void rfcomm_send_cmd(struct spa_source *source, char *data)
|
|||
spa_log_error(backend->log, NAME": RFCOMM write error: %s", strerror(errno));
|
||||
}
|
||||
|
||||
static int rfcomm_send_reply(struct spa_source *source, char *data)
|
||||
static int rfcomm_send_reply(struct spa_source *source, const char *data)
|
||||
{
|
||||
struct rfcomm *rfcomm = source->data;
|
||||
struct impl *backend = rfcomm->backend;
|
||||
|
@ -231,6 +268,30 @@ static int rfcomm_send_reply(struct spa_source *source, char *data)
|
|||
return len;
|
||||
}
|
||||
|
||||
static void rfcomm_emit_volume_changed(struct rfcomm *rfcomm, int id, int hw_volume)
|
||||
{
|
||||
struct spa_bt_transport_volume *t_volume;
|
||||
|
||||
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, NAME": 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 spa_source *source, char* buf)
|
||||
{
|
||||
|
@ -243,16 +304,16 @@ static bool rfcomm_hsp_ag(struct spa_source *source, char* buf)
|
|||
* 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 <= 15) {
|
||||
/* t->speaker_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
|
||||
rfcomm_send_reply(source, "OK");
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf);
|
||||
rfcomm_send_reply(source, "ERROR");
|
||||
}
|
||||
} else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) {
|
||||
if (gain <= 15) {
|
||||
/* t->microphone_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
|
||||
rfcomm_send_reply(source, "OK");
|
||||
} else {
|
||||
rfcomm_send_reply(source, "ERROR");
|
||||
|
@ -267,6 +328,33 @@ static bool rfcomm_hsp_ag(struct spa_source *source, char* buf)
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool rfcomm_send_volume_cmd(struct spa_source *source, int id)
|
||||
{
|
||||
struct rfcomm *rfcomm = source->data;
|
||||
struct spa_bt_transport_volume *t_volume;
|
||||
char *cmd;
|
||||
int hw_volume;
|
||||
|
||||
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)
|
||||
cmd = spa_aprintf("AT+VGM=%u", hw_volume);
|
||||
else if (id == SPA_BT_VOLUME_ID_RX)
|
||||
cmd = spa_aprintf("AT+VGS=%u", hw_volume);
|
||||
else
|
||||
spa_assert_not_reached();
|
||||
|
||||
rfcomm_send_cmd(source, cmd);
|
||||
free(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rfcomm_hsp_hs(struct spa_source *source, char* buf)
|
||||
{
|
||||
struct rfcomm *rfcomm = source->data;
|
||||
|
@ -281,17 +369,29 @@ static bool rfcomm_hsp_hs(struct spa_source *source, char* buf)
|
|||
* 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 <= 15) {
|
||||
/* t->microphone_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf);
|
||||
}
|
||||
} else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
|
||||
if (gain <= 15) {
|
||||
/* t->speaker_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", buf);
|
||||
}
|
||||
} if (strncmp(buf, "\r\nOK\r\n", 6) == 0) {
|
||||
if (rfcomm->hs_state == hsp_hs_init2) {
|
||||
if (rfcomm_send_volume_cmd(&rfcomm->source, 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->source, SPA_BT_VOLUME_ID_TX))
|
||||
rfcomm->hs_state = hsp_hs_vgm;
|
||||
else
|
||||
rfcomm->hs_state = hsp_hs_init1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -363,10 +463,16 @@ static bool rfcomm_hfp_ag(struct spa_source *source, char* buf)
|
|||
unsigned int selected_codec;
|
||||
|
||||
if (sscanf(buf, "AT+BRSF=%u", &features) == 1) {
|
||||
|
||||
unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE;
|
||||
char *cmd;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
a) mSBC support is enabled in config file and
|
||||
|
@ -451,6 +557,7 @@ static bool rfcomm_hfp_ag(struct spa_source *source, char* buf)
|
|||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -492,21 +599,22 @@ static bool rfcomm_hfp_ag(struct spa_source *source, char* buf)
|
|||
}
|
||||
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(source, "OK");
|
||||
if (was_switching_codec)
|
||||
spa_bt_device_emit_codec_switched(rfcomm->device, 0);
|
||||
} else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) {
|
||||
if (gain <= 15) {
|
||||
/* t->microphone_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
|
||||
rfcomm_send_reply(source, "OK");
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", buf);
|
||||
rfcomm_send_reply(source, "ERROR");
|
||||
}
|
||||
} else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) {
|
||||
if (gain <= 15) {
|
||||
/* t->speaker_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
|
||||
rfcomm_send_reply(source, "OK");
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf);
|
||||
|
@ -617,8 +725,8 @@ static bool rfcomm_hfp_hf(struct spa_source *source, char* buf)
|
|||
token = strtok(NULL, separators);
|
||||
gain = atoi(token);
|
||||
|
||||
if (gain <= 15) {
|
||||
/* t->speaker_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", token);
|
||||
}
|
||||
|
@ -627,8 +735,8 @@ static bool rfcomm_hfp_hf(struct spa_source *source, char* buf)
|
|||
token = strtok(NULL, separators);
|
||||
gain = atoi(token);
|
||||
|
||||
if (gain <= 15) {
|
||||
/* t->microphone_gain = gain; */
|
||||
if (gain <= SPA_BT_VOLUME_HS_MAX) {
|
||||
rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
|
||||
} else {
|
||||
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", token);
|
||||
}
|
||||
|
@ -656,7 +764,7 @@ static bool rfcomm_hfp_hf(struct spa_source *source, char* buf)
|
|||
rfcomm->hf_state = hfp_hf_cmer;
|
||||
break;
|
||||
case hfp_hf_cmer:
|
||||
rfcomm->hf_state = hfp_hf_slc;
|
||||
rfcomm->hf_state = hfp_hf_slc1;
|
||||
rfcomm->slc_configured = true;
|
||||
if (!rfcomm->codec_negotiation_supported) {
|
||||
rfcomm->transport = _transport_create(rfcomm);
|
||||
|
@ -668,6 +776,18 @@ static bool rfcomm_hfp_hf(struct spa_source *source, char* buf)
|
|||
spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
|
||||
}
|
||||
}
|
||||
/* Report volume on SLC establishment */
|
||||
if (rfcomm_send_volume_cmd(source, SPA_BT_VOLUME_ID_RX))
|
||||
rfcomm->hf_state = hfp_hf_vgs;
|
||||
break;
|
||||
case hfp_hf_slc2:
|
||||
if (rfcomm_send_volume_cmd(source, 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(source, SPA_BT_VOLUME_ID_TX))
|
||||
rfcomm->hf_state = hfp_hf_vgm;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -985,6 +1105,19 @@ static void sco_listen_event(struct spa_source *source)
|
|||
|
||||
spa_log_debug(backend->log, NAME": transport %p: audio connected", t);
|
||||
|
||||
/* Report initial volume to remote */
|
||||
if (t->profile == SPA_BT_PROFILE_HSP_AG) {
|
||||
if (rfcomm_send_volume_cmd(&rfcomm->source, 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->source, 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;
|
||||
|
||||
|
@ -1043,10 +1176,47 @@ fail_close:
|
|||
return -1;
|
||||
}
|
||||
|
||||
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;
|
||||
char *msg;
|
||||
int value;
|
||||
|
||||
if (!(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT))
|
||||
return -ENOTSUP;
|
||||
|
||||
if (!(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;
|
||||
|
||||
if (id == SPA_BT_VOLUME_ID_RX)
|
||||
msg = spa_aprintf("+VGM: %d", value);
|
||||
else if (id == SPA_BT_VOLUME_ID_TX)
|
||||
msg = spa_aprintf("+VGS: %d", value);
|
||||
else
|
||||
spa_assert_not_reached();
|
||||
|
||||
if (rfcomm->transport)
|
||||
rfcomm_send_reply(&rfcomm->source, msg);
|
||||
|
||||
free(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -1279,6 +1449,12 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
|
|||
rfcomm->source.mask = SPA_IO_IN;
|
||||
rfcomm->source.rmask = 0;
|
||||
|
||||
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);
|
||||
|
@ -1296,6 +1472,11 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
|
|||
}
|
||||
rfcomm->transport = t;
|
||||
|
||||
if (profile == SPA_BT_PROFILE_HSP_AG) {
|
||||
rfcomm->has_volume = true;
|
||||
rfcomm->hs_state = hsp_hs_init1;
|
||||
}
|
||||
|
||||
spa_bt_device_connect_profile(t->device, profile);
|
||||
|
||||
spa_log_debug(backend->log, NAME": Transport %s available for profile %s", t->path, handler);
|
||||
|
@ -1319,6 +1500,9 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
|
|||
rfcomm->codec_negotiation_supported = false;
|
||||
}
|
||||
|
||||
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 */
|
||||
cmd = spa_aprintf("AT+BRSF=%u", hf_features);
|
||||
rfcomm_send_cmd(&rfcomm->source, cmd);
|
||||
|
|
Loading…
Reference in a new issue