| /* 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 <syslog.h> |
| |
| #include "cras_bt_io.h" |
| #include "cras_bt_device.h" |
| #include "cras_dbus_util.h" |
| #include "cras_iodev.h" |
| #include "cras_iodev_list.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; |
| 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; |
| } |
| } |
| |
| /* 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 |
| * when trying to set the format to btio before open it. Base 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 update_supported_formats(struct cras_iodev *iodev) |
| { |
| struct bt_io *btio = (struct bt_io *)iodev; |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| int rc, length, i; |
| |
| /* 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_on_open(btio->device, iodev); |
| return -EAGAIN; |
| } |
| |
| if (dev->format == NULL) { |
| dev->format = (struct cras_audio_format *) |
| malloc(sizeof(*dev->format)); |
| *dev->format = *iodev->format; |
| } |
| |
| 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]; |
| return 0; |
| } |
| |
| static int open_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. */ |
| *dev->format = *iodev->format; |
| |
| rc = dev->open_dev(dev); |
| if (rc) { |
| /* Free format here to assure the update_supported_format |
| * callback will be called before any future open_dev call. */ |
| cras_iodev_free_format(iodev); |
| return rc; |
| } |
| |
| iodev->buffer_size = dev->buffer_size; |
| iodev->min_buffer_level = dev->min_buffer_level; |
| 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); |
| |
| /* Force back to A2DP if closing HFP. */ |
| if (device_using_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY | |
| CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY) && |
| iodev->direction == CRAS_STREAM_INPUT && |
| cras_bt_device_has_a2dp(btio->device)) { |
| cras_bt_device_set_active_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE); |
| cras_bt_device_switch_profile_on_close(btio->device, |
| iodev); |
| } |
| |
| rc = dev->close_dev(dev); |
| if (rc < 0) |
| return rc; |
| cras_iodev_free_format(iodev); |
| return 0; |
| } |
| |
| static void set_bt_volume(struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| |
| if (dev->active_node) |
| dev->active_node->volume = iodev->active_node->volume; |
| if (dev->set_volume) |
| dev->set_volume(dev); |
| } |
| |
| static int is_open(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return 0; |
| return dev->is_open(dev); |
| } |
| |
| static int frames_queued(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->frames_queued(dev); |
| } |
| |
| static int dev_running(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->dev_running(dev); |
| } |
| |
| 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) |
| { |
| struct bt_io *btio = (struct bt_io *)iodev; |
| struct cras_ionode *node; |
| struct bt_node *active = (struct bt_node *)iodev->active_node; |
| |
| if (device_using_profile(btio->device, active->profile)) |
| return; |
| |
| /* 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; |
| |
| /* Fill all volume related stuff to/from the |
| * profile dev. */ |
| iodev->software_volume_needed = |
| active->profile_dev->software_volume_needed; |
| set_bt_volume(iodev); |
| } |
| } |
| } |
| |
| 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->is_open = is_open; /* Needed by thread_add_stream */ |
| iodev->frames_queued = frames_queued; |
| iodev->dev_running = dev_running; |
| 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->software_volume_needed = dev->software_volume_needed; |
| iodev->set_volume = set_bt_volume; |
| |
| /* Create the dummy node set to plugged 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) |
| return NULL; |
| active->base.dev = iodev; |
| active->base.idx = btio->next_node_id++; |
| active->base.type = CRAS_NODE_TYPE_BLUETOOTH; |
| active->base.volume = 100; |
| active->base.plugged = 1; |
| active->profile = profile; |
| active->profile_dev = dev; |
| gettimeofday(&active->base.plugged_time, NULL); |
| 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_destroy(struct cras_iodev *bt_iodev) |
| { |
| int rc; |
| struct bt_io *btio = (struct bt_io *)bt_iodev; |
| struct cras_ionode *node; |
| struct bt_node *n; |
| |
| 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; |
| |
| DL_FOREACH(bt_iodev->nodes, node) { |
| n = (struct bt_node *)node; |
| cras_iodev_rm_node(bt_iodev, node); |
| free(n); |
| } |
| 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_on_open(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); |
| } |
| |
| int cras_bt_io_update_buffer_size(struct cras_iodev *bt_iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(bt_iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| bt_iodev->buffer_size = dev->buffer_size; |
| return 0; |
| } |
| |
| 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); |
| btnode = (struct bt_node *)bt_iodev->active_node; |
| if ((btnode->profile == 0) || (btnode->profile_dev == NULL)) |
| return -EINVAL; |
| |
| return 0; |
| } |