blob: acdca809eacdf307164395a4e4044c000e15f33b [file] [log] [blame]
/* Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <sys/time.h>
#include <syslog.h>
#include "cras_bt_io.h"
#include "cras_bt_device.h"
#include "cras_hfp_iodev.h"
#include "cras_utf8.h"
#include "cras_iodev.h"
#include "cras_iodev_list.h"
#include "sfh.h"
#include "utlist.h"
#define DEFAULT_BT_DEVICE_NAME "BLUETOOTH"
/* Extends cras_ionode to hold bluetooth profile information
* so that iodevs of different profile(A2DP or HFP/HSP) can be
* associated with the same bt_io.
* Members:
* base - The base class cras_ionode.
* profile_dev - Pointer to the profile specific iodev.
* profile - The bluetooth profile profile_dev runs on.
*/
struct bt_node {
struct cras_ionode base;
struct cras_iodev *profile_dev;
unsigned int profile;
};
/* The structure represents a virtual input or output device of a
* bluetooth audio device, speaker or headset for example. A node
* will be added to this virtual iodev for each profile supported
* by the bluetooth audio device.
* Member:
* base - The base class cras_iodev
* next_node_id - The index will give to the next node
*/
struct bt_io {
struct cras_iodev base;
unsigned int next_node_id;
struct cras_bt_device *device;
};
/* Returns the active profile specific iodev. */
static struct cras_iodev *active_profile_dev(const struct cras_iodev *iodev)
{
struct bt_node *active = (struct bt_node *)iodev->active_node;
return active->profile_dev;
}
/* Adds a profile specific iodev to btio. */
static struct cras_ionode *add_profile_dev(struct cras_iodev *bt_iodev,
struct cras_iodev *dev,
enum cras_bt_device_profile profile)
{
struct bt_node *n;
struct bt_io *btio = (struct bt_io *)bt_iodev;
n = (struct bt_node *)calloc(1, sizeof(*n));
if (!n)
return NULL;
n->base.dev = bt_iodev;
n->base.idx = btio->next_node_id++;
n->base.type = CRAS_NODE_TYPE_BLUETOOTH;
n->base.volume = 100;
n->base.stable_id = dev->info.stable_id;
n->base.capture_gain = 0;
gettimeofday(&n->base.plugged_time, NULL);
strcpy(n->base.name, dev->info.name);
n->profile_dev = dev;
n->profile = profile;
cras_iodev_add_node(bt_iodev, &n->base);
return &n->base;
}
/* Forces bt device to switch to use the given profile. Note that if
* it has already been open for streaming, the new active profile will
* take effect after the related btio(s) are reopened.
*/
static void bt_switch_to_profile(struct cras_bt_device *device,
enum cras_bt_device_profile profile)
{
switch (profile) {
case CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY:
case CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY:
cras_bt_device_set_active_profile(
device,
CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY |
CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY);
break;
case CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE:
cras_bt_device_set_active_profile(
device, CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE);
break;
default:
syslog(LOG_ERR, "Unexpect profile %u", profile);
break;
}
}
/* Switches the active profile to A2DP if it can. */
static void bt_possibly_switch_to_a2dp(struct bt_io *btio)
{
if (!cras_bt_device_has_a2dp(btio->device))
return;
cras_bt_device_set_active_profile(btio->device,
CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE);
cras_bt_device_switch_profile(btio->device, &btio->base);
}
/* Checks if bt device is active for the given profile.
*/
static int device_using_profile(struct cras_bt_device *device,
unsigned int profile)
{
return cras_bt_device_get_active_profile(device) & profile;
}
/* Checks if the condition is met to switch to a different profile based
* on two rules:
* (1) Prefer to use A2DP for output since the audio quality is better.
* (2) Must use HFP/HSP for input since A2DP doesn't support audio input.
*
* If the profile switch happens, return non-zero error code, otherwise
* return zero.
*/
static int open_dev(struct cras_iodev *iodev)
{
struct bt_io *btio = (struct bt_io *)iodev;
struct cras_iodev *dev = active_profile_dev(iodev);
int rc;
/* Force to use HFP if opening input dev. */
if (device_using_profile(btio->device,
CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE) &&
iodev->direction == CRAS_STREAM_INPUT) {
bt_switch_to_profile(btio->device,
CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY);
cras_bt_device_switch_profile_enable_dev(btio->device, iodev);
return -EAGAIN;
}
if (dev && dev->open_dev) {
rc = dev->open_dev(dev);
if (rc == 0)
return 0;
/* If input iodev open fails, switch profile back to A2DP. */
if (iodev->direction == CRAS_STREAM_INPUT)
bt_possibly_switch_to_a2dp(btio);
return rc;
}
return 0;
}
static int update_supported_formats(struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
int rc, length, i;
if (!dev)
return -EINVAL;
if (dev->update_supported_formats) {
rc = dev->update_supported_formats(dev);
if (rc)
return rc;
}
/* Fill in the supported rates and channel counts. */
for (length = 0; dev->supported_rates[length]; length++)
;
free(iodev->supported_rates);
iodev->supported_rates = (size_t *)malloc(
(length + 1) * sizeof(*iodev->supported_rates));
for (i = 0; i < length + 1; i++)
iodev->supported_rates[i] = dev->supported_rates[i];
for (length = 0; dev->supported_channel_counts[length]; length++)
;
iodev->supported_channel_counts = (size_t *)malloc(
(length + 1) * sizeof(*iodev->supported_channel_counts));
for (i = 0; i < length + 1; i++)
iodev->supported_channel_counts[i] =
dev->supported_channel_counts[i];
for (length = 0; dev->supported_formats[length]; length++)
;
iodev->supported_formats = (snd_pcm_format_t *)malloc(
(length + 1) * sizeof(*iodev->supported_formats));
for (i = 0; i < length + 1; i++)
iodev->supported_formats[i] = dev->supported_formats[i];
/* Record max supported channels into cras_iodev_info. */
iodev->info.max_supported_channels = dev->info.max_supported_channels;
return 0;
}
static int configure_dev(struct cras_iodev *iodev)
{
int rc;
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
/* Fill back the format iodev is using. */
if (dev->format == NULL) {
dev->format = (struct cras_audio_format *)malloc(
sizeof(*dev->format));
if (!dev->format)
return -ENOMEM;
*dev->format = *iodev->format;
}
rc = dev->configure_dev(dev);
if (rc)
return rc;
iodev->buffer_size = dev->buffer_size;
iodev->min_buffer_level = dev->min_buffer_level;
if (dev->start)
dev->state = CRAS_IODEV_STATE_OPEN;
else
dev->state = CRAS_IODEV_STATE_NO_STREAM_RUN;
return 0;
}
static int close_dev(struct cras_iodev *iodev)
{
struct bt_io *btio = (struct bt_io *)iodev;
int rc;
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
/* If input iodev is in open state and being closed, switch profile
* from HFP to A2DP. */
if (cras_iodev_is_open(iodev) &&
device_using_profile(
btio->device,
CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY |
CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY) &&
(iodev->direction == CRAS_STREAM_INPUT))
bt_possibly_switch_to_a2dp(btio);
rc = dev->close_dev(dev);
if (rc < 0)
return rc;
cras_iodev_free_format(iodev);
dev->state = CRAS_IODEV_STATE_CLOSE;
return 0;
}
static void set_bt_volume(struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return;
if (dev->active_node)
dev->active_node->volume = iodev->active_node->volume;
/* The parent bt_iodev could set software_volume_needed flag for cases
* that software volume provides better experience across profiles
* (HFP and A2DP). Otherwise, use the profile specific implementation
* to adjust volume. */
if (dev->set_volume && !iodev->software_volume_needed)
dev->set_volume(dev);
}
static int frames_queued(const struct cras_iodev *iodev,
struct timespec *tstamp)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
return dev->frames_queued(dev, tstamp);
}
static int delay_frames(const struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
return dev->delay_frames(dev);
}
static int get_buffer(struct cras_iodev *iodev, struct cras_audio_area **area,
unsigned *frames)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
return dev->get_buffer(dev, area, frames);
}
static int put_buffer(struct cras_iodev *iodev, unsigned nwritten)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
return dev->put_buffer(dev, nwritten);
}
static int flush_buffer(struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
return dev->flush_buffer(dev);
}
/* If the first private iodev doesn't match the active profile stored on
* device, select to the correct private iodev.
*/
static void update_active_node(struct cras_iodev *iodev, unsigned node_idx,
unsigned dev_enabled)
{
struct bt_io *btio = (struct bt_io *)iodev;
struct cras_ionode *node;
struct bt_node *active = (struct bt_node *)iodev->active_node;
struct cras_iodev *dev;
int rc;
if (device_using_profile(btio->device, active->profile))
goto leave;
/* Switch to the correct dev using active_profile. */
DL_FOREACH (iodev->nodes, node) {
struct bt_node *n = (struct bt_node *)node;
if (n == active)
continue;
if (device_using_profile(btio->device, n->profile)) {
active->profile = n->profile;
active->profile_dev = n->profile_dev;
/* Set volume for the new profile. */
set_bt_volume(iodev);
}
}
leave:
dev = active_profile_dev(iodev);
if (dev && dev->update_active_node)
dev->update_active_node(dev, node_idx, dev_enabled);
/* Update supported formats here to get the supported formats from the
* new updated active profile dev.
*/
rc = update_supported_formats(iodev);
if (rc) {
syslog(LOG_ERR, "Failed to update supported formats, rc=%d",
rc);
}
}
static int output_underrun(struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
if (dev->output_underrun) {
dev->min_cb_level = iodev->min_cb_level;
dev->max_cb_level = iodev->max_cb_level;
dev->buffer_size = iodev->buffer_size;
return dev->output_underrun(dev);
}
return 0;
}
static int no_stream(struct cras_iodev *iodev, int enable)
{
struct cras_iodev *dev = active_profile_dev(iodev);
int rc;
if (!dev)
return -EINVAL;
if (dev->no_stream) {
/*
* Copy iodev->min_cb_level and iodev->max_cb_level from the
* parent (i.e. bt_io). no_stream() of hfp_alsa_iodev will
* use them.
* A2DP and HFP dev will use buffer and callback sizes to fill
* zeros in no stream state.
*/
dev->min_cb_level = iodev->min_cb_level;
dev->max_cb_level = iodev->max_cb_level;
dev->buffer_size = iodev->buffer_size;
rc = dev->no_stream(dev, enable);
if (rc < 0)
return rc;
}
if (enable)
dev->state = CRAS_IODEV_STATE_NO_STREAM_RUN;
else
dev->state = CRAS_IODEV_STATE_NORMAL_RUN;
return 0;
}
static int is_free_running(const struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
if (dev->is_free_running)
return dev->is_free_running(dev);
return 0;
}
static int start(const struct cras_iodev *iodev)
{
struct cras_iodev *dev = active_profile_dev(iodev);
int rc;
if (!dev)
return -EINVAL;
if (dev->start) {
rc = dev->start(dev);
if (rc)
return rc;
}
dev->state = CRAS_IODEV_STATE_NORMAL_RUN;
return 0;
}
static unsigned int frames_to_play_in_sleep(struct cras_iodev *iodev,
unsigned int *hw_level,
struct timespec *hw_tstamp)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev || !dev->frames_to_play_in_sleep)
return cras_iodev_default_frames_to_play_in_sleep(
iodev, hw_level, hw_tstamp);
return dev->frames_to_play_in_sleep(dev, hw_level, hw_tstamp);
}
static int get_valid_frames(struct cras_iodev *iodev,
struct timespec *hw_tstamp)
{
struct cras_iodev *dev = active_profile_dev(iodev);
if (!dev)
return -EINVAL;
if (dev->get_valid_frames)
return dev->get_valid_frames(dev, hw_tstamp);
return cras_iodev_frames_queued(iodev, hw_tstamp);
}
struct cras_iodev *cras_bt_io_create(struct cras_bt_device *device,
struct cras_iodev *dev,
enum cras_bt_device_profile profile)
{
int err;
struct bt_io *btio;
struct cras_iodev *iodev;
struct cras_ionode *node;
struct bt_node *active;
if (!dev)
return NULL;
btio = (struct bt_io *)calloc(1, sizeof(*btio));
if (!btio)
goto error;
btio->device = device;
iodev = &btio->base;
iodev->direction = dev->direction;
strcpy(iodev->info.name, dev->info.name);
iodev->info.stable_id = dev->info.stable_id;
iodev->open_dev = open_dev;
iodev->configure_dev = configure_dev;
iodev->frames_queued = frames_queued;
iodev->delay_frames = delay_frames;
iodev->get_buffer = get_buffer;
iodev->put_buffer = put_buffer;
iodev->flush_buffer = flush_buffer;
iodev->close_dev = close_dev;
iodev->update_supported_formats = update_supported_formats;
iodev->update_active_node = update_active_node;
iodev->no_stream = no_stream;
iodev->output_underrun = output_underrun;
iodev->is_free_running = is_free_running;
iodev->get_valid_frames = get_valid_frames;
iodev->start = start;
iodev->frames_to_play_in_sleep = frames_to_play_in_sleep;
/* Input also checks |software_volume_needed| flag for using software
* gain. Keep it as false for BT input.
* TODO(hychao): after wide band speech mode is supported, consider
* enable software gain.
*/
if (dev->direction == CRAS_STREAM_OUTPUT) {
iodev->software_volume_needed =
!cras_bt_device_get_use_hardware_volume(device);
iodev->set_volume = set_bt_volume;
}
/* Create the fake node so it's the only node exposed to UI, and
* point it to the first profile dev. */
active = (struct bt_node *)calloc(1, sizeof(*active));
if (!active)
goto error;
active->base.dev = iodev;
active->base.idx = btio->next_node_id++;
active->base.type = dev->active_node->type;
active->base.volume = 100;
active->base.stable_id = cras_bt_device_get_stable_id(device);
active->base.ui_gain_scaler = 1.0f;
/*
* If the same headset is connected in wideband mode, we shall assign
* a separate stable_id so the node priority/preference mechanism in
* Chrome UI doesn't break.
*/
if ((active->base.type == CRAS_NODE_TYPE_BLUETOOTH) &&
(dev->direction == CRAS_STREAM_INPUT))
active->base.stable_id =
SuperFastHash((const char *)&active->base.type,
sizeof(active->base.type),
active->base.stable_id);
active->profile = profile;
active->profile_dev = dev;
strcpy(active->base.name, dev->info.name);
/* The node name exposed to UI should be a valid UTF8 string. */
if (!is_utf8_string(active->base.name))
strcpy(active->base.name, DEFAULT_BT_DEVICE_NAME);
cras_iodev_add_node(iodev, &active->base);
node = add_profile_dev(&btio->base, dev, profile);
if (node == NULL)
goto error;
/* Default active profile to a2dp whenever it's allowed. */
if (!cras_bt_device_get_active_profile(device) ||
(profile == CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE &&
cras_bt_device_can_switch_to_a2dp(device)))
bt_switch_to_profile(device, profile);
if (iodev->direction == CRAS_STREAM_OUTPUT)
err = cras_iodev_list_add_output(iodev);
else
err = cras_iodev_list_add_input(iodev);
if (err)
goto error;
cras_iodev_set_active_node(iodev, &active->base);
return &btio->base;
error:
if (btio)
free(btio);
return NULL;
}
void cras_bt_io_free_resources(struct cras_iodev *bt_iodev)
{
struct cras_ionode *node;
struct bt_node *n;
free(bt_iodev->supported_rates);
free(bt_iodev->supported_channel_counts);
free(bt_iodev->supported_formats);
DL_FOREACH (bt_iodev->nodes, node) {
n = (struct bt_node *)node;
cras_iodev_rm_node(bt_iodev, node);
free(n);
}
cras_iodev_free_resources(bt_iodev);
}
void cras_bt_io_destroy(struct cras_iodev *bt_iodev)
{
int rc;
struct bt_io *btio = (struct bt_io *)bt_iodev;
if (bt_iodev->direction == CRAS_STREAM_OUTPUT)
rc = cras_iodev_list_rm_output(bt_iodev);
else
rc = cras_iodev_list_rm_input(bt_iodev);
if (rc == -EBUSY)
return;
cras_bt_io_free_resources(bt_iodev);
free(btio);
}
struct cras_ionode *cras_bt_io_get_profile(struct cras_iodev *bt_iodev,
enum cras_bt_device_profile profile)
{
struct cras_ionode *node;
DL_FOREACH (bt_iodev->nodes, node) {
struct bt_node *n = (struct bt_node *)node;
if (n->profile & profile)
return node;
}
return NULL;
}
int cras_bt_io_append(struct cras_iodev *bt_iodev, struct cras_iodev *dev,
enum cras_bt_device_profile profile)
{
struct cras_ionode *node;
struct bt_io *btio = (struct bt_io *)bt_iodev;
if (cras_bt_io_get_profile(bt_iodev, profile))
return -EEXIST;
node = add_profile_dev(bt_iodev, dev, profile);
if (!node)
return -ENOMEM;
if (profile == CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE &&
cras_bt_device_can_switch_to_a2dp(btio->device)) {
bt_switch_to_profile(btio->device,
CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE);
cras_bt_device_switch_profile(btio->device, bt_iodev);
syslog(LOG_ERR, "Switch to A2DP on append");
}
return 0;
}
int cras_bt_io_on_profile(struct cras_iodev *bt_iodev,
enum cras_bt_device_profile profile)
{
struct bt_node *btnode = (struct bt_node *)bt_iodev->active_node;
return !!(profile & btnode->profile);
}
enum cras_bt_device_profile
cras_bt_io_profile_to_log(struct cras_iodev *bt_iodev)
{
struct bt_node *btnode = (struct bt_node *)bt_iodev->active_node;
if (btnode->profile & CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE)
return CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE;
if (hfp_iodev_is_hsp(btnode->profile_dev))
return CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY;
else
return CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY;
}
unsigned int cras_bt_io_try_remove(struct cras_iodev *bt_iodev,
struct cras_iodev *dev)
{
struct cras_ionode *node;
struct bt_node *active, *btnode;
unsigned int try_profile = 0;
active = (struct bt_node *)bt_iodev->active_node;
if (active->profile_dev == dev) {
DL_FOREACH (bt_iodev->nodes, node) {
btnode = (struct bt_node *)node;
/* Skip the active node and the node we're trying
* to remove. */
if (btnode == active || btnode->profile_dev == dev)
continue;
try_profile = btnode->profile;
break;
}
} else {
try_profile = active->profile;
}
return try_profile;
}
int cras_bt_io_remove(struct cras_iodev *bt_iodev, struct cras_iodev *dev)
{
struct cras_ionode *node;
struct bt_node *btnode;
DL_FOREACH (bt_iodev->nodes, node) {
btnode = (struct bt_node *)node;
if (btnode->profile_dev != dev)
continue;
/* If this is the active node, reset it. Otherwise delete
* this node. */
if (node == bt_iodev->active_node) {
btnode->profile_dev = NULL;
btnode->profile = 0;
} else {
DL_DELETE(bt_iodev->nodes, node);
free(node);
}
}
/* The node of active profile could have been removed, update it.
* Return err when fail to locate the active profile dev. */
update_active_node(bt_iodev, 0, 1);
btnode = (struct bt_node *)bt_iodev->active_node;
if ((btnode->profile == 0) || (btnode->profile_dev == NULL))
return -EINVAL;
return 0;
}