bluez5: add support for HSP/HFP hardware volume control

Only native backend is fulfilled.
This commit is contained in:
Huang-Huang Bao 2021-04-15 18:22:09 +08:00
parent 80f6ddf526
commit 387f7e327f
No known key found for this signature in database
GPG key ID: 33C3271387A13D1B

View file

@ -77,6 +77,7 @@ struct impl {
}; };
struct transport_data { struct transport_data {
struct rfcomm *rfcomm;
struct spa_source sco; struct spa_source sco;
}; };
@ -86,10 +87,25 @@ enum hfp_hf_state {
hfp_hf_cind1, hfp_hf_cind1,
hfp_hf_cind2, hfp_hf_cind2,
hfp_hf_cmer, hfp_hf_cmer,
hfp_hf_slc, hfp_hf_slc1,
hfp_hf_slc2,
hfp_hf_vgs,
hfp_hf_vgm,
hfp_hf_bcs 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 rfcomm {
struct spa_list link; struct spa_list link;
struct spa_source source; struct spa_source source;
@ -101,6 +117,8 @@ struct rfcomm {
enum spa_bt_profile profile; enum spa_bt_profile profile;
struct spa_source timer; struct spa_source timer;
char* path; char* path;
bool has_volume;
struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM];
#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
unsigned int slc_configured:1; unsigned int slc_configured:1;
unsigned int codec_negotiation_supported:1; unsigned int codec_negotiation_supported:1;
@ -109,6 +127,7 @@ struct rfcomm {
unsigned int hfp_ag_switching_codec:1; unsigned int hfp_ag_switching_codec:1;
unsigned int hfp_ag_initial_codec_setup:2; unsigned int hfp_ag_initial_codec_setup:2;
enum hfp_hf_state hf_state; enum hfp_hf_state hf_state;
enum hsp_hs_state hs_state;
unsigned int codec; unsigned int codec;
#endif #endif
}; };
@ -148,6 +167,7 @@ static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm)
{ {
struct impl *backend = rfcomm->backend; struct impl *backend = rfcomm->backend;
struct spa_bt_transport *t = NULL; struct spa_bt_transport *t = NULL;
struct transport_data *td;
char* pathfd; char* pathfd;
if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL) 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->n_channels = 1;
t->channels[0] = SPA_AUDIO_CHANNEL_MONO; 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); spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);
finish: 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)); 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 rfcomm *rfcomm = source->data;
struct impl *backend = rfcomm->backend; struct impl *backend = rfcomm->backend;
@ -231,6 +268,30 @@ static int rfcomm_send_reply(struct spa_source *source, char *data)
return len; 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 #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
static bool rfcomm_hsp_ag(struct spa_source *source, char* buf) 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+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. */ * AT+CKPD=200: Sent by HS when headset button is pressed. */
if (sscanf(buf, "AT+VGS=%d", &gain) == 1) { if (sscanf(buf, "AT+VGS=%d", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->speaker_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
rfcomm_send_reply(source, "OK"); rfcomm_send_reply(source, "OK");
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf); spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf);
rfcomm_send_reply(source, "ERROR"); rfcomm_send_reply(source, "ERROR");
} }
} else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) { } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->microphone_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
rfcomm_send_reply(source, "OK"); rfcomm_send_reply(source, "OK");
} else { } else {
rfcomm_send_reply(source, "ERROR"); rfcomm_send_reply(source, "ERROR");
@ -267,6 +328,33 @@ static bool rfcomm_hsp_ag(struct spa_source *source, char* buf)
return true; 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) static bool rfcomm_hsp_hs(struct spa_source *source, char* buf)
{ {
struct rfcomm *rfcomm = source->data; 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 * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because
* it does not expect a reply. */ * it does not expect a reply. */
if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) { if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->microphone_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf); 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) { } else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->speaker_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", buf); 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; return true;
@ -363,10 +463,16 @@ static bool rfcomm_hfp_ag(struct spa_source *source, char* buf)
unsigned int selected_codec; unsigned int selected_codec;
if (sscanf(buf, "AT+BRSF=%u", &features) == 1) { if (sscanf(buf, "AT+BRSF=%u", &features) == 1) {
unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE; unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE;
char *cmd; 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 /* Decide if we want to signal that the computer supports mSBC negotiation
This should be done when This should be done when
a) mSBC support is enabled in config file and 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 { } else {
rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); 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; rfcomm->transport->codec = selected_codec;
spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID);
rfcomm_send_reply(source, "OK"); rfcomm_send_reply(source, "OK");
if (was_switching_codec) if (was_switching_codec)
spa_bt_device_emit_codec_switched(rfcomm->device, 0); spa_bt_device_emit_codec_switched(rfcomm->device, 0);
} else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) { } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->microphone_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
rfcomm_send_reply(source, "OK"); rfcomm_send_reply(source, "OK");
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", buf); spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", buf);
rfcomm_send_reply(source, "ERROR"); rfcomm_send_reply(source, "ERROR");
} }
} else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) { } else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) {
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->speaker_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
rfcomm_send_reply(source, "OK"); rfcomm_send_reply(source, "OK");
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", buf); 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); token = strtok(NULL, separators);
gain = atoi(token); gain = atoi(token);
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->speaker_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGM gain: %s", token); 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); token = strtok(NULL, separators);
gain = atoi(token); gain = atoi(token);
if (gain <= 15) { if (gain <= SPA_BT_VOLUME_HS_MAX) {
/* t->microphone_gain = gain; */ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
} else { } else {
spa_log_debug(backend->log, NAME": RFCOMM receive unsupported VGS gain: %s", token); 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; rfcomm->hf_state = hfp_hf_cmer;
break; break;
case hfp_hf_cmer: case hfp_hf_cmer:
rfcomm->hf_state = hfp_hf_slc; rfcomm->hf_state = hfp_hf_slc1;
rfcomm->slc_configured = true; rfcomm->slc_configured = true;
if (!rfcomm->codec_negotiation_supported) { if (!rfcomm->codec_negotiation_supported) {
rfcomm->transport = _transport_create(rfcomm); 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); 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; break;
default: default:
break; 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); 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); spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING);
return; return;
@ -1043,10 +1176,47 @@ fail_close:
return -1; 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 = { static const struct spa_bt_transport_implementation sco_transport_impl = {
SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
.acquire = sco_acquire_cb, .acquire = sco_acquire_cb,
.release = sco_release_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) 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.mask = SPA_IO_IN;
rfcomm->source.rmask = 0; 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_bt_device_add_listener(d, &rfcomm->device_listener, &device_events, rfcomm);
spa_loop_add_source(backend->main_loop, &rfcomm->source); spa_loop_add_source(backend->main_loop, &rfcomm->source);
spa_list_append(&backend->rfcomm_list, &rfcomm->link); spa_list_append(&backend->rfcomm_list, &rfcomm->link);
@ -1296,6 +1472,11 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
} }
rfcomm->transport = t; 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_bt_device_connect_profile(t->device, profile);
spa_log_debug(backend->log, NAME": Transport %s available for profile %s", t->path, handler); 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->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 */ /* send command to AG with the features supported by Hands-Free */
cmd = spa_aprintf("AT+BRSF=%u", hf_features); cmd = spa_aprintf("AT+BRSF=%u", hf_features);
rfcomm_send_cmd(&rfcomm->source, cmd); rfcomm_send_cmd(&rfcomm->source, cmd);