| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2022-2025 Qualcomm Innovation Center, Inc. All rights reserved. |
| */ |
| #include <linux/of.h> |
| #include <linux/usb.h> |
| |
| #include <sound/jack.h> |
| #include <sound/soc-usb.h> |
| |
| #include "../usb/card.h" |
| |
| static DEFINE_MUTEX(ctx_mutex); |
| static LIST_HEAD(usb_ctx_list); |
| |
| static struct device_node *snd_soc_find_phandle(struct device *dev) |
| { |
| struct device_node *node; |
| |
| node = of_parse_phandle(dev->of_node, "usb-soc-be", 0); |
| if (!node) |
| return ERR_PTR(-ENODEV); |
| |
| return node; |
| } |
| |
| static struct snd_soc_usb *snd_soc_usb_ctx_lookup(struct device_node *node) |
| { |
| struct snd_soc_usb *ctx; |
| |
| if (!node) |
| return NULL; |
| |
| list_for_each_entry(ctx, &usb_ctx_list, list) { |
| if (ctx->component->dev->of_node == node) |
| return ctx; |
| } |
| |
| return NULL; |
| } |
| |
| static struct snd_soc_usb *snd_soc_find_usb_ctx(struct device *dev) |
| { |
| struct snd_soc_usb *ctx; |
| struct device_node *node; |
| |
| node = snd_soc_find_phandle(dev); |
| if (!IS_ERR(node)) { |
| ctx = snd_soc_usb_ctx_lookup(node); |
| of_node_put(node); |
| } else { |
| ctx = snd_soc_usb_ctx_lookup(dev->of_node); |
| } |
| |
| return ctx ? ctx : NULL; |
| } |
| |
| /* SOC USB sound kcontrols */ |
| /** |
| * snd_soc_usb_setup_offload_jack() - Create USB offloading jack |
| * @component: USB DPCM backend DAI component |
| * @jack: jack structure to create |
| * |
| * Creates a jack device for notifying userspace of the availability |
| * of an offload capable device. |
| * |
| * Returns 0 on success, negative on error. |
| * |
| */ |
| int snd_soc_usb_setup_offload_jack(struct snd_soc_component *component, |
| struct snd_soc_jack *jack) |
| { |
| int ret; |
| |
| ret = snd_soc_card_jack_new(component->card, "USB Offload Jack", |
| SND_JACK_USB, jack); |
| if (ret < 0) { |
| dev_err(component->card->dev, "Unable to add USB offload jack: %d\n", |
| ret); |
| return ret; |
| } |
| |
| ret = snd_soc_component_set_jack(component, jack, NULL); |
| if (ret) { |
| dev_err(component->card->dev, "Failed to set jack: %d\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_setup_offload_jack); |
| |
| /** |
| * snd_soc_usb_update_offload_route - Find active USB offload path |
| * @dev: USB device to get offload status |
| * @card: USB card index |
| * @pcm: USB PCM device index |
| * @direction: playback or capture direction |
| * @path: pcm or card index |
| * @route: pointer to route output array |
| * |
| * Fetch the current status for the USB SND card and PCM device indexes |
| * specified. The "route" argument should be an array of integers being |
| * used for a kcontrol output. The first element should have the selected |
| * card index, and the second element should have the selected pcm device |
| * index. |
| */ |
| int snd_soc_usb_update_offload_route(struct device *dev, int card, int pcm, |
| int direction, enum snd_soc_usb_kctl path, |
| long *route) |
| { |
| struct snd_soc_usb *ctx; |
| int ret = -ENODEV; |
| |
| mutex_lock(&ctx_mutex); |
| ctx = snd_soc_find_usb_ctx(dev); |
| if (!ctx) |
| goto exit; |
| |
| if (ctx->update_offload_route_info) |
| ret = ctx->update_offload_route_info(ctx->component, card, pcm, |
| direction, path, route); |
| exit: |
| mutex_unlock(&ctx_mutex); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_update_offload_route); |
| |
| /** |
| * snd_soc_usb_find_priv_data() - Retrieve private data stored |
| * @usbdev: device reference |
| * |
| * Fetch the private data stored in the USB SND SoC structure. |
| * |
| */ |
| void *snd_soc_usb_find_priv_data(struct device *usbdev) |
| { |
| struct snd_soc_usb *ctx; |
| |
| mutex_lock(&ctx_mutex); |
| ctx = snd_soc_find_usb_ctx(usbdev); |
| mutex_unlock(&ctx_mutex); |
| |
| return ctx ? ctx->priv_data : NULL; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_find_priv_data); |
| |
| /** |
| * snd_soc_usb_find_supported_format() - Check if audio format is supported |
| * @card_idx: USB sound chip array index |
| * @params: PCM parameters |
| * @direction: capture or playback |
| * |
| * Ensure that a requested audio profile from the ASoC side is able to be |
| * supported by the USB device. |
| * |
| * Return 0 on success, negative on error. |
| * |
| */ |
| int snd_soc_usb_find_supported_format(int card_idx, |
| struct snd_pcm_hw_params *params, |
| int direction) |
| { |
| struct snd_usb_stream *as; |
| |
| as = snd_usb_find_suppported_substream(card_idx, params, direction); |
| if (!as) |
| return -EOPNOTSUPP; |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_find_supported_format); |
| |
| /** |
| * snd_soc_usb_allocate_port() - allocate a SoC USB port for offloading support |
| * @component: USB DPCM backend DAI component |
| * @data: private data |
| * |
| * Allocate and initialize a SoC USB port. The SoC USB port is used to communicate |
| * different USB audio devices attached, in order to start audio offloading handled |
| * by an ASoC entity. USB device plug in/out events are signaled with a |
| * notification, but don't directly impact the memory allocated for the SoC USB |
| * port. |
| * |
| */ |
| struct snd_soc_usb *snd_soc_usb_allocate_port(struct snd_soc_component *component, |
| void *data) |
| { |
| struct snd_soc_usb *usb; |
| |
| usb = kzalloc(sizeof(*usb), GFP_KERNEL); |
| if (!usb) |
| return ERR_PTR(-ENOMEM); |
| |
| usb->component = component; |
| usb->priv_data = data; |
| |
| return usb; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_allocate_port); |
| |
| /** |
| * snd_soc_usb_free_port() - free a SoC USB port used for offloading support |
| * @usb: allocated SoC USB port |
| * |
| * Free and remove the SoC USB port from the available list of ports. This will |
| * ensure that the communication between USB SND and ASoC is halted. |
| * |
| */ |
| void snd_soc_usb_free_port(struct snd_soc_usb *usb) |
| { |
| snd_soc_usb_remove_port(usb); |
| kfree(usb); |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_free_port); |
| |
| /** |
| * snd_soc_usb_add_port() - Add a USB backend port |
| * @usb: soc usb port to add |
| * |
| * Register a USB backend DAI link to the USB SoC framework. Memory is allocated |
| * as part of the USB backend DAI link. |
| * |
| */ |
| void snd_soc_usb_add_port(struct snd_soc_usb *usb) |
| { |
| mutex_lock(&ctx_mutex); |
| list_add_tail(&usb->list, &usb_ctx_list); |
| mutex_unlock(&ctx_mutex); |
| |
| snd_usb_rediscover_devices(); |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_add_port); |
| |
| /** |
| * snd_soc_usb_remove_port() - Remove a USB backend port |
| * @usb: soc usb port to remove |
| * |
| * Remove a USB backend DAI link from USB SoC. Memory is freed when USB backend |
| * DAI is removed, or when snd_soc_usb_free_port() is called. |
| * |
| */ |
| void snd_soc_usb_remove_port(struct snd_soc_usb *usb) |
| { |
| struct snd_soc_usb *ctx, *tmp; |
| |
| mutex_lock(&ctx_mutex); |
| list_for_each_entry_safe(ctx, tmp, &usb_ctx_list, list) { |
| if (ctx == usb) { |
| list_del(&ctx->list); |
| break; |
| } |
| } |
| mutex_unlock(&ctx_mutex); |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_remove_port); |
| |
| /** |
| * snd_soc_usb_connect() - Notification of USB device connection |
| * @usbdev: USB bus device |
| * @sdev: USB SND device to add |
| * |
| * Notify of a new USB SND device connection. The sdev->card_idx can be used to |
| * handle how the DPCM backend selects, which device to enable USB offloading |
| * on. |
| * |
| */ |
| int snd_soc_usb_connect(struct device *usbdev, struct snd_soc_usb_device *sdev) |
| { |
| struct snd_soc_usb *ctx; |
| |
| if (!usbdev) |
| return -ENODEV; |
| |
| mutex_lock(&ctx_mutex); |
| ctx = snd_soc_find_usb_ctx(usbdev); |
| if (!ctx) |
| goto exit; |
| |
| if (ctx->connection_status_cb) |
| ctx->connection_status_cb(ctx, sdev, true); |
| |
| exit: |
| mutex_unlock(&ctx_mutex); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_connect); |
| |
| /** |
| * snd_soc_usb_disconnect() - Notification of USB device disconnection |
| * @usbdev: USB bus device |
| * @sdev: USB SND device to remove |
| * |
| * Notify of a new USB SND device disconnection to the USB backend. |
| * |
| */ |
| int snd_soc_usb_disconnect(struct device *usbdev, struct snd_soc_usb_device *sdev) |
| { |
| struct snd_soc_usb *ctx; |
| |
| if (!usbdev) |
| return -ENODEV; |
| |
| mutex_lock(&ctx_mutex); |
| ctx = snd_soc_find_usb_ctx(usbdev); |
| if (!ctx) |
| goto exit; |
| |
| if (ctx->connection_status_cb) |
| ctx->connection_status_cb(ctx, sdev, false); |
| |
| exit: |
| mutex_unlock(&ctx_mutex); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(snd_soc_usb_disconnect); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("SoC USB driver for offloading"); |