diff options
author | Greg Kroah-Hartman <gregkh@google.com> | 2021-09-24 16:16:23 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-09-24 16:16:23 +0000 |
commit | 0ae1cf21e2192e23141539e565644a9841bf737a (patch) | |
tree | 217bfac655aa0d5334426b4c9f8c737d689a7b63 | |
parent | 8190771d07e545070f8ad4d2af03473182ce405e (diff) | |
parent | b9c7067933d35da686607575b3f8da5ba6220d4b (diff) | |
download | virtual-device-0ae1cf21e2192e23141539e565644a9841bf737a.tar.gz |
Merge "Add virtio-snd" into android12-5.10
-rw-r--r-- | Kbuild | 2 | ||||
-rw-r--r-- | uapi/linux/virtio_snd.h | 361 | ||||
-rw-r--r-- | virtio_snd/Kbuild | 16 | ||||
-rw-r--r-- | virtio_snd/virtio_card.c | 472 | ||||
-rw-r--r-- | virtio_snd/virtio_card.h | 152 | ||||
-rw-r--r-- | virtio_snd/virtio_chmap.c | 252 | ||||
-rw-r--r-- | virtio_snd/virtio_ctl_msg.c | 206 | ||||
-rw-r--r-- | virtio_snd/virtio_ctl_msg.h | 75 | ||||
-rw-r--r-- | virtio_snd/virtio_dc.c | 377 | ||||
-rw-r--r-- | virtio_snd/virtio_event.c | 127 | ||||
-rw-r--r-- | virtio_snd/virtio_jack.c | 263 | ||||
-rw-r--r-- | virtio_snd/virtio_opsy.h | 31 | ||||
-rw-r--r-- | virtio_snd/virtio_opsy_ctl_msg.c | 156 | ||||
-rw-r--r-- | virtio_snd/virtio_pcm.c | 620 | ||||
-rw-r--r-- | virtio_snd/virtio_pcm.h | 125 | ||||
-rw-r--r-- | virtio_snd/virtio_pcm_msg.c | 256 | ||||
-rw-r--r-- | virtio_snd/virtio_pcm_ops.c | 385 | ||||
-rw-r--r-- | virtio_snd/virtio_snd.h | 479 |
18 files changed, 4355 insertions, 0 deletions
@@ -1,5 +1,7 @@ obj-m += virtio_gpu/ +obj-m += virtio_snd/ + obj-$(CONFIG_VIRT_WIFI) += wlan_simulation/ obj-${BUILD_GOLDFISH_DRIVERS} += goldfish_drivers/ diff --git a/uapi/linux/virtio_snd.h b/uapi/linux/virtio_snd.h new file mode 100644 index 0000000..1ff6310 --- /dev/null +++ b/uapi/linux/virtio_snd.h @@ -0,0 +1,361 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (C) 2020 OpenSynergy GmbH + * + * This header is BSD licensed so anyone can use the definitions to + * implement compatible drivers/servers. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of OpenSynergy GmbH nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IBM OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#ifndef VIRTIO_SND_IF_H +#define VIRTIO_SND_IF_H + +#include <linux/virtio_types.h> + +/******************************************************************************* + * CONFIGURATION SPACE + */ +struct virtio_snd_config { + /* # of available physical jacks */ + __le32 jacks; + /* # of available PCM streams */ + __le32 streams; + /* # of available channel maps */ + __le32 chmaps; +}; + +enum { + /* device virtqueue indexes */ + VIRTIO_SND_VQ_CONTROL = 0, + VIRTIO_SND_VQ_EVENT, + VIRTIO_SND_VQ_TX, + VIRTIO_SND_VQ_RX, + /* # of device virtqueues */ + VIRTIO_SND_VQ_MAX +}; + +/******************************************************************************* + * COMMON DEFINITIONS + */ + +/* supported dataflow directions */ +enum { + VIRTIO_SND_D_OUTPUT = 0, + VIRTIO_SND_D_INPUT +}; + +enum { + /* jack control request types */ + VIRTIO_SND_R_JACK_INFO = 1, + VIRTIO_SND_R_JACK_REMAP, + + /* PCM control request types */ + VIRTIO_SND_R_PCM_INFO = 0x0100, + VIRTIO_SND_R_PCM_SET_PARAMS, + VIRTIO_SND_R_PCM_PREPARE, + VIRTIO_SND_R_PCM_RELEASE, + VIRTIO_SND_R_PCM_START, + VIRTIO_SND_R_PCM_STOP, + + /* channel map control request types */ + VIRTIO_SND_R_CHMAP_INFO = 0x0200, + + /* jack event types */ + VIRTIO_SND_EVT_JACK_CONNECTED = 0x1000, + VIRTIO_SND_EVT_JACK_DISCONNECTED, + + /* PCM event types */ + VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED = 0x1100, + VIRTIO_SND_EVT_PCM_XRUN, + + /* common status codes */ + VIRTIO_SND_S_OK = 0x8000, + VIRTIO_SND_S_BAD_MSG, + VIRTIO_SND_S_NOT_SUPP, + VIRTIO_SND_S_IO_ERR +}; + +/* common header */ +struct virtio_snd_hdr { + __le32 code; +}; + +/* event notification */ +struct virtio_snd_event { + /* VIRTIO_SND_EVT_XXX */ + struct virtio_snd_hdr hdr; + /* optional event data */ + __le32 data; +}; + +/* common control request to query an item information */ +struct virtio_snd_query_info { + /* VIRTIO_SND_R_XXX_INFO */ + struct virtio_snd_hdr hdr; + /* item start identifier */ + __le32 start_id; + /* item count to query */ + __le32 count; + /* item information size in bytes */ + __le32 size; +}; + +/* common item information header */ +struct virtio_snd_info { + /* function group node id (High Definition Audio Specification 7.1.2) */ + __le32 hda_fn_nid; +}; + +/******************************************************************************* + * JACK CONTROL MESSAGES + */ +struct virtio_snd_jack_hdr { + /* VIRTIO_SND_R_JACK_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::jacks - 1 */ + __le32 jack_id; +}; + +/* supported jack features */ +enum { + VIRTIO_SND_JACK_F_REMAP = 0 +}; + +struct virtio_snd_jack_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_JACK_F_XXX) */ + __le32 features; + /* pin configuration (High Definition Audio Specification 7.3.3.31) */ + __le32 hda_reg_defconf; + /* pin capabilities (High Definition Audio Specification 7.3.4.9) */ + __le32 hda_reg_caps; + /* current jack connection status (0: disconnected, 1: connected) */ + __u8 connected; + + __u8 padding[7]; +}; + +/* jack remapping control request */ +struct virtio_snd_jack_remap { + /* .code = VIRTIO_SND_R_JACK_REMAP */ + struct virtio_snd_jack_hdr hdr; + /* selected association number */ + __le32 association; + /* selected sequence number */ + __le32 sequence; +}; + +/******************************************************************************* + * PCM CONTROL MESSAGES + */ +struct virtio_snd_pcm_hdr { + /* VIRTIO_SND_R_PCM_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* supported PCM stream features */ +enum { + VIRTIO_SND_PCM_F_SHMEM_HOST = 0, + VIRTIO_SND_PCM_F_SHMEM_GUEST, + VIRTIO_SND_PCM_F_MSG_POLLING, + VIRTIO_SND_PCM_F_EVT_SHMEM_PERIODS, + VIRTIO_SND_PCM_F_EVT_XRUNS +}; + +/* supported PCM sample formats */ +enum { + /* analog formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_IMA_ADPCM = 0, /* 4 / 4 bits */ + VIRTIO_SND_PCM_FMT_MU_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_A_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_S18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_U18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_U20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_S24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_U24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_U20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_S24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_U24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_S32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT64, /* 64 / 64 bits */ + /* digital formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_DSD_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_DSD_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_DSD_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME /* 32 / 32 bits */ +}; + +/* supported PCM frame rates */ +enum { + VIRTIO_SND_PCM_RATE_5512 = 0, + VIRTIO_SND_PCM_RATE_8000, + VIRTIO_SND_PCM_RATE_11025, + VIRTIO_SND_PCM_RATE_16000, + VIRTIO_SND_PCM_RATE_22050, + VIRTIO_SND_PCM_RATE_32000, + VIRTIO_SND_PCM_RATE_44100, + VIRTIO_SND_PCM_RATE_48000, + VIRTIO_SND_PCM_RATE_64000, + VIRTIO_SND_PCM_RATE_88200, + VIRTIO_SND_PCM_RATE_96000, + VIRTIO_SND_PCM_RATE_176400, + VIRTIO_SND_PCM_RATE_192000, + VIRTIO_SND_PCM_RATE_384000 +}; + +struct virtio_snd_pcm_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* supported sample format bit map (1 << VIRTIO_SND_PCM_FMT_XXX) */ + __le64 formats; + /* supported frame rate bit map (1 << VIRTIO_SND_PCM_RATE_XXX) */ + __le64 rates; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* minimum # of supported channels */ + __u8 channels_min; + /* maximum # of supported channels */ + __u8 channels_max; + + __u8 padding[5]; +}; + +/* set PCM stream format */ +struct virtio_snd_pcm_set_params { + /* .code = VIRTIO_SND_R_PCM_SET_PARAMS */ + struct virtio_snd_pcm_hdr hdr; + /* size of the hardware buffer */ + __le32 buffer_bytes; + /* size of the hardware period */ + __le32 period_bytes; + /* selected feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* selected # of channels */ + __u8 channels; + /* selected sample format (VIRTIO_SND_PCM_FMT_XXX) */ + __u8 format; + /* selected frame rate (VIRTIO_SND_PCM_RATE_XXX) */ + __u8 rate; + + __u8 padding; +}; + +/******************************************************************************* + * PCM I/O MESSAGES + */ + +/* I/O request header */ +struct virtio_snd_pcm_xfer { + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* I/O request status */ +struct virtio_snd_pcm_status { + /* VIRTIO_SND_S_XXX */ + __le32 status; + /* current device latency */ + __le32 latency_bytes; +}; + +/******************************************************************************* + * CHANNEL MAP CONTROL MESSAGES + */ +struct virtio_snd_chmap_hdr { + /* VIRTIO_SND_R_CHMAP_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::chmaps - 1 */ + __le32 chmap_id; +}; + +/* standard channel position definition */ +enum { + VIRTIO_SND_CHMAP_NONE = 0, /* undefined */ + VIRTIO_SND_CHMAP_NA, /* silent */ + VIRTIO_SND_CHMAP_MONO, /* mono stream */ + VIRTIO_SND_CHMAP_FL, /* front left */ + VIRTIO_SND_CHMAP_FR, /* front right */ + VIRTIO_SND_CHMAP_RL, /* rear left */ + VIRTIO_SND_CHMAP_RR, /* rear right */ + VIRTIO_SND_CHMAP_FC, /* front center */ + VIRTIO_SND_CHMAP_LFE, /* low frequency (LFE) */ + VIRTIO_SND_CHMAP_SL, /* side left */ + VIRTIO_SND_CHMAP_SR, /* side right */ + VIRTIO_SND_CHMAP_RC, /* rear center */ + VIRTIO_SND_CHMAP_FLC, /* front left center */ + VIRTIO_SND_CHMAP_FRC, /* front right center */ + VIRTIO_SND_CHMAP_RLC, /* rear left center */ + VIRTIO_SND_CHMAP_RRC, /* rear right center */ + VIRTIO_SND_CHMAP_FLW, /* front left wide */ + VIRTIO_SND_CHMAP_FRW, /* front right wide */ + VIRTIO_SND_CHMAP_FLH, /* front left high */ + VIRTIO_SND_CHMAP_FCH, /* front center high */ + VIRTIO_SND_CHMAP_FRH, /* front right high */ + VIRTIO_SND_CHMAP_TC, /* top center */ + VIRTIO_SND_CHMAP_TFL, /* top front left */ + VIRTIO_SND_CHMAP_TFR, /* top front right */ + VIRTIO_SND_CHMAP_TFC, /* top front center */ + VIRTIO_SND_CHMAP_TRL, /* top rear left */ + VIRTIO_SND_CHMAP_TRR, /* top rear right */ + VIRTIO_SND_CHMAP_TRC, /* top rear center */ + VIRTIO_SND_CHMAP_TFLC, /* top front left center */ + VIRTIO_SND_CHMAP_TFRC, /* top front right center */ + VIRTIO_SND_CHMAP_TSL, /* top side left */ + VIRTIO_SND_CHMAP_TSR, /* top side right */ + VIRTIO_SND_CHMAP_LLFE, /* left LFE */ + VIRTIO_SND_CHMAP_RLFE, /* right LFE */ + VIRTIO_SND_CHMAP_BC, /* bottom center */ + VIRTIO_SND_CHMAP_BLC, /* bottom left center */ + VIRTIO_SND_CHMAP_BRC /* bottom right center */ +}; + +/* maximum possible number of channels */ +#define VIRTIO_SND_CHMAP_MAX_SIZE 18 + +struct virtio_snd_chmap_info { + /* common header */ + struct virtio_snd_info hdr; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* # of valid channel position values */ + __u8 channels; + /* channel position values (VIRTIO_SND_CHMAP_XXX) */ + __u8 positions[VIRTIO_SND_CHMAP_MAX_SIZE]; +}; + +#endif /* VIRTIO_SND_IF_H */ diff --git a/virtio_snd/Kbuild b/virtio_snd/Kbuild new file mode 100644 index 0000000..fb6474b --- /dev/null +++ b/virtio_snd/Kbuild @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0 + +virtio_snd-objs := \ + virtio_card.o \ + virtio_chmap.o \ + virtio_ctl_msg.o \ + virtio_dc.o \ + virtio_event.o \ + virtio_jack.o \ + virtio_opsy_ctl_msg.o \ + virtio_pcm.o \ + virtio_pcm_msg.o \ + virtio_pcm_ops.o + +obj-m += virtio_snd.o +LINUXINCLUDE := -I$(srctree)/../common-modules/virtual-device/uapi ${LINUXINCLUDE} diff --git a/virtio_snd/virtio_card.c b/virtio_snd/virtio_card.c new file mode 100644 index 0000000..afa908a --- /dev/null +++ b/virtio_snd/virtio_card.c @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/version.h> +#include <linux/virtio_config.h> +#include <sound/initval.h> + +#include "virtio_card.h" + +#ifndef VIRTIO_ID_SOUND +#define VIRTIO_ID_SOUND 25 +#endif + +static int virtsnd_find_vqs(struct virtio_snd *snd) +{ + int rc; + int i; + struct virtio_device *vdev = snd->vdev; + vq_callback_t *callbacks[VIRTIO_SND_VQ_MAX] = { 0 }; + const char *names[VIRTIO_SND_VQ_MAX] = { + "virtsnd-ctl", "virtsnd-event", "virtsnd-tx", "virtsnd-rx" + }; + struct virtqueue *vqs[VIRTIO_SND_VQ_MAX] = { 0 }; + unsigned int streams = 0; + + callbacks[VIRTIO_SND_VQ_CONTROL] = virtsnd_ctl_notify_cb; + callbacks[VIRTIO_SND_VQ_EVENT] = virtsnd_event_notify_cb; + + virtio_cread(vdev, struct virtio_snd_config, streams, &streams); + if (streams) { + callbacks[VIRTIO_SND_VQ_TX] = virtsnd_pcm_tx_notify_cb; + callbacks[VIRTIO_SND_VQ_RX] = virtsnd_pcm_rx_notify_cb; + } + +#if KERNEL_VERSION(4, 12, 0) <= LINUX_VERSION_CODE + rc = virtio_find_vqs(vdev, VIRTIO_SND_VQ_MAX, vqs, callbacks, names, + NULL); +#else + rc = vdev->config->find_vqs(vdev, VIRTIO_SND_VQ_MAX, vqs, callbacks, + names); +#endif + if (rc) { + dev_err(&vdev->dev, "Failed to initialize virtqueues"); + return rc; + } + + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) { + /* + * By default, disable callbacks for all queues except the + * control queue, since the device must be fully initialized + * first. + */ + if (i != VIRTIO_SND_VQ_CONTROL) + virtqueue_disable_cb(vqs[i]); + + snd->queues[i].vqueue = vqs[i]; + } + + rc = virtsnd_event_populate(snd); + if (rc) + return rc; + + return 0; +} + +static void virtsnd_enable_vqs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtqueue *vqueue; + + vqueue = snd->queues[VIRTIO_SND_VQ_EVENT].vqueue; + if (!virtqueue_enable_cb(vqueue)) + virtsnd_event_notify_cb(vqueue); + + if (snd->nsubstreams) { + vqueue = snd->queues[VIRTIO_SND_VQ_TX].vqueue; + if (!virtqueue_enable_cb(vqueue)) + dev_warn(&vdev->dev, + "Suspicious notification in the TX queue"); + vqueue = snd->queues[VIRTIO_SND_VQ_RX].vqueue; + if (!virtqueue_enable_cb(vqueue)) + dev_warn(&vdev->dev, + "Suspicious notification in the RX queue"); + } +} + +static void virtsnd_disable_vqs(struct virtio_snd *snd) +{ + int i; + unsigned long flags; + + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) { + struct virtio_snd_queue *queue = &snd->queues[i]; + + spin_lock_irqsave(&queue->lock, flags); + virtqueue_disable_cb(queue->vqueue); + queue->vqueue = NULL; + spin_unlock_irqrestore(&queue->lock, flags); + } +} + +static void virtsnd_flush_vqs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + + if (!list_empty(&snd->ctl_msgs)) { + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + unsigned long flags; + struct virtio_snd_msg *msg; + struct virtio_snd_msg *next; + + spin_lock_irqsave(&queue->lock, flags); + list_for_each_entry_safe(msg, next, &snd->ctl_msgs, list) { + struct virtio_snd_hdr *response = + sg_virt(&msg->sg_response); + + list_del(&msg->list); + + response->code = cpu_to_virtio32(vdev, + VIRTIO_SND_S_IO_ERR); + + complete(&msg->notify); + + virtsnd_ctl_msg_unref(vdev, msg); + } + spin_unlock_irqrestore(&queue->lock, flags); + } + + if (snd->event_msgs) + devm_kfree(&vdev->dev, snd->event_msgs); + + snd->event_msgs = NULL; +} + +static void virtsnd_reset_fn(struct work_struct *work) +{ + struct virtio_snd *snd = + container_of(work, struct virtio_snd, reset_work); + struct virtio_device *vdev = snd->vdev; + struct device *dev = &vdev->dev; + int rc; + + dev_info(dev, "Sound device needs reset"); + + rc = dev->bus->remove(dev); + if (rc) + dev_warn(dev, "bus->remove() failed: %d", rc); + + rc = dev->bus->probe(dev); + if (rc) + dev_err(dev, "bus->probe() failed: %d", rc); +} + +static int virtsnd_card_info(struct virtio_snd *snd) +{ + if (VIRTIO_HAS_OPSY_EXTENSION(snd, DEV_EXT_INFO)) { + int code; + + code = virtsnd_ctl_alsa_card_info(snd); + if (!code || code != -EOPNOTSUPP) + return code; + } + + strlcpy(snd->card->id, "viosnd", sizeof(snd->card->id)); + strlcpy(snd->card->driver, "virtio_snd", sizeof(snd->card->driver)); + strlcpy(snd->card->shortname, "VIOSND", sizeof(snd->card->shortname)); + strlcpy(snd->card->longname, "VirtIO Sound Card", + sizeof(snd->card->longname)); + + return 0; +} + +static int virtsnd_build_devs(struct virtio_snd *snd) +{ + static struct snd_device_ops ops = { 0 }; + struct virtio_device *vdev = snd->vdev; + int rc; + + /* query supported OPSY extensions */ + if (virtio_has_feature(vdev, VIRTIO_SND_F_OPSY_EXT)) { + rc = virtsnd_ctl_query_opsy_extensions(snd); + if (rc) + return rc; + } + + rc = snd_card_new(&vdev->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, + THIS_MODULE, 0, &snd->card); + if (rc < 0) + return rc; + + snd->card->private_data = snd; + + rc = virtsnd_card_info(snd); + if (rc) + return rc; + + rc = snd_device_new(snd->card, SNDRV_DEV_LOWLEVEL, snd, &ops); + if (rc < 0) + return rc; + + rc = virtsnd_jack_parse_cfg(snd); + if (rc) + return rc; + + rc = virtsnd_pcm_parse_cfg(snd); + if (rc) + return rc; + + rc = virtsnd_chmap_parse_cfg(snd); + if (rc) + return rc; + + if (VIRTIO_HAS_OPSY_EXTENSION(snd, DEV_CTLS)) { + rc = virtsnd_dc_parse_cfg(snd); + if (rc) + return rc; + } + + if (snd->njacks) { + rc = virtsnd_jack_build_devs(snd); + if (rc) + return rc; + } + + if (snd->nsubstreams) { + rc = virtsnd_pcm_build_devs(snd); + if (rc) + return rc; + } + + rc = virtsnd_chmap_build_devs(snd); + if (rc) + return rc; + + return snd_card_register(snd->card); +} + +static int virtsnd_validate(struct virtio_device *vdev) +{ + if (!vdev->config->get) { + dev_err(&vdev->dev, "Config access disabled"); + return -EINVAL; + } + + if (virtsnd_pcm_validate(vdev)) + return -EINVAL; + + return 0; +} + +static void virtsnd_remove(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + struct virtio_pcm *pcm; + struct virtio_pcm *pcm_next; + + virtsnd_disable_vqs(snd); + + virtsnd_flush_vqs(snd); + + if (snd->card) + snd_card_free(snd->card); + + vdev->config->reset(vdev); + vdev->config->del_vqs(vdev); + + list_for_each_entry_safe(pcm, pcm_next, &snd->pcm_list, list) { + unsigned int i; + + list_del(&pcm->list); + + for (i = 0; i < ARRAY_SIZE(pcm->streams); ++i) { + struct virtio_pcm_stream *stream = &pcm->streams[i]; + + if (stream->substreams) + devm_kfree(&vdev->dev, stream->substreams); + if (stream->chmaps) + devm_kfree(&vdev->dev, stream->chmaps); + } + + devm_kfree(&vdev->dev, pcm); + } + + if (snd->jacks) + devm_kfree(&vdev->dev, snd->jacks); + + if (snd->substreams) + devm_kfree(&vdev->dev, snd->substreams); + + if (snd->chmaps) + devm_kfree(&vdev->dev, snd->chmaps); + + snd->card = NULL; + snd->jacks = NULL; + snd->njacks = 0; + snd->substreams = NULL; + snd->nsubstreams = 0; + snd->chmaps = NULL; + snd->nchmaps = 0; +} + +static int virtsnd_probe(struct virtio_device *vdev) +{ + int rc; + unsigned int i; + struct virtio_snd *snd = vdev->priv; + + /* + * if we got here because the NEEDS_RESET status was set, we do not need + * to create the structure of the device. + */ + if (!snd) { + snd = devm_kzalloc(&vdev->dev, sizeof(*snd), GFP_KERNEL); + if (!snd) + return -ENOMEM; + + snd->vdev = vdev; + INIT_WORK(&snd->reset_work, virtsnd_reset_fn); + INIT_LIST_HEAD(&snd->ctl_msgs); + INIT_LIST_HEAD(&snd->pcm_list); + + vdev->priv = snd; + + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) + spin_lock_init(&snd->queues[i].lock); + } + + rc = virtsnd_find_vqs(snd); + if (rc) + goto on_failure; + + virtio_device_ready(vdev); + + rc = virtsnd_build_devs(snd); + if (rc) + goto on_failure; + + virtsnd_enable_vqs(snd); + +on_failure: + if (rc) + virtsnd_remove(vdev); + + return rc; +} + +static void virtsnd_config_changed(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + unsigned int status = vdev->config->get_status(vdev); + + if (status & VIRTIO_CONFIG_S_NEEDS_RESET) + schedule_work(&snd->reset_work); + else + dev_warn(&vdev->dev, "Sound device configuration was changed"); +} + +#ifdef CONFIG_PM_SLEEP +static int virtsnd_freeze(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + + virtsnd_disable_vqs(snd); + + virtsnd_flush_vqs(snd); + + vdev->config->reset(vdev); + vdev->config->del_vqs(vdev); + + return 0; +} + +static int virtsnd_restore(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + int rc; + + rc = virtsnd_find_vqs(snd); + if (rc) + return rc; + + virtio_device_ready(vdev); + + /* If the configuration has been changed, reset the device. */ + if (virtsnd_jack_check_cfg(snd)) + goto on_reset; + + if (virtsnd_pcm_check_cfg(snd)) + goto on_reset; + + if (virtsnd_chmap_check_cfg(snd)) + goto on_reset; + + /* If the configuration has not been changed, continue as usual. */ + virtsnd_enable_vqs(snd); + + if (snd->nsubstreams) { + rc = virtsnd_pcm_restore(snd); + if (rc) + return rc; + } + + return 0; + +on_reset: + dev_warn(&vdev->dev, "configuration has changed -> reset device\n"); + + virtsnd_disable_vqs(snd); + + schedule_work(&snd->reset_work); + + return 0; +} +#endif /* CONFIG_PM_SLEEP */ + +static struct virtio_device_id id_table[] = { + { VIRTIO_ID_SOUND, VIRTIO_DEV_ANY_ID }, + { 0 }, +}; + +static unsigned int features[] = { + VIRTIO_SND_F_OPSY_EXT +}; + +static struct virtio_driver virtsnd_driver = { + .driver.name = KBUILD_MODNAME, + .driver.owner = THIS_MODULE, + .id_table = id_table, + .feature_table = features, + .feature_table_size = ARRAY_SIZE(features), + .validate = virtsnd_validate, + .probe = virtsnd_probe, + .remove = virtsnd_remove, + .config_changed = virtsnd_config_changed, +#ifdef CONFIG_PM_SLEEP + .freeze = virtsnd_freeze, + .restore = virtsnd_restore, +#endif +}; + +static int __init init(void) +{ + return register_virtio_driver(&virtsnd_driver); +} +module_init(init); + +static void __exit fini(void) +{ + unregister_virtio_driver(&virtsnd_driver); +} +module_exit(fini); + +MODULE_DEVICE_TABLE(virtio, id_table); +MODULE_DESCRIPTION("Virtio sound card driver"); +MODULE_LICENSE("GPL"); diff --git a/virtio_snd/virtio_card.h b/virtio_snd/virtio_card.h new file mode 100644 index 0000000..7b9a354 --- /dev/null +++ b/virtio_snd/virtio_card.h @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#ifndef VIRTIO_SND_CARD_H +#define VIRTIO_SND_CARD_H + +#include <linux/virtio.h> +#include <sound/core.h> + +#include "virtio_snd.h" +#include "virtio_ctl_msg.h" +#include "virtio_pcm.h" + +#include "virtio_opsy.h" + +struct virtio_jack; +struct virtio_pcm_substream; +struct virtio_kctl_ctx; + +/** + * struct virtio_snd_queue - Virtqueue wrapper structure. + * @lock: Used to synchronize access to a virtqueue. + * @vqueue: Pointer to underlying virtqueue structure. + */ +struct virtio_snd_queue { + spinlock_t lock; + struct virtqueue *vqueue; +}; + +/** + * struct virtio_snd - Virtio sound card device representation. + * @vdev: Underlying virtio device. + * @queues: Virtqueue wrappers. + * @card: Kernel sound card device. + * @pcm_list: List of virtio PCM devices. + * @jacks: Virtio jacks. + * @njacks: Number of jacks. + * @substreams: Virtio PCM substreams. + * @nsubstreams: Number of PCM stream. + * @extensions: Supported OpSy extension bit map (1 << VIRTIO_SND_OPSY_F_XXX). + */ +struct virtio_snd { + struct virtio_device *vdev; + struct virtio_snd_queue queues[VIRTIO_SND_VQ_MAX]; + struct work_struct reset_work; + struct snd_card *card; + struct list_head ctl_msgs; + struct virtio_snd_event *event_msgs; + struct list_head pcm_list; + struct virtio_jack *jacks; + unsigned int njacks; + struct virtio_pcm_substream *substreams; + unsigned int nsubstreams; + struct virtio_snd_chmap_info *chmaps; + unsigned int nchmaps; + u32 extensions; + /* --- OpenSynergy extensions --------------------------------------- */ + struct virtio_kctl_ctx *kctl_ctx; + struct work_struct kctl_work; +}; + +static inline void +virtsnd_strlcpy(char *dst, const char *src, size_t max_size) +{ + strlcpy(dst, src, max_size); + dst[max_size - 1] = 0; +} + +static inline struct virtio_snd_queue * +virtsnd_control_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_CONTROL]; +} + +static inline struct virtio_snd_queue * +virtsnd_event_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_EVENT]; +} + +static inline struct virtio_snd_queue * +virtsnd_tx_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_TX]; +} + +static inline struct virtio_snd_queue * +virtsnd_rx_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_RX]; +} + +static inline struct virtio_snd_queue * +virtsnd_pcm_queue(struct virtio_pcm_substream *substream) +{ + if (substream->direction == SNDRV_PCM_STREAM_PLAYBACK) + return virtsnd_tx_queue(substream->snd); + else + return virtsnd_rx_queue(substream->snd); +} + +/* + * event related functions: + */ +int virtsnd_event_populate(struct virtio_snd *snd); + +void virtsnd_event_notify_cb(struct virtqueue *vqueue); + +/* + * jack related functions: + */ +int virtsnd_jack_parse_cfg(struct virtio_snd *snd); + +int virtsnd_jack_check_cfg(struct virtio_snd *snd); + +int virtsnd_jack_build_devs(struct virtio_snd *snd); + +void virtsnd_jack_event(struct virtio_snd *snd, + struct virtio_snd_event *event); + +/* + * channel map related functions: + */ +int virtsnd_chmap_parse_cfg(struct virtio_snd *snd); + +int virtsnd_chmap_check_cfg(struct virtio_snd *snd); + +int virtsnd_chmap_build_devs(struct virtio_snd *snd); + +/* + * device controls related functions: + */ +int virtsnd_dc_parse_cfg(struct virtio_snd *vsnd); + +void virtsnd_dc_event(struct virtio_snd *vsnd, struct virtio_snd_event *event); + +#endif /* VIRTIO_SND_CARD_H */ diff --git a/virtio_snd/virtio_chmap.c b/virtio_snd/virtio_chmap.c new file mode 100644 index 0000000..17dd8ae --- /dev/null +++ b/virtio_snd/virtio_chmap.c @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +static const u8 g_v2a_position_map[] = { + [VIRTIO_SND_CHMAP_NONE] = SNDRV_CHMAP_UNKNOWN, + [VIRTIO_SND_CHMAP_NA] = SNDRV_CHMAP_NA, + [VIRTIO_SND_CHMAP_MONO] = SNDRV_CHMAP_MONO, + [VIRTIO_SND_CHMAP_FL] = SNDRV_CHMAP_FL, + [VIRTIO_SND_CHMAP_FR] = SNDRV_CHMAP_FR, + [VIRTIO_SND_CHMAP_RL] = SNDRV_CHMAP_RL, + [VIRTIO_SND_CHMAP_RR] = SNDRV_CHMAP_RR, + [VIRTIO_SND_CHMAP_FC] = SNDRV_CHMAP_FC, + [VIRTIO_SND_CHMAP_LFE] = SNDRV_CHMAP_LFE, + [VIRTIO_SND_CHMAP_SL] = SNDRV_CHMAP_SL, + [VIRTIO_SND_CHMAP_SR] = SNDRV_CHMAP_SR, + [VIRTIO_SND_CHMAP_RC] = SNDRV_CHMAP_RC, + [VIRTIO_SND_CHMAP_FLC] = SNDRV_CHMAP_FLC, + [VIRTIO_SND_CHMAP_FRC] = SNDRV_CHMAP_FRC, + [VIRTIO_SND_CHMAP_RLC] = SNDRV_CHMAP_RLC, + [VIRTIO_SND_CHMAP_RRC] = SNDRV_CHMAP_RRC, + [VIRTIO_SND_CHMAP_FLW] = SNDRV_CHMAP_FLW, + [VIRTIO_SND_CHMAP_FRW] = SNDRV_CHMAP_FRW, + [VIRTIO_SND_CHMAP_FLH] = SNDRV_CHMAP_FLH, + [VIRTIO_SND_CHMAP_FCH] = SNDRV_CHMAP_FCH, + [VIRTIO_SND_CHMAP_FRH] = SNDRV_CHMAP_FRH, + [VIRTIO_SND_CHMAP_TC] = SNDRV_CHMAP_TC, + [VIRTIO_SND_CHMAP_TFL] = SNDRV_CHMAP_TFL, + [VIRTIO_SND_CHMAP_TFR] = SNDRV_CHMAP_TFR, + [VIRTIO_SND_CHMAP_TFC] = SNDRV_CHMAP_TFC, + [VIRTIO_SND_CHMAP_TRL] = SNDRV_CHMAP_TRL, + [VIRTIO_SND_CHMAP_TRR] = SNDRV_CHMAP_TRR, + [VIRTIO_SND_CHMAP_TRC] = SNDRV_CHMAP_TRC, + [VIRTIO_SND_CHMAP_TFLC] = SNDRV_CHMAP_TFLC, + [VIRTIO_SND_CHMAP_TFRC] = SNDRV_CHMAP_TFRC, + [VIRTIO_SND_CHMAP_TSL] = SNDRV_CHMAP_TSL, + [VIRTIO_SND_CHMAP_TSR] = SNDRV_CHMAP_TSR, + [VIRTIO_SND_CHMAP_LLFE] = SNDRV_CHMAP_LLFE, + [VIRTIO_SND_CHMAP_RLFE] = SNDRV_CHMAP_RLFE, + [VIRTIO_SND_CHMAP_BC] = SNDRV_CHMAP_BC, + [VIRTIO_SND_CHMAP_BLC] = SNDRV_CHMAP_BLC, + [VIRTIO_SND_CHMAP_BRC] = SNDRV_CHMAP_BRC +}; + +int virtsnd_chmap_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + int rc; + struct virtio_pcm *pcm; + struct virtio_pcm_stream *stream; + unsigned int i; + + virtio_cread(vdev, struct virtio_snd_config, chmaps, &snd->nchmaps); + if (!snd->nchmaps) + return 0; + + snd->chmaps = devm_kcalloc(&vdev->dev, snd->nchmaps, + sizeof(*snd->chmaps), GFP_KERNEL); + if (!snd->chmaps) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_CHMAP_INFO, 0, + snd->nchmaps, sizeof(*snd->chmaps), + snd->chmaps); + if (rc) + return rc; + + /* Count the number of channel maps per each pcm/stream. */ + for (i = 0; i < snd->nchmaps; ++i) { + struct virtio_snd_chmap_info *info = &snd->chmaps[i]; + unsigned int nid = le32_to_cpu(info->hdr.hda_fn_nid); + + pcm = virtsnd_pcm_find_or_create(snd, nid); + if (IS_ERR(pcm)) + return PTR_ERR(pcm); + + switch (info->direction) { + case VIRTIO_SND_D_OUTPUT: { + stream = &pcm->streams[SNDRV_PCM_STREAM_PLAYBACK]; + break; + } + case VIRTIO_SND_D_INPUT: { + stream = &pcm->streams[SNDRV_PCM_STREAM_CAPTURE]; + break; + } + default: { + dev_err(&vdev->dev, + "chmap #%u: unknown direction (%u)", i, + info->direction); + return -EINVAL; + } + } + + stream->nchmaps++; + } + + return 0; +} + +int virtsnd_chmap_check_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + int rc; + struct virtio_snd_chmap_info *info; + unsigned int i; + + virtio_cread(vdev, struct virtio_snd_config, chmaps, &i); + if (snd->nchmaps != i) { + dev_warn(&vdev->dev, + "config: number of channel maps has changed (%u->%u)", + snd->nchmaps, i); + return -EINVAL; + } + + if (!snd->chmaps) + return 0; + + info = devm_kcalloc(&vdev->dev, snd->nchmaps, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_CHMAP_INFO, 0, + snd->nchmaps, sizeof(*info), info); + if (rc) + goto on_failure; + + for (i = 0; i < snd->nchmaps; ++i) + if (memcmp(&snd->chmaps[i], &info[i], sizeof(*info)) != 0) { + dev_warn(&vdev->dev, + "config: channel map #%u has changed", i); + rc = -EINVAL; + break; + } + +on_failure: + devm_kfree(&vdev->dev, info); + + return rc; +} + +static int virtsnd_chmap_add_ctls(struct snd_pcm *pcm, int direction, + struct virtio_pcm_stream *stream) +{ + unsigned int i; + int max_channels = 0; + + for (i = 0; i < stream->nchmaps; i++) + if (max_channels < stream->chmaps[i].channels) + max_channels = stream->chmaps[i].channels; + + return snd_pcm_add_chmap_ctls(pcm, direction, stream->chmaps, + max_channels, 0, NULL); +} + +int virtsnd_chmap_build_devs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *pcm; + struct virtio_pcm_stream *stream; + unsigned int i; + int rc; + + /* Allocate channel map elements per each pcm/stream. */ + list_for_each_entry(pcm, &snd->pcm_list, list) { + for (i = 0; i < ARRAY_SIZE(pcm->streams); ++i) { + stream = &pcm->streams[i]; + + if (!stream->nchmaps) + continue; + + stream->chmaps = devm_kcalloc(&vdev->dev, + stream->nchmaps + 1, + sizeof(*stream->chmaps), + GFP_KERNEL); + if (!stream->chmaps) + return -ENOMEM; + + stream->nchmaps = 0; + } + } + + /* Initialize channel maps per each pcm/stream. */ + for (i = 0; i < snd->nchmaps; ++i) { + struct virtio_snd_chmap_info *info = &snd->chmaps[i]; + unsigned int nid = le32_to_cpu(info->hdr.hda_fn_nid); + unsigned int channels = info->channels; + unsigned int ch; + struct snd_pcm_chmap_elem *chmap; + + pcm = virtsnd_pcm_find(snd, nid); + if (IS_ERR(pcm)) + return PTR_ERR(pcm); + + if (info->direction == VIRTIO_SND_D_OUTPUT) + stream = &pcm->streams[SNDRV_PCM_STREAM_PLAYBACK]; + else + stream = &pcm->streams[SNDRV_PCM_STREAM_CAPTURE]; + + chmap = &stream->chmaps[stream->nchmaps++]; + + if (channels > ARRAY_SIZE(chmap->map)) + channels = ARRAY_SIZE(chmap->map); + + chmap->channels = channels; + + for (ch = 0; ch < channels; ++ch) { + u8 position = info->positions[ch]; + + if (position >= ARRAY_SIZE(g_v2a_position_map)) + return -EINVAL; + + chmap->map[ch] = g_v2a_position_map[position]; + } + } + + list_for_each_entry(pcm, &snd->pcm_list, list) { + if (!pcm->pcm) + continue; + + for (i = 0; i < ARRAY_SIZE(pcm->streams); ++i) { + stream = &pcm->streams[i]; + + if (!stream->nchmaps) + continue; + + rc = virtsnd_chmap_add_ctls(pcm->pcm, i, stream); + if (rc) + return rc; + } + } + + return 0; +} diff --git a/virtio_snd/virtio_ctl_msg.c b/virtio_snd/virtio_ctl_msg.c new file mode 100644 index 0000000..4b743a5 --- /dev/null +++ b/virtio_snd/virtio_ctl_msg.c @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/moduleparam.h> +#include <linux/virtio_config.h> + +#include "virtio_card.h" +#include "virtio_ctl_msg.h" + +static int msg_timeout_ms = 1000; +module_param(msg_timeout_ms, int, 0644); +MODULE_PARM_DESC(msg_timeout_ms, "Message completion timeout in milliseconds"); + +struct virtio_snd_msg *virtsnd_ctl_msg_alloc(struct virtio_device *vdev, + size_t request_size, + size_t response_size, gfp_t gfp) +{ + struct virtio_snd_msg *msg; + + msg = devm_kzalloc(&vdev->dev, + sizeof(*msg) + request_size + response_size, + gfp); + if (!msg) + return ERR_PTR(-ENOMEM); + + sg_init_one(&msg->sg_request, (u8 *)msg + sizeof(*msg), request_size); + sg_init_one(&msg->sg_response, (u8 *)msg + sizeof(*msg) + request_size, + response_size); + + init_completion(&msg->notify); + atomic_set(&msg->ref_count, 1); + + return msg; +} + +int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg) +{ + int rc; + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + struct scatterlist *psgs[4]; + unsigned int out_nsgs = 1; + unsigned int in_nsgs = 1; + unsigned int nsgs = 0; + bool notify = false; + unsigned long flags; + + psgs[nsgs++] = &msg->sg_request; + if (msg->sg_request_ext) { + psgs[nsgs++] = msg->sg_request_ext; + out_nsgs++; + } + psgs[nsgs++] = &msg->sg_response; + if (msg->sg_response_ext) { + psgs[nsgs++] = msg->sg_response_ext; + in_nsgs++; + } + + spin_lock_irqsave(&queue->lock, flags); + if (queue->vqueue) + rc = virtqueue_add_sgs(queue->vqueue, psgs, out_nsgs, in_nsgs, + msg, GFP_ATOMIC); + else + rc = -EIO; + if (!rc) { + notify = virtqueue_kick_prepare(queue->vqueue); + list_add_tail(&msg->list, &snd->ctl_msgs); + } + spin_unlock_irqrestore(&queue->lock, flags); + + if (rc) + goto on_failure; + + if (notify) + if (!virtqueue_notify(queue->vqueue)) + goto on_failure; + + return 0; + +on_failure: + virtsnd_ctl_msg_unref(snd->vdev, msg); + + return -EIO; +} + +int virtsnd_ctl_msg_send_sync(struct virtio_snd *snd, + struct virtio_snd_msg *msg) +{ + int code; + struct virtio_device *vdev = snd->vdev; + unsigned int js = msecs_to_jiffies(msg_timeout_ms); + struct virtio_snd_hdr *response; + + virtsnd_ctl_msg_ref(vdev, msg); + + code = virtsnd_ctl_msg_send(snd, msg); + if (code) + goto on_failure; + + code = wait_for_completion_interruptible_timeout(&msg->notify, js); + if (code <= 0) { + if (!code) { + dev_err(&vdev->dev, "control message timeout"); + code = -EIO; + } + + goto on_failure; + } + + response = sg_virt(&msg->sg_response); + + switch (le32_to_cpu(response->code)) { + case VIRTIO_SND_S_OK: + code = 0; + break; + case VIRTIO_SND_S_BAD_MSG: + code = -EINVAL; + break; + case VIRTIO_SND_S_NOT_SUPP: + code = -EOPNOTSUPP; + break; + case VIRTIO_SND_S_IO_ERR: + code = -EIO; + break; + default: + code = -EPERM; + break; + } + +on_failure: + virtsnd_ctl_msg_unref(vdev, msg); + + return code; +} + +int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id, + int count, size_t size, void *info) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_query_info *query; + struct scatterlist sg_response_ext; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*query), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + query = sg_virt(&msg->sg_request); + query->hdr.code = cpu_to_virtio32(vdev, command); + query->start_id = cpu_to_virtio32(vdev, start_id); + query->count = cpu_to_virtio32(vdev, count); + query->size = cpu_to_virtio32(vdev, size); + + sg_init_one(&sg_response_ext, info, count * size); + msg->sg_response_ext = &sg_response_ext; + + return virtsnd_ctl_msg_send_sync(snd, msg); +} + +void virtsnd_ctl_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + while (queue->vqueue) { + virtqueue_disable_cb(queue->vqueue); + + for (;;) { + struct virtio_snd_msg *msg; + u32 length; + + msg = virtqueue_get_buf(queue->vqueue, &length); + if (!msg) + break; + + list_del(&msg->list); + complete(&msg->notify); + + virtsnd_ctl_msg_unref(snd->vdev, msg); + } + + if (unlikely(virtqueue_is_broken(queue->vqueue))) + break; + + if (virtqueue_enable_cb(queue->vqueue)) + break; + } + spin_unlock_irqrestore(&queue->lock, flags); +} diff --git a/virtio_snd/virtio_ctl_msg.h b/virtio_snd/virtio_ctl_msg.h new file mode 100644 index 0000000..8a38bad --- /dev/null +++ b/virtio_snd/virtio_ctl_msg.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#ifndef VIRTIO_SND_MSG_H +#define VIRTIO_SND_MSG_H + +#include <linux/atomic.h> +#include <linux/virtio.h> + +struct virtio_snd; + +/** + * struct virtio_snd_msg - Device message common representation. + * @sg_request: Scattergather element containing a device request (header). + * @sg_request_ext: Scattergather element containing optinal request payload. + * @sg_response: Scattergather element containing a device response (status). + * @sg_response_ext: Scattergather element containing optinal response payload. + * @notify: Request completed notification. + * @ref_count: Reference count used to manage a message lifetime. + */ +struct virtio_snd_msg { +/* public: */ + struct scatterlist sg_request; + struct scatterlist *sg_request_ext; + struct scatterlist sg_response; + struct scatterlist *sg_response_ext; +/* private: internal use only */ + struct list_head list; + struct completion notify; + atomic_t ref_count; +}; + +static inline void virtsnd_ctl_msg_ref(struct virtio_device *vdev, + struct virtio_snd_msg *msg) +{ + atomic_inc(&msg->ref_count); +} + +static inline void virtsnd_ctl_msg_unref(struct virtio_device *vdev, + struct virtio_snd_msg *msg) +{ + if (!atomic_dec_return(&msg->ref_count)) + devm_kfree(&vdev->dev, msg); +} + +struct virtio_snd_msg *virtsnd_ctl_msg_alloc(struct virtio_device *vdev, + size_t request_size, + size_t response_size, gfp_t gfp); + +int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg); + +int virtsnd_ctl_msg_send_sync(struct virtio_snd *snd, + struct virtio_snd_msg *msg); + +int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id, + int count, size_t size, void *info); + +void virtsnd_ctl_notify_cb(struct virtqueue *vqueue); + +#endif /* VIRTIO_SND_MSG_H */ diff --git a/virtio_snd/virtio_dc.c b/virtio_snd/virtio_dc.c new file mode 100644 index 0000000..7bc91d8 --- /dev/null +++ b/virtio_snd/virtio_dc.c @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <sound/control.h> +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +/** + * struct virtio_kctl - virtio device control representation. + * @kctl: Kernel device control. + * @info: Device control information. + * @enum_values: Values for the ENUMERATED control type. + */ +struct virtio_kctl { + struct snd_kcontrol *kctl; + struct virtio_snd_dc_info *info; + struct virtio_snd_dc_enum_value *enum_values; +}; + +/** + * struct virtio_kctl_ctx - device control context. + * @events_enabled: Event handling state. + * @kctls: Virtio device controls. + * @nkctls: Number of device controls. + */ +struct virtio_kctl_ctx { + atomic_t events_enabled; + struct virtio_kctl *kctls; + unsigned int nkctls; +}; + +/** + * struct virtio_snd_dc_info - virtio device control information. + * @events_enabled: Event handling state. + * @hdr: Common virtio item information header. + * @elem_info: ALSA element information (uapi/sound/asound.h). + */ +struct virtio_snd_dc_info { + struct virtio_snd_info hdr; + struct snd_ctl_elem_info elem_info; +}; + +static int virtsnd_dc_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct virtio_snd *snd = kcontrol->private_data; + struct virtio_kctl_ctx *ctx = snd->kctl_ctx; + struct virtio_kctl *kctl = &ctx->kctls[kcontrol->private_value]; + struct snd_ctl_elem_info *info = &kctl->info->elem_info; + + if (info->type == SNDRV_CTL_ELEM_TYPE_ENUMERATED) { + unsigned int item = uinfo->value.enumerated.item; + + if (item >= info->value.enumerated.items) + return -EINVAL; + + strlcpy(info->value.enumerated.name, + kctl->enum_values[item].name, + sizeof(info->value.enumerated.name)); + } + + memcpy(uinfo, info, sizeof(*uinfo)); + + uinfo->id = info->id; + + return 0; +} + +static int virtsnd_dc_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct virtio_snd *snd = kcontrol->private_data; + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_dc_hdr *hdr; + unsigned int subcid = snd_ctl_get_ioff(kcontrol, &ucontrol->id); + struct scatterlist sg; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + hdr = sg_virt(&msg->sg_request); + hdr->hdr.code = cpu_to_virtio32(vdev, VIRTIO_SND_R_DC_READ); + hdr->control_id = cpu_to_virtio16(vdev, kcontrol->private_value); + hdr->subcontrol_id = cpu_to_virtio16(vdev, subcid); + + sg_init_one(&sg, ucontrol, sizeof(*ucontrol)); + msg->sg_response_ext = &sg; + + return virtsnd_ctl_msg_send_sync(snd, msg); +} + +static int virtsnd_dc_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct virtio_snd *snd = kcontrol->private_data; + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_dc_hdr *hdr; + unsigned int subcid = snd_ctl_get_ioff(kcontrol, &ucontrol->id); + struct scatterlist sg; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + hdr = sg_virt(&msg->sg_request); + hdr->hdr.code = cpu_to_virtio32(vdev, VIRTIO_SND_R_DC_WRITE); + hdr->control_id = cpu_to_virtio16(vdev, kcontrol->private_value); + hdr->subcontrol_id = cpu_to_virtio16(vdev, subcid); + + sg_init_one(&sg, ucontrol, sizeof(*ucontrol)); + msg->sg_request_ext = &sg; + + return virtsnd_ctl_msg_send_sync(snd, msg); +} + +static int virtsnd_dc_tlv_op(struct snd_kcontrol *kcontrol, int op_flag, + unsigned int size, unsigned int *utlv) +{ + struct virtio_snd *snd = kcontrol->private_data; + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_dc_hdr *hdr; + unsigned int cmd = 0; + unsigned int *tlv = NULL; + struct scatterlist sg_request_ext; + struct scatterlist sg_response_ext; + int code; + + switch (op_flag) { + case SNDRV_CTL_TLV_OP_READ: { + cmd = VIRTIO_SND_R_DC_TLV_READ; + break; + } + case SNDRV_CTL_TLV_OP_WRITE: { + cmd = VIRTIO_SND_R_DC_TLV_WRITE; + break; + } + case SNDRV_CTL_TLV_OP_CMD: { + cmd = VIRTIO_SND_R_DC_TLV_COMMAND; + break; + } + default: { + return -EINVAL; + } + } + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + hdr = sg_virt(&msg->sg_request); + hdr->hdr.code = cpu_to_virtio32(vdev, cmd); + hdr->control_id = cpu_to_virtio16(vdev, kcontrol->private_value); + + tlv = devm_kzalloc(&vdev->dev, size, GFP_KERNEL); + if (!tlv) + return -ENOMEM; + + if (cmd == VIRTIO_SND_R_DC_TLV_READ) { + sg_init_one(&sg_response_ext, tlv, size); + msg->sg_response_ext = &sg_response_ext; + } else { + if (copy_from_user(tlv, utlv, size)) { + code = -EFAULT; + goto on_failure; + } + + sg_init_one(&sg_request_ext, tlv, size); + msg->sg_request_ext = &sg_request_ext; + } + + code = virtsnd_ctl_msg_send_sync(snd, msg); + if (!code) + if (cmd == VIRTIO_SND_R_DC_TLV_READ) + if (copy_to_user(utlv, tlv, size)) + code = -EFAULT; + +on_failure: + devm_kfree(&vdev->dev, tlv); + + return code; +} + +static int virtsnd_dc_query_enum_info(struct virtio_snd *snd, unsigned int cid, + unsigned int nvalues) +{ + struct virtio_kctl_ctx *ctx = snd->kctl_ctx; + struct virtio_device *vdev = snd->vdev; + struct virtio_kctl *kctl = &ctx->kctls[cid]; + struct virtio_snd_msg *msg; + struct virtio_snd_dc_hdr *hdr; + struct virtio_snd_dc_enum_value *values; + struct scatterlist sg_response_ext; + int code; + + values = devm_kcalloc(&vdev->dev, nvalues, sizeof(*values), GFP_KERNEL); + if (!values) + return -ENOMEM; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) { + devm_kfree(&vdev->dev, values); + return PTR_ERR(msg); + } + + hdr = sg_virt(&msg->sg_request); + hdr->hdr.code = cpu_to_virtio32(vdev, VIRTIO_SND_R_DC_ENUM_INFO); + hdr->control_id = cpu_to_virtio16(vdev, cid); + + sg_init_one(&sg_response_ext, values, nvalues * sizeof(*values)); + msg->sg_response_ext = &sg_response_ext; + + code = virtsnd_ctl_msg_send_sync(snd, msg); + if (code) { + dev_warn(&vdev->dev, + "Failed to query enumerated information: %d\n", + code); + devm_kfree(&vdev->dev, values); + return code; + } + + kctl->enum_values = values; + + return 0; +} + +static void virtsnd_dc_work(struct work_struct *work) +{ + struct virtio_snd *snd = + container_of(work, struct virtio_snd, kctl_work); + struct virtio_kctl_ctx *ctx = snd->kctl_ctx; + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_dc_info *info; + unsigned int i; + unsigned int tlv_mask = SNDRV_CTL_ELEM_ACCESS_TLV_READ | + SNDRV_CTL_ELEM_ACCESS_TLV_WRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_COMMAND; + int code; + + info = devm_kcalloc(&vdev->dev, ctx->nkctls, sizeof(*info), GFP_KERNEL); + if (!info) + return; + + code = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_DC_INFO, 0, ctx->nkctls, + sizeof(*info), info); + if (code) { + dev_warn(&vdev->dev, + "Failed to query control element information: %d\n", + code); + devm_kfree(&vdev->dev, info); + return; + } + + for (i = 0; i < ctx->nkctls; ++i) { + struct virtio_kctl *kctl = &ctx->kctls[i]; + struct snd_ctl_elem_info *elem_info = &info[i].elem_info; + struct snd_kcontrol_new kctl_new; + + kctl->info = &info[i]; + + if (elem_info->type == SNDRV_CTL_ELEM_TYPE_ENUMERATED) { + unsigned int nvalues = + elem_info->value.enumerated.items; + + code = virtsnd_dc_query_enum_info(snd, i, nvalues); + if (code) + continue; + } + + memset(&kctl_new, 0, sizeof(kctl_new)); + + kctl_new.iface = elem_info->id.iface; + if (kctl_new.iface == SNDRV_CTL_ELEM_IFACE_PCM) + kctl_new.device = le32_to_cpu(info[i].hdr.hda_fn_nid); + + kctl_new.name = elem_info->id.name; + kctl_new.index = elem_info->id.index; + + elem_info->access &= ~(SNDRV_CTL_ELEM_ACCESS_LOCK | + SNDRV_CTL_ELEM_ACCESS_OWNER | + SNDRV_CTL_ELEM_ACCESS_USER); + if (elem_info->access & tlv_mask) { + elem_info->access |= SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK; + kctl_new.tlv.c = virtsnd_dc_tlv_op; + } + + kctl_new.access = elem_info->access; + + kctl_new.info = virtsnd_dc_info; + kctl_new.get = virtsnd_dc_get; + kctl_new.put = virtsnd_dc_put; + + kctl->kctl = snd_ctl_new1(&kctl_new, snd); + if (!kctl->kctl) { + dev_warn(&vdev->dev, + "Failed to create a control [#%u]\n", i); + continue; + } + + kctl->kctl->private_value = i; + + code = snd_ctl_add(snd->card, kctl->kctl); + if (code) + dev_warn(&vdev->dev, + "Failed to add a control [#%u]: %d\n", i, + code); + } + + atomic_set(&ctx->events_enabled, 1); +} + +int virtsnd_dc_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_kctl_ctx *ctx; + unsigned int nkctls; + + virtio_cread(vdev, struct virtio_snd_config, controls, &nkctls); + if (!nkctls) + return 0; + + ctx = devm_kzalloc(&vdev->dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + ctx->nkctls = nkctls; + ctx->kctls = devm_kcalloc(&vdev->dev, nkctls, sizeof(*ctx->kctls), + GFP_KERNEL); + if (!ctx->kctls) + return -ENOMEM; + + snd->kctl_ctx = ctx; + INIT_WORK(&snd->kctl_work, virtsnd_dc_work); + + schedule_work(&snd->kctl_work); + + return 0; +} + +void virtsnd_dc_event(struct virtio_snd *snd, struct virtio_snd_event *event) +{ + struct virtio_kctl_ctx *ctx = snd->kctl_ctx; + struct virtio_snd_dc_event *dce = + (struct virtio_snd_dc_event *)event; + struct virtio_kctl *kctl; + unsigned int cid = le16_to_cpu(dce->control_id); + + if (!atomic_read(&ctx->events_enabled) || cid >= ctx->nkctls) + return; + + kctl = &ctx->kctls[cid]; + + snd_ctl_notify(snd->card, le16_to_cpu(dce->mask), &kctl->kctl->id); +} diff --git a/virtio_snd/virtio_event.c b/virtio_snd/virtio_event.c new file mode 100644 index 0000000..22ef6ce --- /dev/null +++ b/virtio_snd/virtio_event.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include "virtio_card.h" + +static void virtsnd_event_dispatch(struct virtio_snd *snd, + struct virtio_snd_event *event) +{ + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_JACK_CONNECTED: + case VIRTIO_SND_EVT_JACK_DISCONNECTED: { + virtsnd_jack_event(snd, event); + break; + } + case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: + case VIRTIO_SND_EVT_PCM_XRUN: { + virtsnd_pcm_event(snd, event); + break; + } + case VIRTIO_SND_EVT_DC_NOTIFY: { + virtsnd_dc_event(snd, event); + break; + } + default: { + break; + } + } +} + +static int virtsnd_event_send(struct virtqueue *vqueue, + struct virtio_snd_event *event, bool notify, + gfp_t gfp) +{ + int rc; + struct scatterlist sg; + struct scatterlist *psgs[1] = { &sg }; + + /* reset event content */ + memset(event, 0, sizeof(*event)); + + sg_init_one(&sg, event, sizeof(*event)); + + rc = virtqueue_add_sgs(vqueue, psgs, 0, 1, event, gfp); + if (rc) + return rc; + + if (notify) + if (virtqueue_kick_prepare(vqueue)) + if (!virtqueue_notify(vqueue)) + return -EIO; + + return 0; +} + +int virtsnd_event_populate(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtqueue *vqueue = virtsnd_event_queue(snd)->vqueue; + unsigned int nevents; + unsigned int i; + + nevents = virtqueue_get_vring_size(vqueue); + + snd->event_msgs = devm_kcalloc(&vdev->dev, nevents, + sizeof(*snd->event_msgs), GFP_KERNEL); + if (!snd->event_msgs) + return -ENOMEM; + + for (i = 0; i < nevents; ++i) { + int rc; + + rc = virtsnd_event_send(vqueue, &snd->event_msgs[i], + false, GFP_KERNEL); + if (rc) + return rc; + } + + return 0; +} + +void virtsnd_event_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + struct virtio_snd_queue *queue = virtsnd_event_queue(snd); + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + while (queue->vqueue) { + virtqueue_disable_cb(queue->vqueue); + + for (;;) { + struct virtio_snd_event *event; + u32 length; + + event = virtqueue_get_buf(queue->vqueue, &length); + if (!event) + break; + + virtsnd_event_dispatch(snd, event); + + virtsnd_event_send(queue->vqueue, event, true, + GFP_ATOMIC); + } + + if (unlikely(virtqueue_is_broken(queue->vqueue))) + break; + + if (virtqueue_enable_cb(queue->vqueue)) + break; + } + spin_unlock_irqrestore(&queue->lock, flags); +} diff --git a/virtio_snd/virtio_jack.c b/virtio_snd/virtio_jack.c new file mode 100644 index 0000000..fdc855c --- /dev/null +++ b/virtio_snd/virtio_jack.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/virtio_config.h> +#include <sound/jack.h> +#include <sound/hda_verbs.h> + +#include "virtio_card.h" + +/** + * struct virtio_jack - Virtio jack representation. + * @jack: Kernel jack control. + * @nid: Functional group node identifier. + * @features: Jack virtio feature bit map (1 << VIRTIO_SND_JACK_F_XXX). + * @defconf: Pin default configuration value. + * @caps: Pin capabilities value. + * @connected: Current jack connection status. + * @type: Kernel jack type (SND_JACK_XXX). + */ +struct virtio_jack { + struct snd_jack *jack; + unsigned int nid; + unsigned int features; + unsigned int defconf; + unsigned int caps; + bool connected; + int type; +}; + +static const char *virtsnd_jack_get_label(struct virtio_jack *jack) +{ + unsigned int defconf = jack->defconf; + unsigned int device = + (defconf & AC_DEFCFG_DEVICE) >> AC_DEFCFG_DEVICE_SHIFT; + unsigned int location = + (defconf & AC_DEFCFG_LOCATION) >> AC_DEFCFG_LOCATION_SHIFT; + + switch (device) { + case AC_JACK_LINE_OUT: + return "Line Out"; + case AC_JACK_SPEAKER: + return "Speaker"; + case AC_JACK_HP_OUT: + return "Headphone"; + case AC_JACK_CD: + return "CD"; + case AC_JACK_SPDIF_OUT: + case AC_JACK_DIG_OTHER_OUT: + if (location == AC_JACK_LOC_HDMI) + return "HDMI Out"; + else + return "SPDIF Out"; + case AC_JACK_LINE_IN: + return "Line"; + case AC_JACK_AUX: + return "Aux"; + case AC_JACK_MIC_IN: + return "Mic"; + case AC_JACK_SPDIF_IN: + return "SPDIF In"; + case AC_JACK_DIG_OTHER_IN: + return "Digital In"; + default: + return "Misc"; + } +} + +static int virtsnd_jack_get_type(struct virtio_jack *jack) +{ + unsigned int defconf = jack->defconf; + unsigned int device = + (defconf & AC_DEFCFG_DEVICE) >> AC_DEFCFG_DEVICE_SHIFT; + + switch (device) { + case AC_JACK_LINE_OUT: + case AC_JACK_SPEAKER: + return SND_JACK_LINEOUT; + case AC_JACK_HP_OUT: + return SND_JACK_HEADPHONE; + case AC_JACK_SPDIF_OUT: + case AC_JACK_DIG_OTHER_OUT: + return SND_JACK_AVOUT; + case AC_JACK_MIC_IN: + return SND_JACK_MICROPHONE; + default: + return SND_JACK_LINEIN; + } +} + +int virtsnd_jack_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + int code; + unsigned int i; + struct virtio_snd_jack_info *info; + + virtio_cread(vdev, struct virtio_snd_config, jacks, &snd->njacks); + if (!snd->njacks) + return 0; + + snd->jacks = devm_kcalloc(&vdev->dev, snd->njacks, sizeof(*snd->jacks), + GFP_KERNEL); + if (!snd->jacks) + return -ENOMEM; + + info = devm_kcalloc(&vdev->dev, snd->njacks, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + code = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_JACK_INFO, 0, + snd->njacks, sizeof(*info), info); + if (code) + return code; + + for (i = 0; i < snd->njacks; ++i) { + struct virtio_jack *jack = &snd->jacks[i]; + struct virtio_pcm *pcm; + + jack->nid = le32_to_cpu(info[i].hdr.hda_fn_nid); + jack->features = le32_to_cpu(info[i].features); + jack->defconf = le32_to_cpu(info[i].hda_reg_defconf); + jack->caps = le32_to_cpu(info[i].hda_reg_caps); + jack->connected = info[i].connected; + + pcm = virtsnd_pcm_find_or_create(snd, jack->nid); + if (IS_ERR(pcm)) + return PTR_ERR(pcm); + } + + devm_kfree(&vdev->dev, info); + + return 0; +} + +static int virtsnd_jack_check_entity_cfg(struct virtio_jack *jack, + struct virtio_snd_jack_info *info) +{ + if (jack->nid != le32_to_cpu(info->hdr.hda_fn_nid)) + return -EINVAL; + if (jack->defconf != le32_to_cpu(info->hda_reg_defconf)) + return -EINVAL; + + jack->features = le32_to_cpu(info->features); + jack->caps = le32_to_cpu(info->hda_reg_caps); + jack->connected = info->connected; + + return 0; +} + +int virtsnd_jack_check_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + int rc; + unsigned int i; + struct virtio_snd_jack_info *info; + + virtio_cread(vdev, struct virtio_snd_config, jacks, &i); + if (snd->njacks != i) { + dev_warn(&vdev->dev, + "config: number of jacks has changed (%u->%u)", + snd->njacks, i); + return -EINVAL; + } + + if (!snd->njacks) + return 0; + + info = devm_kcalloc(&vdev->dev, snd->njacks, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_JACK_INFO, 0, + snd->njacks, sizeof(*info), info); + if (rc) + goto on_failure; + + for (i = 0; i < snd->njacks; ++i) { + struct virtio_jack *jack = &snd->jacks[i]; + + rc = virtsnd_jack_check_entity_cfg(jack, &info[i]); + if (rc) { + dev_warn(&vdev->dev, + "config: jack#%u configuration has changed", i); + break; + } + + snd_jack_report(jack->jack, jack->connected ? jack->type : 0); + } + +on_failure: + devm_kfree(&vdev->dev, info); + + return rc; +} + +int virtsnd_jack_build_devs(struct virtio_snd *snd) +{ + unsigned int i; + int code; + + for (i = 0; i < snd->njacks; ++i) { + struct virtio_jack *jack = &snd->jacks[i]; + + jack->type = virtsnd_jack_get_type(jack); + + code = snd_jack_new(snd->card, virtsnd_jack_get_label(jack), + jack->type, &jack->jack, true, true); + if (code) + return code; + + if (!jack->jack) + continue; + + jack->jack->private_data = jack; + + snd_jack_report(jack->jack, + jack->connected ? jack->type : 0); + } + + return 0; +} + +void virtsnd_jack_event(struct virtio_snd *snd, struct virtio_snd_event *event) +{ + unsigned int jack_id = le32_to_cpu(event->data); + struct virtio_jack *jack; + + if (jack_id >= snd->njacks) + return; + + jack = &snd->jacks[jack_id]; + + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_JACK_CONNECTED: { + jack->connected = true; + break; + } + case VIRTIO_SND_EVT_JACK_DISCONNECTED: { + jack->connected = false; + break; + } + default: { + return; + } + } + + snd_jack_report(jack->jack, jack->connected ? jack->type : 0); +} diff --git a/virtio_snd/virtio_opsy.h b/virtio_snd/virtio_opsy.h new file mode 100644 index 0000000..fd8ecee --- /dev/null +++ b/virtio_snd/virtio_opsy.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#ifndef VIRTIO_SND_OPSY_H +#define VIRTIO_SND_OPSY_H + +#define VIRTIO_HAS_OPSY_EXTENSION(_snd_, _extension_) \ + ((_snd_)->extensions & (1U << (VIRTIO_SND_OPSY_F_ ## _extension_))) + +int virtsnd_ctl_query_opsy_extensions(struct virtio_snd *snd); + +int virtsnd_ctl_alsa_card_info(struct virtio_snd *snd); + +int virtsnd_ctl_alsa_pcm_info(struct virtio_snd *snd, struct snd_pcm *pcm); + +#endif /* VIRTIO_SND_OPSY_H */ diff --git a/virtio_snd/virtio_opsy_ctl_msg.c b/virtio_snd/virtio_opsy_ctl_msg.c new file mode 100644 index 0000000..8aa5e12 --- /dev/null +++ b/virtio_snd/virtio_opsy_ctl_msg.c @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +int virtsnd_ctl_query_opsy_extensions(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_hdr *hdr; + struct virtio_snd_opsy_info *info; + int code; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), sizeof(*info), + GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + virtsnd_ctl_msg_ref(vdev, msg); + + hdr = sg_virt(&msg->sg_request); + hdr->code = cpu_to_virtio32(vdev, VIRTIO_SND_R_OPSY_INFO); + + code = virtsnd_ctl_msg_send_sync(snd, msg); + if (code) + return code; + + info = sg_virt(&msg->sg_response); + + snd->extensions = le32_to_cpu(info->extensions); + + if (VIRTIO_HAS_OPSY_EXTENSION(snd, DEV_EXT_INFO)) + dev_info(&vdev->dev, + "OpSy extension: ALSA extended device information\n"); + if (VIRTIO_HAS_OPSY_EXTENSION(snd, DEV_CTLS)) + dev_info(&vdev->dev, "OpSy extension: ALSA device controls\n"); + + virtsnd_ctl_msg_unref(vdev, msg); + + return 0; +} + +int virtsnd_ctl_alsa_card_info(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_hdr *hdr; + struct virtio_snd_alsa_card_info *info = NULL; + struct scatterlist sg_response_ext; + int code; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), sizeof(*hdr), + GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + virtsnd_ctl_msg_ref(vdev, msg); + + info = devm_kzalloc(&vdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) { + code = -ENOMEM; + goto on_failure; + } + + hdr = sg_virt(&msg->sg_request); + hdr->code = cpu_to_virtio32(vdev, VIRTIO_SND_R_ALSA_CARD_INFO); + + sg_init_one(&sg_response_ext, info, sizeof(*info)); + msg->sg_response_ext = &sg_response_ext; + + code = virtsnd_ctl_msg_send_sync(snd, msg); + if (code) + goto on_failure; + + virtsnd_strlcpy(snd->card->id, info->id, sizeof(snd->card->id)); + virtsnd_strlcpy(snd->card->driver, info->driver, + sizeof(snd->card->driver)); + virtsnd_strlcpy(snd->card->shortname, info->name, + sizeof(snd->card->shortname)); + virtsnd_strlcpy(snd->card->longname, info->longname, + sizeof(snd->card->longname)); + virtsnd_strlcpy(snd->card->mixername, info->mixername, + sizeof(snd->card->mixername)); + virtsnd_strlcpy(snd->card->components, info->components, + sizeof(snd->card->components)); + +on_failure: + if (info) + devm_kfree(&vdev->dev, info); + + virtsnd_ctl_msg_unref(vdev, msg); + + return code; +} + +int virtsnd_ctl_alsa_pcm_info(struct virtio_snd *snd, struct snd_pcm *pcm) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_alsa_query_pcm_info *hdr; + struct virtio_snd_alsa_pcm_info *info = NULL; + struct scatterlist sg_response_ext; + int code; + + msg = virtsnd_ctl_msg_alloc(vdev, sizeof(*hdr), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + virtsnd_ctl_msg_ref(vdev, msg); + + info = devm_kzalloc(&vdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) { + code = -ENOMEM; + goto on_failure; + } + + hdr = sg_virt(&msg->sg_request); + hdr->hdr.code = cpu_to_virtio32(vdev, VIRTIO_SND_R_ALSA_PCM_INFO); + hdr->hda_fn_nid = cpu_to_virtio32(vdev, pcm->device); + + sg_init_one(&sg_response_ext, info, sizeof(*info)); + msg->sg_response_ext = &sg_response_ext; + + code = virtsnd_ctl_msg_send_sync(snd, msg); + if (code) + goto on_failure; + + virtsnd_strlcpy(pcm->id, info->id, sizeof(pcm->id)); + virtsnd_strlcpy(pcm->name, info->name, sizeof(pcm->name)); + +on_failure: + if (info) + devm_kfree(&vdev->dev, info); + + virtsnd_ctl_msg_unref(vdev, msg); + + return 0; +} diff --git a/virtio_snd/virtio_pcm.c b/virtio_snd/virtio_pcm.c new file mode 100644 index 0000000..4c699b5 --- /dev/null +++ b/virtio_snd/virtio_pcm.c @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <linux/moduleparam.h> +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +static unsigned int pcm_buffer_ms = 160; +module_param(pcm_buffer_ms, uint, 0644); +MODULE_PARM_DESC(pcm_buffer_ms, "PCM substream buffer time in milliseconds"); + +static unsigned int pcm_periods_min = 2; +module_param(pcm_periods_min, uint, 0644); +MODULE_PARM_DESC(pcm_periods_min, "Minimum number of PCM periods"); + +static unsigned int pcm_periods_max = 16; +module_param(pcm_periods_max, uint, 0644); +MODULE_PARM_DESC(pcm_periods_max, "Maximum number of PCM periods"); + +static unsigned int pcm_period_ms_min = 10; +module_param(pcm_period_ms_min, uint, 0644); +MODULE_PARM_DESC(pcm_period_ms_min, "Minimum PCM period time in milliseconds"); + +static unsigned int pcm_period_ms_max = 80; +module_param(pcm_period_ms_max, uint, 0644); +MODULE_PARM_DESC(pcm_period_ms_max, "Maximum PCM period time in milliseconds"); + +static const unsigned int g_v2a_format_map[] = { + [VIRTIO_SND_PCM_FMT_IMA_ADPCM] = SNDRV_PCM_FORMAT_IMA_ADPCM, + [VIRTIO_SND_PCM_FMT_MU_LAW] = SNDRV_PCM_FORMAT_MU_LAW, + [VIRTIO_SND_PCM_FMT_A_LAW] = SNDRV_PCM_FORMAT_A_LAW, + [VIRTIO_SND_PCM_FMT_S8] = SNDRV_PCM_FORMAT_S8, + [VIRTIO_SND_PCM_FMT_U8] = SNDRV_PCM_FORMAT_U8, + [VIRTIO_SND_PCM_FMT_S16] = SNDRV_PCM_FORMAT_S16_LE, + [VIRTIO_SND_PCM_FMT_U16] = SNDRV_PCM_FORMAT_U16_LE, + [VIRTIO_SND_PCM_FMT_S18_3] = SNDRV_PCM_FORMAT_S18_3LE, + [VIRTIO_SND_PCM_FMT_U18_3] = SNDRV_PCM_FORMAT_U18_3LE, + [VIRTIO_SND_PCM_FMT_S20_3] = SNDRV_PCM_FORMAT_S20_3LE, + [VIRTIO_SND_PCM_FMT_U20_3] = SNDRV_PCM_FORMAT_U20_3LE, + [VIRTIO_SND_PCM_FMT_S24_3] = SNDRV_PCM_FORMAT_S24_3LE, + [VIRTIO_SND_PCM_FMT_U24_3] = SNDRV_PCM_FORMAT_U24_3LE, +#ifdef SNDRV_PCM_FORMAT_S20 + [VIRTIO_SND_PCM_FMT_S20] = SNDRV_PCM_FORMAT_S20_LE, +#endif +#ifdef SNDRV_PCM_FORMAT_U20 + [VIRTIO_SND_PCM_FMT_U20] = SNDRV_PCM_FORMAT_U20_LE, +#endif + [VIRTIO_SND_PCM_FMT_S24] = SNDRV_PCM_FORMAT_S24_LE, + [VIRTIO_SND_PCM_FMT_U24] = SNDRV_PCM_FORMAT_U24_LE, + [VIRTIO_SND_PCM_FMT_S32] = SNDRV_PCM_FORMAT_S32_LE, + [VIRTIO_SND_PCM_FMT_U32] = SNDRV_PCM_FORMAT_U32_LE, + [VIRTIO_SND_PCM_FMT_FLOAT] = SNDRV_PCM_FORMAT_FLOAT_LE, + [VIRTIO_SND_PCM_FMT_FLOAT64] = SNDRV_PCM_FORMAT_FLOAT64_LE, + [VIRTIO_SND_PCM_FMT_DSD_U8] = SNDRV_PCM_FORMAT_DSD_U8, + [VIRTIO_SND_PCM_FMT_DSD_U16] = SNDRV_PCM_FORMAT_DSD_U16_LE, + [VIRTIO_SND_PCM_FMT_DSD_U32] = SNDRV_PCM_FORMAT_DSD_U32_LE, + [VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME] = + SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE +}; + +struct virtsnd_v2a_rate { + unsigned int alsa_bit; + unsigned int rate; +}; + +static const struct virtsnd_v2a_rate g_v2a_rate_map[] = { + [VIRTIO_SND_PCM_RATE_5512] = { SNDRV_PCM_RATE_5512, 5512 }, + [VIRTIO_SND_PCM_RATE_8000] = { SNDRV_PCM_RATE_8000, 8000 }, + [VIRTIO_SND_PCM_RATE_11025] = { SNDRV_PCM_RATE_11025, 11025 }, + [VIRTIO_SND_PCM_RATE_16000] = { SNDRV_PCM_RATE_16000, 16000 }, + [VIRTIO_SND_PCM_RATE_22050] = { SNDRV_PCM_RATE_22050, 22050 }, + [VIRTIO_SND_PCM_RATE_32000] = { SNDRV_PCM_RATE_32000, 32000 }, + [VIRTIO_SND_PCM_RATE_44100] = { SNDRV_PCM_RATE_44100, 44100 }, + [VIRTIO_SND_PCM_RATE_48000] = { SNDRV_PCM_RATE_48000, 48000 }, + [VIRTIO_SND_PCM_RATE_64000] = { SNDRV_PCM_RATE_64000, 64000 }, + [VIRTIO_SND_PCM_RATE_88200] = { SNDRV_PCM_RATE_88200, 88200 }, + [VIRTIO_SND_PCM_RATE_96000] = { SNDRV_PCM_RATE_96000, 96000 }, + [VIRTIO_SND_PCM_RATE_176400] = { SNDRV_PCM_RATE_176400, 176400 }, + [VIRTIO_SND_PCM_RATE_192000] = { SNDRV_PCM_RATE_192000, 192000 } +}; + +static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *substream, + struct virtio_snd_pcm_info *info) +{ + struct virtio_device *vdev = substream->snd->vdev; + unsigned int i; + u64 values; + size_t sample_max = 0; + size_t sample_min = 0; + + substream->features = le32_to_cpu(info->features); + + /* + * TODO: set SNDRV_PCM_INFO_{BATCH,BLOCK_TRANSFER} if device supports + * only message-based transport. + */ + substream->hw.info = + SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_BATCH | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_RESUME | + SNDRV_PCM_INFO_PAUSE; + + if (!info->channels_min || info->channels_min > info->channels_max) { + dev_err(&vdev->dev, + "SID %u: invalid channel range [%u %u]", substream->sid, + info->channels_min, info->channels_max); + return -EINVAL; + } + + substream->hw.channels_min = info->channels_min; + substream->hw.channels_max = info->channels_max; + + values = le64_to_cpu(info->formats); + + for (i = 0; i < ARRAY_SIZE(g_v2a_format_map); ++i) + if (values & (1ULL << i)) { + unsigned int alsa_fmt = g_v2a_format_map[i]; + int bytes = snd_pcm_format_physical_width(alsa_fmt) / 8; + + if (!sample_min || sample_min > bytes) + sample_min = bytes; + + if (sample_max < bytes) + sample_max = bytes; + + substream->hw.formats |= (1ULL << alsa_fmt); + } + + if (!substream->hw.formats) { + dev_err(&vdev->dev, + "SID %u: no supported PCM sample formats found", + substream->sid); + return -EINVAL; + } + + values = le64_to_cpu(info->rates); + + for (i = 0; i < ARRAY_SIZE(g_v2a_rate_map); ++i) + if (values & (1ULL << i)) { + if (!substream->hw.rate_min || + substream->hw.rate_min > g_v2a_rate_map[i].rate) + substream->hw.rate_min = g_v2a_rate_map[i].rate; + + if (substream->hw.rate_max < g_v2a_rate_map[i].rate) + substream->hw.rate_max = g_v2a_rate_map[i].rate; + + substream->hw.rates |= g_v2a_rate_map[i].alsa_bit; + } + + if (!substream->hw.rates) { + dev_err(&vdev->dev, + "SID %u: no supported PCM frame rates found", + substream->sid); + return -EINVAL; + } + + substream->hw.periods_min = pcm_periods_min; + substream->hw.periods_max = pcm_periods_max; + + /* + * We must ensure that there is enough space in the buffer to store + * pcm_buffer_ms ms for the combination (Cmax, Smax, Rmax), where: + * Cmax = maximum supported number of channels, + * Smax = maximum supported sample size in bytes, + * Rmax = maximum supported frame rate. + */ + substream->hw.buffer_bytes_max = + sample_max * substream->hw.channels_max * pcm_buffer_ms * + (substream->hw.rate_max / MSEC_PER_SEC); + + /* Align the buffer size to the page size */ + substream->hw.buffer_bytes_max = + (substream->hw.buffer_bytes_max + PAGE_SIZE - 1) & -PAGE_SIZE; + + /* + * We must ensure that the minimum period size is enough to store + * pcm_period_ms_min ms for the combination (Cmin, Smin, Rmin), where: + * Cmin = minimum supported number of channels, + * Smin = minimum supported sample size in bytes, + * Rmin = minimum supported frame rate. + */ + substream->hw.period_bytes_min = + sample_min * substream->hw.channels_min * pcm_period_ms_min * + (substream->hw.rate_min / MSEC_PER_SEC); + + /* + * We must ensure that the maximum period size is enough to store + * pcm_period_ms_max ms for the combination (Cmax, Smax, Rmax). + */ + substream->hw.period_bytes_max = + sample_max * substream->hw.channels_max * pcm_period_ms_max * + (substream->hw.rate_max / MSEC_PER_SEC); + + return 0; +} + +static void virtsnd_pcm_prealloc_pages(struct virtio_pcm_substream *substream) +{ + struct snd_pcm_substream *ksubstream = substream->substream; + size_t size = substream->hw.buffer_bytes_max; + struct device *data = snd_dma_continuous_data(GFP_KERNEL); + + snd_pcm_lib_preallocate_pages(ksubstream, + SNDRV_DMA_TYPE_CONTINUOUS, data, + size, size); +} + +struct virtio_pcm *virtsnd_pcm_find(struct virtio_snd *snd, unsigned int nid) +{ + struct virtio_pcm *pcm; + + list_for_each_entry(pcm, &snd->pcm_list, list) + if (pcm->nid == nid) + return pcm; + + return ERR_PTR(-ENOENT); +} + +struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, + unsigned int nid) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *pcm; + + pcm = virtsnd_pcm_find(snd, nid); + if (!IS_ERR(pcm)) + return pcm; + + pcm = devm_kzalloc(&vdev->dev, sizeof(*pcm), GFP_KERNEL); + if (!pcm) + return ERR_PTR(-ENOMEM); + + pcm->nid = nid; + list_add_tail(&pcm->list, &snd->pcm_list); + + return pcm; +} + +int virtsnd_pcm_validate(struct virtio_device *vdev) +{ + if (pcm_periods_min < 2 || pcm_periods_min > pcm_periods_max) { + dev_err(&vdev->dev, + "invalid range [%u %u] of the number of PCM periods", + pcm_periods_min, pcm_periods_max); + return -EINVAL; + } + + if (!pcm_period_ms_min || pcm_period_ms_min > pcm_period_ms_max) { + dev_err(&vdev->dev, + "invalid range [%u %u] of the size of the PCM period", + pcm_period_ms_min, pcm_period_ms_max); + return -EINVAL; + } + + if (pcm_buffer_ms < pcm_periods_min * pcm_period_ms_min) { + dev_err(&vdev->dev, + "pcm_buffer_ms(=%u) value cannot be < %u ms", + pcm_buffer_ms, pcm_periods_min * pcm_period_ms_min); + return -EINVAL; + } + + if (pcm_period_ms_max > pcm_buffer_ms / 2) { + dev_err(&vdev->dev, + "pcm_period_ms_max(=%u) value cannot be > %u ms", + pcm_period_ms_max, pcm_buffer_ms / 2); + return -EINVAL; + } + + return 0; +} + +int virtsnd_pcm_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_pcm_info *info; + unsigned int i; + int rc; + + virtio_cread(vdev, struct virtio_snd_config, streams, + &snd->nsubstreams); + if (!snd->nsubstreams) + return 0; + + snd->substreams = devm_kcalloc(&vdev->dev, snd->nsubstreams, + sizeof(*snd->substreams), GFP_KERNEL); + if (!snd->substreams) + return -ENOMEM; + + info = devm_kcalloc(&vdev->dev, snd->nsubstreams, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_PCM_INFO, 0, + snd->nsubstreams, sizeof(*info), info); + if (rc) + return rc; + + for (i = 0; i < snd->nsubstreams; ++i) { + struct virtio_pcm_substream *substream = &snd->substreams[i]; + struct virtio_pcm *pcm; + + substream->snd = snd; + substream->sid = i; + init_waitqueue_head(&substream->msg_empty); + + rc = virtsnd_pcm_build_hw(substream, &info[i]); + if (rc) + return rc; + + substream->nid = le32_to_cpu(info[i].hdr.hda_fn_nid); + + pcm = virtsnd_pcm_find_or_create(snd, substream->nid); + if (IS_ERR(pcm)) + return PTR_ERR(pcm); + + switch (info[i].direction) { + case VIRTIO_SND_D_OUTPUT: { + substream->direction = SNDRV_PCM_STREAM_PLAYBACK; + break; + } + case VIRTIO_SND_D_INPUT: { + substream->direction = SNDRV_PCM_STREAM_CAPTURE; + break; + } + default: { + dev_err(&vdev->dev, "SID %u: unknown direction (%u)", + substream->sid, info[i].direction); + return -EINVAL; + } + } + + pcm->streams[substream->direction].nsubstreams++; + } + + devm_kfree(&vdev->dev, info); + + return 0; +} + +static int virtsnd_pcm_check_entity_cfg(struct virtio_pcm_substream *substream, + struct virtio_snd_pcm_info *info) +{ + unsigned int i; + u64 values; + u64 formats = 0; + u64 rates = 0; + bool changed = false; + int rc; + + if (substream->nid != le32_to_cpu(info->hdr.hda_fn_nid)) + return -EINVAL; + if (substream->direction != info->direction) + return -EINVAL; + + if (substream->features != le32_to_cpu(info->features)) + changed = true; + if (substream->hw.channels_min != info->channels_min) + changed = true; + if (substream->hw.channels_max != info->channels_max) + changed = true; + + values = le64_to_cpu(info->formats); + + for (i = 0; i < ARRAY_SIZE(g_v2a_format_map); ++i) + if (values & (1ULL << i)) + formats |= (1ULL << g_v2a_format_map[i]); + + if (substream->hw.formats != formats) + changed = true; + + values = le64_to_cpu(info->rates); + + for (i = 0; i < ARRAY_SIZE(g_v2a_rate_map); ++i) + if (values & (1ULL << i)) + rates |= g_v2a_rate_map[i].alsa_bit; + + if (substream->hw.rates != rates) + changed = true; + + if (changed) { + struct snd_pcm_runtime *runtime = substream->substream->runtime; + size_t buffer_bytes_max = substream->hw.buffer_bytes_max; + + if (runtime && runtime->status && + runtime->status->state != SNDRV_PCM_STATE_OPEN) + return -EINVAL; + + rc = virtsnd_pcm_build_hw(substream, info); + if (rc) + return rc; + + if (buffer_bytes_max < substream->hw.buffer_bytes_max) + return -EINVAL; + } + + return 0; +} + +int virtsnd_pcm_check_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_pcm_info *info; + unsigned int i; + int rc; + + virtio_cread(vdev, struct virtio_snd_config, streams, &i); + if (snd->nsubstreams != i) { + dev_warn(&vdev->dev, + "config: number of streams has changed (%u->%u)", + snd->nsubstreams, i); + return -EINVAL; + } + + if (!snd->nsubstreams) + return 0; + + info = devm_kcalloc(&vdev->dev, snd->nsubstreams, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_PCM_INFO, 0, + snd->nsubstreams, sizeof(*info), info); + if (rc) + goto on_failure; + + for (i = 0; i < snd->nsubstreams; ++i) { + rc = virtsnd_pcm_check_entity_cfg(&snd->substreams[i], + &info[i]); + if (rc) { + dev_warn(&vdev->dev, + "config: stream#%u configuration has changed", + i); + break; + } + } + +on_failure: + devm_kfree(&vdev->dev, info); + + return rc; +} + +static int virtsnd_pcm_info(struct virtio_snd *snd, struct snd_pcm *pcm) +{ + if (VIRTIO_HAS_OPSY_EXTENSION(snd, DEV_EXT_INFO)) { + int code; + + code = virtsnd_ctl_alsa_pcm_info(snd, pcm); + if (!code || code != -EOPNOTSUPP) + return code; + } + + strlcpy(pcm->name, "VirtIO PCM", sizeof(pcm->name)); + + return 0; +} + +int virtsnd_pcm_build_devs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *pcm; + unsigned int i; + int code; + + list_for_each_entry(pcm, &snd->pcm_list, list) { + unsigned int npbs = + pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].nsubstreams; + unsigned int ncps = + pcm->streams[SNDRV_PCM_STREAM_CAPTURE].nsubstreams; + + if (!npbs && !ncps) + continue; + + code = snd_pcm_new(snd->card, "virtio_snd", pcm->nid, npbs, + ncps, &pcm->pcm); + if (code) { + dev_err(&vdev->dev, "snd_pcm_new[%u] failed: %d", + pcm->nid, code); + return code; + } + + code = virtsnd_pcm_info(snd, pcm->pcm); + if (code) + return code; + + pcm->pcm->info_flags = 0; + pcm->pcm->dev_class = SNDRV_PCM_CLASS_GENERIC; + pcm->pcm->dev_subclass = SNDRV_PCM_SUBCLASS_GENERIC_MIX; + + pcm->pcm->private_data = pcm; + + for (i = 0; i < ARRAY_SIZE(pcm->streams); ++i) { + struct virtio_pcm_stream *stream = &pcm->streams[i]; + + if (!stream->nsubstreams) + continue; + + stream->substreams = + devm_kcalloc(&vdev->dev, + stream->nsubstreams, + sizeof(*stream->substreams), + GFP_KERNEL); + if (!stream->substreams) + return -ENOMEM; + + stream->nsubstreams = 0; + } + } + + for (i = 0; i < snd->nsubstreams; ++i) { + struct virtio_pcm_substream *substream = &snd->substreams[i]; + struct virtio_pcm_stream *stream; + + pcm = virtsnd_pcm_find(snd, substream->nid); + if (IS_ERR(pcm)) + return PTR_ERR(pcm); + + stream = &pcm->streams[substream->direction]; + stream->substreams[stream->nsubstreams++] = substream; + } + + list_for_each_entry(pcm, &snd->pcm_list, list) + for (i = 0; i < ARRAY_SIZE(pcm->streams); ++i) { + struct virtio_pcm_stream *stream = &pcm->streams[i]; + struct snd_pcm_str *kstream; + struct snd_pcm_substream *ksubstream; + + if (!stream->nsubstreams) + continue; + + kstream = &pcm->pcm->streams[i]; + ksubstream = kstream->substream; + + while (ksubstream) { + struct virtio_pcm_substream *substream = + stream->substreams[ksubstream->number]; + + substream->substream = ksubstream; + ksubstream = ksubstream->next; + + virtsnd_pcm_prealloc_pages(substream); + } + + snd_pcm_set_ops(pcm->pcm, i, &virtsnd_pcm_ops); + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +int virtsnd_pcm_restore(struct virtio_snd *snd) +{ + unsigned int i; + + for (i = 0; i < snd->nsubstreams; ++i) { + struct snd_pcm_substream *substream = + snd->substreams[i].substream; + struct snd_pcm_runtime *runtime = substream->runtime; + int rc; + + if (!runtime || !runtime->status || + runtime->status->state != SNDRV_PCM_STATE_SUSPENDED) + continue; + + rc = substream->ops->hw_params(substream, NULL); + if (rc) + return rc; + + rc = substream->ops->prepare(substream); + if (rc) + return rc; + } + + return 0; +} +#endif /* CONFIG_PM_SLEEP */ + +void virtsnd_pcm_event(struct virtio_snd *snd, struct virtio_snd_event *event) +{ + struct virtio_pcm_substream *substream; + unsigned int sid = le32_to_cpu(event->data); + + if (sid >= snd->nsubstreams) + return; + + substream = &snd->substreams[sid]; + + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: { + /* TODO: deal with shmem elapsed period */ + break; + } + case VIRTIO_SND_EVT_PCM_XRUN: { + if (atomic_read(&substream->xfer_enabled)) + atomic_set(&substream->xfer_xrun, 1); + break; + } + } +} diff --git a/virtio_snd/virtio_pcm.h b/virtio_snd/virtio_pcm.h new file mode 100644 index 0000000..eb0a03a --- /dev/null +++ b/virtio_snd/virtio_pcm.h @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#ifndef VIRTIO_SND_PCM_H +#define VIRTIO_SND_PCM_H + +#include <linux/atomic.h> +#include <linux/virtio_config.h> +#include <sound/pcm.h> + +struct virtio_pcm; +struct virtio_pcm_msg; + +/** + * struct virtio_pcm_substream - virtio PCM substream representation. + * @snd: Virtio sound card device. + * @nid: Functional group node identifier. + * @sid: Stream identifier. + * @direction: Stream data flow direction (VIRTIO_SND_D_XXX). + * @features: Stream virtio feature bit map (1 << VIRTIO_SND_PCM_F_XXX). + * @substream: Kernel substream. + * @hw: Kernel substream hardware descriptor. + * @hw_ptr: Substream hardware pointer value. + * @xfer_enabled: Data transfer state. + * @xfer_draining: Data draining state. + * @xfer_xrun: Data underflow/overflow state. + * @msg_list: Pending I/O message list. + * @msg_empty: Notify when msg_list is empty. + */ +struct virtio_pcm_substream { + struct virtio_snd *snd; + unsigned int nid; + unsigned int sid; + u32 direction; + u32 features; + struct snd_pcm_substream *substream; + struct snd_pcm_hardware hw; + atomic_t hw_ptr; + atomic_t xfer_enabled; + atomic_t xfer_xrun; + struct virtio_pcm_msg *msgs; + int msg_last_enqueued; + atomic_t msg_count; + wait_queue_head_t msg_empty; +}; + +/** + * struct virtio_pcm_stream - virtio PCM stream representation. + * @substreams: Virtio substreams belonging to the stream. + * @nsubstreams: Number of substreams. + * @chmaps: Kernel channel maps belonging to the stream. + * @nchmaps: Number of channel maps. + */ +struct virtio_pcm_stream { + struct virtio_pcm_substream **substreams; + unsigned int nsubstreams; + struct snd_pcm_chmap_elem *chmaps; + unsigned int nchmaps; +}; + +/** + * struct virtio_pcm - virtio PCM device representation. + * @list: PCM list entry. + * @nid: Functional group node identifier. + * @pcm: Kernel PCM device. + * @streams: Virtio streams (playback and capture). + */ +struct virtio_pcm { + struct list_head list; + unsigned int nid; + struct snd_pcm *pcm; + struct virtio_pcm_stream streams[SNDRV_PCM_STREAM_LAST + 1]; +}; + +extern const struct snd_pcm_ops virtsnd_pcm_ops; + +int virtsnd_pcm_validate(struct virtio_device *vdev); + +int virtsnd_pcm_parse_cfg(struct virtio_snd *snd); + +int virtsnd_pcm_check_cfg(struct virtio_snd *snd); + +int virtsnd_pcm_build_devs(struct virtio_snd *snd); + +#ifdef CONFIG_PM_SLEEP +int virtsnd_pcm_restore(struct virtio_snd *snd); +#endif /* CONFIG_PM_SLEEP */ + +void virtsnd_pcm_event(struct virtio_snd *snd, struct virtio_snd_event *event); + +void virtsnd_pcm_tx_notify_cb(struct virtqueue *vqueue); + +void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue); + +struct virtio_pcm *virtsnd_pcm_find(struct virtio_snd *snd, unsigned int nid); + +struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, + unsigned int nid); + +struct virtio_snd_msg * +virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *substream, + unsigned int command, gfp_t gfp); + +int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *substream, + unsigned int nmsg, u8 *dma_area, + unsigned int period_bytes); + +int virtsnd_pcm_msg_send(struct virtio_pcm_substream *substream); + +#endif /* VIRTIO_SND_PCM_H */ diff --git a/virtio_snd/virtio_pcm_msg.c b/virtio_snd/virtio_pcm_msg.c new file mode 100644 index 0000000..6b7839e --- /dev/null +++ b/virtio_snd/virtio_pcm_msg.c @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <sound/pcm_params.h> + +#include "virtio_card.h" + +/** + * enum pcm_msg_sg_index - Scatter-gather element indexes for an I/O message + * @PCM_MSG_SG_XFER: Element containing a virtio_snd_pcm_xfer structure + * @PCM_MSG_SG_DATA: Element containing a data buffer + * @PCM_MSG_SG_STATUS: Element containing a virtio_snd_pcm_status structure + * @PCM_MSG_SG_MAX: The maximum number of elements in the scatter-gather table + * + * These values are used as the index of the scatter-gather table. + */ +enum pcm_msg_sg_index { + PCM_MSG_SG_XFER = 0, + PCM_MSG_SG_DATA, + PCM_MSG_SG_STATUS, + PCM_MSG_SG_MAX +}; + +/** + * struct virtio_pcm_msg - I/O message representation + * @list: Pending I/O message list entry + * @stream: Pointer to virtio PCM stream structure + * @xfer: I/O message header payload + * @status: I/O message status payload + * @one_shot_data: if the message should not be resent to the device, the field + * contains a pointer to the optional payload that should be + * released after completion + * @sgs: I/O message payload scatter-gather table + */ +struct virtio_pcm_msg { + struct virtio_pcm_substream *substream; + struct virtio_snd_pcm_xfer xfer; + struct virtio_snd_pcm_status status; + struct scatterlist sgs[PCM_MSG_SG_MAX]; +}; + +int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *substream, + unsigned int nmsg, u8 *dma_area, + unsigned int period_bytes) +{ + struct virtio_device *vdev = substream->snd->vdev; + unsigned int i; + + if (substream->msgs) + devm_kfree(&vdev->dev, substream->msgs); + + substream->msgs = devm_kcalloc(&vdev->dev, nmsg, + sizeof(*substream->msgs), GFP_KERNEL); + if (!substream->msgs) + return -ENOMEM; + + for (i = 0; i < nmsg; ++i) { + struct virtio_pcm_msg *msg = &substream->msgs[i]; + + msg->substream = substream; + + sg_init_table(msg->sgs, PCM_MSG_SG_MAX); + sg_init_one(&msg->sgs[PCM_MSG_SG_XFER], &msg->xfer, + sizeof(msg->xfer)); + sg_init_one(&msg->sgs[PCM_MSG_SG_DATA], + dma_area + period_bytes * i, period_bytes); + sg_init_one(&msg->sgs[PCM_MSG_SG_STATUS], &msg->status, + sizeof(msg->status)); + } + + return 0; +} + +int virtsnd_pcm_msg_send(struct virtio_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->substream->runtime; + struct virtio_snd *snd = substream->snd; + struct virtio_device *vdev = snd->vdev; + struct virtqueue *vqueue = virtsnd_pcm_queue(substream)->vqueue; + int i; + int n; + bool notify = false; + + if (!vqueue) + return -EIO; + + i = (substream->msg_last_enqueued + 1) % runtime->periods; + n = runtime->periods - atomic_read(&substream->msg_count); + + for (; n; --n, i = (i + 1) % runtime->periods) { + struct virtio_pcm_msg *msg = &substream->msgs[i]; + struct scatterlist *psgs[PCM_MSG_SG_MAX] = { + [PCM_MSG_SG_XFER] = &msg->sgs[PCM_MSG_SG_XFER], + [PCM_MSG_SG_DATA] = &msg->sgs[PCM_MSG_SG_DATA], + [PCM_MSG_SG_STATUS] = &msg->sgs[PCM_MSG_SG_STATUS] + }; + int rc; + + msg->xfer.stream_id = cpu_to_virtio32(vdev, substream->sid); + memset(&msg->status, 0, sizeof(msg->status)); + + atomic_inc(&substream->msg_count); + + if (substream->direction == SNDRV_PCM_STREAM_PLAYBACK) + rc = virtqueue_add_sgs(vqueue, psgs, 2, 1, msg, + GFP_ATOMIC); + else + rc = virtqueue_add_sgs(vqueue, psgs, 1, 2, msg, + GFP_ATOMIC); + + if (rc) { + atomic_dec(&substream->msg_count); + return -EIO; + } + + substream->msg_last_enqueued = i; + } + + if (!(substream->features & (1U << VIRTIO_SND_PCM_F_MSG_POLLING))) + notify = virtqueue_kick_prepare(vqueue); + + if (notify) + if (!virtqueue_notify(vqueue)) + return -EIO; + + return 0; +} + +static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg, size_t size) +{ + struct virtio_pcm_substream *substream = msg->substream; + struct snd_pcm_runtime *runtime = substream->substream->runtime; + snd_pcm_uframes_t hw_ptr; + + /* TODO: propagate an error to upper layer? */ + if (le32_to_cpu(msg->status.status) != VIRTIO_SND_S_OK) + return; + + hw_ptr = (snd_pcm_uframes_t)atomic_read(&substream->hw_ptr); + + if (substream->direction == SNDRV_PCM_STREAM_PLAYBACK) { + hw_ptr += runtime->period_size; + } else { + if (size > sizeof(struct virtio_snd_pcm_status)) + size -= sizeof(struct virtio_snd_pcm_status); + else + /* TODO: propagate an error to upper layer? */ + return; + + hw_ptr += bytes_to_frames(runtime, size); + } + + atomic_set(&substream->hw_ptr, (u32)(hw_ptr % runtime->buffer_size)); + atomic_set(&substream->xfer_xrun, 0); + + runtime->delay = + bytes_to_frames(runtime, + le32_to_cpu(msg->status.latency_bytes)); + + snd_pcm_period_elapsed(substream->substream); +} + +static inline void virtsnd_pcm_notify_cb(struct virtio_snd_queue *queue) +{ + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + while (queue->vqueue) { + virtqueue_disable_cb(queue->vqueue); + + for (;;) { + struct virtio_pcm_substream *substream; + struct virtio_pcm_msg *msg; + unsigned int msg_count; + u32 length; + + msg = virtqueue_get_buf(queue->vqueue, &length); + if (!msg) + break; + + substream = msg->substream; + + msg_count = atomic_dec_return(&substream->msg_count); + + if (atomic_read(&substream->xfer_enabled)) { + virtsnd_pcm_msg_complete(msg, length); + virtsnd_pcm_msg_send(substream); + } else if (!msg_count) { + wake_up_all(&substream->msg_empty); + } + } + + if (unlikely(virtqueue_is_broken(queue->vqueue))) + break; + + if (virtqueue_enable_cb(queue->vqueue)) + break; + } + spin_unlock_irqrestore(&queue->lock, flags); +} + +void virtsnd_pcm_tx_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + + virtsnd_pcm_notify_cb(virtsnd_tx_queue(snd)); +} + +void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + + virtsnd_pcm_notify_cb(virtsnd_rx_queue(snd)); +} + +struct virtio_snd_msg * +virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *substream, + unsigned int command, gfp_t gfp) +{ + struct virtio_device *vdev = substream->snd->vdev; + size_t request_size = sizeof(struct virtio_snd_pcm_hdr); + size_t response_size = sizeof(struct virtio_snd_hdr); + struct virtio_snd_msg *msg; + + switch (command) { + case VIRTIO_SND_R_PCM_SET_PARAMS: { + request_size = sizeof(struct virtio_snd_pcm_set_params); + break; + } + } + + msg = virtsnd_ctl_msg_alloc(vdev, request_size, response_size, gfp); + if (!IS_ERR(msg)) { + struct virtio_snd_pcm_hdr *hdr = sg_virt(&msg->sg_request); + + hdr->hdr.code = cpu_to_virtio32(vdev, command); + hdr->stream_id = cpu_to_virtio32(vdev, substream->sid); + } + + return msg; +} diff --git a/virtio_snd/virtio_pcm_ops.c b/virtio_snd/virtio_pcm_ops.c new file mode 100644 index 0000000..0743a7b --- /dev/null +++ b/virtio_snd/virtio_pcm_ops.c @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Sound card driver for virtio + * Copyright (C) 2020 OpenSynergy GmbH + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ +#include <sound/pcm_params.h> + +#include "virtio_card.h" + +struct virtsnd_a2v_format { + unsigned int alsa_bit; + unsigned int vio_bit; +}; + +static const struct virtsnd_a2v_format g_a2v_format_map[] = { + { SNDRV_PCM_FORMAT_IMA_ADPCM, VIRTIO_SND_PCM_FMT_IMA_ADPCM }, + { SNDRV_PCM_FORMAT_MU_LAW, VIRTIO_SND_PCM_FMT_MU_LAW }, + { SNDRV_PCM_FORMAT_A_LAW, VIRTIO_SND_PCM_FMT_A_LAW }, + { SNDRV_PCM_FORMAT_S8, VIRTIO_SND_PCM_FMT_S8 }, + { SNDRV_PCM_FORMAT_U8, VIRTIO_SND_PCM_FMT_U8 }, + { SNDRV_PCM_FORMAT_S16_LE, VIRTIO_SND_PCM_FMT_S16 }, + { SNDRV_PCM_FORMAT_U16_LE, VIRTIO_SND_PCM_FMT_U16 }, + { SNDRV_PCM_FORMAT_S18_3LE, VIRTIO_SND_PCM_FMT_S18_3 }, + { SNDRV_PCM_FORMAT_U18_3LE, VIRTIO_SND_PCM_FMT_U18_3 }, + { SNDRV_PCM_FORMAT_S20_3LE, VIRTIO_SND_PCM_FMT_S20_3 }, + { SNDRV_PCM_FORMAT_U20_3LE, VIRTIO_SND_PCM_FMT_U20_3 }, + { SNDRV_PCM_FORMAT_S24_3LE, VIRTIO_SND_PCM_FMT_S24_3 }, + { SNDRV_PCM_FORMAT_U24_3LE, VIRTIO_SND_PCM_FMT_U24_3 }, +#ifdef SNDRV_PCM_FORMAT_S20 + { SNDRV_PCM_FORMAT_S20_LE, VIRTIO_SND_PCM_FMT_S20 }, +#endif +#ifdef SNDRV_PCM_FORMAT_U20 + { SNDRV_PCM_FORMAT_U20_LE, VIRTIO_SND_PCM_FMT_U20 }, +#endif + { SNDRV_PCM_FORMAT_S24_LE, VIRTIO_SND_PCM_FMT_S24 }, + { SNDRV_PCM_FORMAT_U24_LE, VIRTIO_SND_PCM_FMT_U24 }, + { SNDRV_PCM_FORMAT_S32_LE, VIRTIO_SND_PCM_FMT_S32 }, + { SNDRV_PCM_FORMAT_U32_LE, VIRTIO_SND_PCM_FMT_U32 }, + { SNDRV_PCM_FORMAT_FLOAT_LE, VIRTIO_SND_PCM_FMT_FLOAT }, + { SNDRV_PCM_FORMAT_FLOAT64_LE, VIRTIO_SND_PCM_FMT_FLOAT64 }, + { SNDRV_PCM_FORMAT_DSD_U8, VIRTIO_SND_PCM_FMT_DSD_U8 }, + { SNDRV_PCM_FORMAT_DSD_U16_LE, VIRTIO_SND_PCM_FMT_DSD_U16 }, + { SNDRV_PCM_FORMAT_DSD_U32_LE, VIRTIO_SND_PCM_FMT_DSD_U32 }, + { SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE, + VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME } +}; + +struct virtsnd_a2v_rate { + unsigned int rate; + unsigned int vio_bit; +}; + +static const struct virtsnd_a2v_rate g_a2v_rate_map[] = { + { 5512, VIRTIO_SND_PCM_RATE_5512 }, + { 8000, VIRTIO_SND_PCM_RATE_8000 }, + { 11025, VIRTIO_SND_PCM_RATE_11025 }, + { 16000, VIRTIO_SND_PCM_RATE_16000 }, + { 22050, VIRTIO_SND_PCM_RATE_22050 }, + { 32000, VIRTIO_SND_PCM_RATE_32000 }, + { 44100, VIRTIO_SND_PCM_RATE_44100 }, + { 48000, VIRTIO_SND_PCM_RATE_48000 }, + { 64000, VIRTIO_SND_PCM_RATE_64000 }, + { 88200, VIRTIO_SND_PCM_RATE_88200 }, + { 96000, VIRTIO_SND_PCM_RATE_96000 }, + { 176400, VIRTIO_SND_PCM_RATE_176400 }, + { 192000, VIRTIO_SND_PCM_RATE_192000 } +}; + +static inline bool virtsnd_pcm_released(struct virtio_pcm_substream *substream) +{ + return atomic_read(&substream->msg_count) == 0; +} + +static int virtsnd_pcm_release(struct virtio_pcm_substream *substream) +{ + struct virtio_snd *snd = substream->snd; + struct virtio_snd_msg *msg; + int rc; + + msg = virtsnd_pcm_ctl_msg_alloc(substream, VIRTIO_SND_R_PCM_RELEASE, + GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + rc = virtsnd_ctl_msg_send_sync(snd, msg); + if (!rc) + rc = wait_event_interruptible(substream->msg_empty, + virtsnd_pcm_released(substream)); + + return rc; +} + +static int virtsnd_pcm_open(struct snd_pcm_substream *substream) +{ + struct virtio_pcm *pcm = snd_pcm_substream_chip(substream); + struct virtio_pcm_substream *ss = NULL; + + if (pcm) { + switch (substream->stream) { + case SNDRV_PCM_STREAM_PLAYBACK: + case SNDRV_PCM_STREAM_CAPTURE: { + struct virtio_pcm_stream *stream = + &pcm->streams[substream->stream]; + + if (substream->number < stream->nsubstreams) + ss = stream->substreams[substream->number]; + break; + } + } + } + + if (!ss) + return -EBADFD; + + substream->runtime->hw = ss->hw; + substream->private_data = ss; + + return 0; +} + +static int virtsnd_pcm_close(struct snd_pcm_substream *substream) +{ + return 0; +} + +static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *hw_params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + snd_pcm_state_t state; + unsigned long flags; + struct virtio_pcm_substream *ss = snd_pcm_substream_chip(substream); + struct virtio_device *vdev = ss->snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_pcm_set_params *request; + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + unsigned int buffer_bytes; + unsigned int period_bytes; + unsigned int periods; + unsigned int i; + int vformat = -1; + int vrate = -1; + int rc; + + snd_pcm_stream_lock_irqsave(substream, flags); + state = substream->runtime->status->state; + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (state != SNDRV_PCM_STATE_SUSPENDED) { + /* + * If we got here after ops->trigger() was called, the queue may + * still contain messages. In this case, we need to release the + * substream first. + */ + if (atomic_read(&ss->msg_count)) { + rc = virtsnd_pcm_release(ss); + if (rc) + return rc; + } + } + + /* Set hardware parameters in device */ + if (hw_params) { + format = params_format(hw_params); + channels = params_channels(hw_params); + rate = params_rate(hw_params); + buffer_bytes = params_buffer_bytes(hw_params); + period_bytes = params_period_bytes(hw_params); + periods = params_periods(hw_params); + } else { + format = runtime->format; + channels = runtime->channels; + rate = runtime->rate; + buffer_bytes = frames_to_bytes(runtime, runtime->buffer_size); + period_bytes = frames_to_bytes(runtime, runtime->period_size); + periods = runtime->periods; + } + + for (i = 0; i < ARRAY_SIZE(g_a2v_format_map); ++i) + if (g_a2v_format_map[i].alsa_bit == format) { + vformat = g_a2v_format_map[i].vio_bit; + + break; + } + + for (i = 0; i < ARRAY_SIZE(g_a2v_rate_map); ++i) + if (g_a2v_rate_map[i].rate == rate) { + vrate = g_a2v_rate_map[i].vio_bit; + + break; + } + + if (vformat == -1 || vrate == -1) + return -EINVAL; + + msg = virtsnd_pcm_ctl_msg_alloc(ss, VIRTIO_SND_R_PCM_SET_PARAMS, + GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + request = sg_virt(&msg->sg_request); + + request->buffer_bytes = cpu_to_virtio32(vdev, buffer_bytes); + request->period_bytes = cpu_to_virtio32(vdev, period_bytes); + request->channels = channels; + request->format = vformat; + request->rate = vrate; + + if (ss->features & (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) + request->features |= + cpu_to_virtio32(vdev, + 1U << VIRTIO_SND_PCM_F_MSG_POLLING); + + if (ss->features & (1U << VIRTIO_SND_PCM_F_EVT_XRUNS)) + request->features |= + cpu_to_virtio32(vdev, + 1U << VIRTIO_SND_PCM_F_EVT_XRUNS); + + rc = virtsnd_ctl_msg_send_sync(ss->snd, msg); + if (rc) + return rc; + + /* If the buffer was already allocated earlier, do nothing. */ + if (runtime->dma_area) + return 0; + + /* Allocate hardware buffer */ + rc = snd_pcm_lib_malloc_pages(substream, buffer_bytes); + if (rc < 0) + return rc; + + /* Allocate and initialize I/O messages */ + rc = virtsnd_pcm_msg_alloc(ss, periods, runtime->dma_area, + period_bytes); + if (rc) + snd_pcm_lib_free_pages(substream); + + return rc; +} + +static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *ss = snd_pcm_substream_chip(substream); + int rc; + + rc = virtsnd_pcm_release(ss); + + /* + * Even if we failed to send the RELEASE message or wait for the queue + * flush to complete, we can safely delete the buffer. Because after + * receiving the STOP command, the device must stop all I/O message + * processing. If there are still pending messages in the queue, the + * next ops->hw_params() call should deal with this. + */ + snd_pcm_lib_free_pages(substream); + + return rc; +} + +static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *ss = snd_pcm_substream_chip(substream); + snd_pcm_state_t state; + struct virtio_snd_msg *msg; + unsigned long flags; + int rc; + + snd_pcm_stream_lock_irqsave(substream, flags); + state = substream->runtime->status->state; + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (state != SNDRV_PCM_STATE_SUSPENDED) { + struct virtio_snd_queue *queue = virtsnd_pcm_queue(ss); + + /* + * If we got here after ops->trigger() was called, the queue may + * still contain messages. In this case, we need to reset the + * substream first. + */ + if (atomic_read(&ss->msg_count)) { + rc = virtsnd_pcm_hw_params(substream, NULL); + if (rc) + return rc; + } + + spin_lock_irqsave(&queue->lock, flags); + ss->msg_last_enqueued = -1; + spin_unlock_irqrestore(&queue->lock, flags); + + atomic_set(&ss->hw_ptr, 0); + } + + atomic_set(&ss->xfer_xrun, 0); + atomic_set(&ss->msg_count, 0); + + msg = virtsnd_pcm_ctl_msg_alloc(ss, VIRTIO_SND_R_PCM_PREPARE, + GFP_KERNEL); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + return virtsnd_ctl_msg_send_sync(ss->snd, msg); +} + +static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) +{ + struct virtio_pcm_substream *ss = snd_pcm_substream_chip(substream); + struct virtio_snd *snd = ss->snd; + struct virtio_snd_queue *queue = virtsnd_pcm_queue(ss); + struct virtio_snd_msg *msg; + + switch (command) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + case SNDRV_PCM_TRIGGER_RESUME: { + int rc; + + spin_lock(&queue->lock); + rc = virtsnd_pcm_msg_send(ss); + spin_unlock(&queue->lock); + if (rc) + return rc; + + atomic_set(&ss->xfer_enabled, 1); + + msg = virtsnd_pcm_ctl_msg_alloc(ss, VIRTIO_SND_R_PCM_START, + GFP_ATOMIC); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + return virtsnd_ctl_msg_send(snd, msg); + } + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + case SNDRV_PCM_TRIGGER_SUSPEND: { + atomic_set(&ss->xfer_enabled, 0); + + msg = virtsnd_pcm_ctl_msg_alloc(ss, VIRTIO_SND_R_PCM_STOP, + GFP_ATOMIC); + if (IS_ERR(msg)) + return PTR_ERR(msg); + + return virtsnd_ctl_msg_send(snd, msg); + } + default: { + return -EINVAL; + } + } +} + +static snd_pcm_uframes_t +virtsnd_pcm_pointer(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *ss = snd_pcm_substream_chip(substream); + + if (atomic_read(&ss->xfer_xrun)) + return SNDRV_PCM_POS_XRUN; + + return (snd_pcm_uframes_t)atomic_read(&ss->hw_ptr); +} + +const struct snd_pcm_ops virtsnd_pcm_ops = { + .open = virtsnd_pcm_open, + .close = virtsnd_pcm_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = virtsnd_pcm_hw_params, + .hw_free = virtsnd_pcm_hw_free, + .prepare = virtsnd_pcm_prepare, + .trigger = virtsnd_pcm_trigger, + .pointer = virtsnd_pcm_pointer, +}; diff --git a/virtio_snd/virtio_snd.h b/virtio_snd/virtio_snd.h new file mode 100644 index 0000000..e5dc8aa --- /dev/null +++ b/virtio_snd/virtio_snd.h @@ -0,0 +1,479 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (C) 2020 OpenSynergy GmbH + * + * This header is BSD licensed so anyone can use the definitions to + * implement compatible drivers/servers. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of OpenSynergy GmbH nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IBM OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#ifndef VIRTIO_SND_IF_H +#define VIRTIO_SND_IF_H + +#include <linux/virtio_types.h> + +/******************************************************************************* + * DEVICE FEATURES + */ +enum { + /* OpenSynergy extensions support */ + VIRTIO_SND_F_OPSY_EXT = 0xf +}; + +/******************************************************************************* + * CONFIGURATION SPACE + */ +struct virtio_snd_config { + /* # of available physical jacks */ + __le32 jacks; + /* # of available PCM streams */ + __le32 streams; + /* # of available channel maps */ + __le32 chmaps; + /* # of available device controls */ + __le32 controls; +}; + +enum { + /* device virtqueue indexes */ + VIRTIO_SND_VQ_CONTROL = 0, + VIRTIO_SND_VQ_EVENT, + VIRTIO_SND_VQ_TX, + VIRTIO_SND_VQ_RX, + /* a number of device virtqueues */ + VIRTIO_SND_VQ_MAX +}; + +/******************************************************************************* + * COMMON DEFINITIONS + */ + +/* supported dataflow directions */ +enum { + VIRTIO_SND_D_OUTPUT = 0, + VIRTIO_SND_D_INPUT +}; + +enum { + /* jack control request types */ + VIRTIO_SND_R_JACK_INFO = 1, + VIRTIO_SND_R_JACK_REMAP, + + /* PCM control request types */ + VIRTIO_SND_R_PCM_INFO = 0x0100, + VIRTIO_SND_R_PCM_SET_PARAMS, + VIRTIO_SND_R_PCM_PREPARE, + VIRTIO_SND_R_PCM_RELEASE, + VIRTIO_SND_R_PCM_START, + VIRTIO_SND_R_PCM_STOP, + + /* channel map control request types */ + VIRTIO_SND_R_CHMAP_INFO = 0x0200, + + /* jack event types */ + VIRTIO_SND_EVT_JACK_CONNECTED = 0x1000, + VIRTIO_SND_EVT_JACK_DISCONNECTED, + + /* PCM event types */ + VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED = 0x1100, + VIRTIO_SND_EVT_PCM_XRUN, + + /* common status codes */ + VIRTIO_SND_S_OK = 0x8000, + VIRTIO_SND_S_BAD_MSG, + VIRTIO_SND_S_NOT_SUPP, + VIRTIO_SND_S_IO_ERR, + + /* --- OpenSynergy extensions --------------------------------------- */ + VIRTIO_SND_R_OPSY_INFO = 0xf000, + + /* ALSA extended device information */ + VIRTIO_SND_R_ALSA_CARD_INFO = 0xf100, + VIRTIO_SND_R_ALSA_PCM_INFO, + + /* ALSA device controls */ + VIRTIO_SND_R_DC_INFO = 0xf200, + VIRTIO_SND_R_DC_ENUM_INFO, + VIRTIO_SND_R_DC_READ, + VIRTIO_SND_R_DC_WRITE, + VIRTIO_SND_R_DC_TLV_READ, + VIRTIO_SND_R_DC_TLV_WRITE, + VIRTIO_SND_R_DC_TLV_COMMAND, + + /* ALSA device control events */ + VIRTIO_SND_EVT_DC_NOTIFY = 0xff00, +}; + +/* common header */ +struct virtio_snd_hdr { + __le32 code; +}; + +/* event notification */ +struct virtio_snd_event { + /* VIRTIO_SND_EVT_XXX */ + struct virtio_snd_hdr hdr; + /* optional event data */ + __le32 data; +}; + +/* common control request to query an item information */ +struct virtio_snd_query_info { + /* VIRTIO_SND_R_XXX_INFO */ + struct virtio_snd_hdr hdr; + /* item start identifier */ + __le32 start_id; + /* item count to query */ + __le32 count; + /* item information size in bytes */ + __le32 size; +}; + +/* common item information header */ +struct virtio_snd_info { + /* function group node id (High Definition Audio Specification 7.1.2) */ + __le32 hda_fn_nid; +}; + +/******************************************************************************* + * JACK CONTROL MESSAGES + */ +struct virtio_snd_jack_hdr { + /* VIRTIO_SND_R_JACK_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::jacks - 1 */ + __le32 jack_id; +}; + +/* supported jack features */ +enum { + VIRTIO_SND_JACK_F_REMAP = 0 +}; + +struct virtio_snd_jack_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_JACK_F_XXX) */ + __le32 features; + /* pin configuration (High Definition Audio Specification 7.3.3.31) */ + __le32 hda_reg_defconf; + /* pin capabilities (High Definition Audio Specification 7.3.4.9) */ + __le32 hda_reg_caps; + /* current jack connection status (0: disconnected, 1: connected) */ + __u8 connected; + + __u8 padding[7]; +}; + +/* jack remapping control request */ +struct virtio_snd_jack_remap { + /* .code = VIRTIO_SND_R_JACK_REMAP */ + struct virtio_snd_jack_hdr hdr; + /* selected association number */ + __le32 association; + /* selected sequence number */ + __le32 sequence; +}; + +/******************************************************************************* + * PCM CONTROL MESSAGES + */ +struct virtio_snd_pcm_hdr { + /* VIRTIO_SND_R_PCM_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* supported PCM stream features */ +enum { + VIRTIO_SND_PCM_F_SHMEM_HOST = 0, + VIRTIO_SND_PCM_F_SHMEM_GUEST, + VIRTIO_SND_PCM_F_MSG_POLLING, + VIRTIO_SND_PCM_F_EVT_SHMEM_PERIODS, + VIRTIO_SND_PCM_F_EVT_XRUNS +}; + +/* supported PCM sample formats */ +enum { + /* analog formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_IMA_ADPCM = 0, /* 4 / 4 bits */ + VIRTIO_SND_PCM_FMT_MU_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_A_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_S18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_U18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_U20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_S24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_U24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_U20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_S24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_U24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_S32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT64, /* 64 / 64 bits */ + /* digital formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_DSD_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_DSD_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_DSD_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME /* 32 / 32 bits */ +}; + +/* supported PCM frame rates */ +enum { + VIRTIO_SND_PCM_RATE_5512 = 0, + VIRTIO_SND_PCM_RATE_8000, + VIRTIO_SND_PCM_RATE_11025, + VIRTIO_SND_PCM_RATE_16000, + VIRTIO_SND_PCM_RATE_22050, + VIRTIO_SND_PCM_RATE_32000, + VIRTIO_SND_PCM_RATE_44100, + VIRTIO_SND_PCM_RATE_48000, + VIRTIO_SND_PCM_RATE_64000, + VIRTIO_SND_PCM_RATE_88200, + VIRTIO_SND_PCM_RATE_96000, + VIRTIO_SND_PCM_RATE_176400, + VIRTIO_SND_PCM_RATE_192000, + VIRTIO_SND_PCM_RATE_384000 +}; + +struct virtio_snd_pcm_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* supported sample format bit map (1 << VIRTIO_SND_PCM_FMT_XXX) */ + __le64 formats; + /* supported frame rate bit map (1 << VIRTIO_SND_PCM_RATE_XXX) */ + __le64 rates; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* minimum # of supported channels */ + __u8 channels_min; + /* maximum # of supported channels */ + __u8 channels_max; + + __u8 padding[5]; +}; + +/* set PCM stream format */ +struct virtio_snd_pcm_set_params { + /* .code = VIRTIO_SND_R_PCM_SET_PARAMS */ + struct virtio_snd_pcm_hdr hdr; + /* size of the hardware buffer */ + __le32 buffer_bytes; + /* size of the hardware period */ + __le32 period_bytes; + /* selected feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* selected # of channels */ + __u8 channels; + /* selected sample format (VIRTIO_SND_PCM_FMT_XXX) */ + __u8 format; + /* selected frame rate (VIRTIO_SND_PCM_RATE_XXX) */ + __u8 rate; + + __u8 padding; +}; + +/******************************************************************************* + * PCM I/O MESSAGES + */ + +/* I/O request header */ +struct virtio_snd_pcm_xfer { + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* I/O request status */ +struct virtio_snd_pcm_status { + /* VIRTIO_SND_S_XXX */ + __le32 status; + /* current device latency */ + __le32 latency_bytes; +}; + +/******************************************************************************* + * CHANNEL MAP CONTROL MESSAGES + */ +struct virtio_snd_chmap_hdr { + /* VIRTIO_SND_R_CHMAP_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::chmaps - 1 */ + __le32 chmap_id; +}; + +/* standard channel position definition */ +enum { + VIRTIO_SND_CHMAP_NONE = 0, /* undefined */ + VIRTIO_SND_CHMAP_NA, /* silent */ + VIRTIO_SND_CHMAP_MONO, /* mono stream */ + VIRTIO_SND_CHMAP_FL, /* front left */ + VIRTIO_SND_CHMAP_FR, /* front right */ + VIRTIO_SND_CHMAP_RL, /* rear left */ + VIRTIO_SND_CHMAP_RR, /* rear right */ + VIRTIO_SND_CHMAP_FC, /* front center */ + VIRTIO_SND_CHMAP_LFE, /* low frequency (LFE) */ + VIRTIO_SND_CHMAP_SL, /* side left */ + VIRTIO_SND_CHMAP_SR, /* side right */ + VIRTIO_SND_CHMAP_RC, /* rear center */ + VIRTIO_SND_CHMAP_FLC, /* front left center */ + VIRTIO_SND_CHMAP_FRC, /* front right center */ + VIRTIO_SND_CHMAP_RLC, /* rear left center */ + VIRTIO_SND_CHMAP_RRC, /* rear right center */ + VIRTIO_SND_CHMAP_FLW, /* front left wide */ + VIRTIO_SND_CHMAP_FRW, /* front right wide */ + VIRTIO_SND_CHMAP_FLH, /* front left high */ + VIRTIO_SND_CHMAP_FCH, /* front center high */ + VIRTIO_SND_CHMAP_FRH, /* front right high */ + VIRTIO_SND_CHMAP_TC, /* top center */ + VIRTIO_SND_CHMAP_TFL, /* top front left */ + VIRTIO_SND_CHMAP_TFR, /* top front right */ + VIRTIO_SND_CHMAP_TFC, /* top front center */ + VIRTIO_SND_CHMAP_TRL, /* top rear left */ + VIRTIO_SND_CHMAP_TRR, /* top rear right */ + VIRTIO_SND_CHMAP_TRC, /* top rear center */ + VIRTIO_SND_CHMAP_TFLC, /* top front left center */ + VIRTIO_SND_CHMAP_TFRC, /* top front right center */ + VIRTIO_SND_CHMAP_TSL, /* top side left */ + VIRTIO_SND_CHMAP_TSR, /* top side right */ + VIRTIO_SND_CHMAP_LLFE, /* left LFE */ + VIRTIO_SND_CHMAP_RLFE, /* right LFE */ + VIRTIO_SND_CHMAP_BC, /* bottom center */ + VIRTIO_SND_CHMAP_BLC, /* bottom left center */ + VIRTIO_SND_CHMAP_BRC /* bottom right center */ +}; + +/* maximum possible number of channels */ +#define VIRTIO_SND_CHMAP_MAX_SIZE 18 + +struct virtio_snd_chmap_info { + /* common header */ + struct virtio_snd_info hdr; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* # of valid channel position values */ + __u8 channels; + /* channel position values (VIRTIO_SND_CHMAP_XXX) */ + __u8 positions[VIRTIO_SND_CHMAP_MAX_SIZE]; +}; + +/******************************************************************************* + * OPENSYNERGY EXTENSIONS + */ +enum { + /* ALSA extended device information support */ + VIRTIO_SND_OPSY_F_DEV_EXT_INFO = 0, + /* ALSA device controls support */ + VIRTIO_SND_OPSY_F_DEV_CTLS +}; + +struct virtio_snd_opsy_info { + /* VIRTIO_SND_S_XXX */ + struct virtio_snd_hdr hdr; + /* supported extension bit map (1 << VIRTIO_SND_OPSY_F_XXX) */ + __le32 extensions; +}; + +/******************************************************************************* + * ALSA EXTENDED DEVICE INFORMATION + */ +struct virtio_snd_alsa_card_info { + /* ID string of soundcard */ + __u8 id[16]; + /* Driver name */ + __u8 driver[16]; + /* short name of soundcard */ + __u8 name[32]; + /* name + info text about soundcard */ + __u8 longname[80]; + /* visual mixer identification */ + __u8 mixername[80]; + /* card components delimited with space */ + __u8 components[128]; +}; + +struct virtio_snd_alsa_query_pcm_info { + /* .code = VIRTIO_SND_R_ALSA_PCM_INFO */ + struct virtio_snd_hdr hdr; + /* function group node id */ + __le32 hda_fn_nid; +}; + +struct virtio_snd_alsa_pcm_info { + /* ID string of PCM device */ + __u8 id[64]; + /* name of PCM device */ + __u8 name[80]; +}; + +/******************************************************************************* + * ALSA DEVICE CONTROLS + */ +struct virtio_snd_dc_hdr { + /* VIRTIO_SND_R_DC_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::controls - 1 */ + __le16 control_id; + /* subcontrol identifier */ + __le16 subcontrol_id; +}; + +/* + * The following structure is de facto part of the protocol, but cannot be + * defined here because it contains the snd_ctl_elem_info body. It represents + * information about a device control in response to the VIRTIO_SND_R_DC_INFO + * command. + * + * struct virtio_snd_dc_info { + * struct virtio_snd_info hdr; + * struct snd_ctl_elem_info elem_info; + * }; + */ + +#define VIRTIO_SND_DC_NAME_MAXLEN 64 + +struct virtio_snd_dc_enum_value { + /* null-terminated ASCII value name */ + __u8 name[VIRTIO_SND_DC_NAME_MAXLEN]; +}; + +struct virtio_snd_dc_event { + /* VIRTIO_SND_EVT_DC_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::controls - 1 */ + __le16 control_id; + /* event mask (SNDRV_CTL_EVENT_MASK_XXX) */ + __le16 mask; +}; + +#endif /* VIRTIO_SND_IF_H */ |