root/sound/virtio/virtio_jack.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * virtio-snd: Virtio sound device
 * Copyright (C) 2021 OpenSynergy GmbH
 */
#include <linux/virtio_config.h>
#include <sound/jack.h>
#include <sound/hda_verbs.h>

#include "virtio_card.h"

/**
 * DOC: Implementation Status
 *
 * At the moment jacks have a simple implementation and can only be used to
 * receive notifications about a plugged in/out device.
 *
 * VIRTIO_SND_R_JACK_REMAP
 *   is not supported
 */

/**
 * struct virtio_jack - VirtIO jack.
 * @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;
        u32 nid;
        u32 features;
        u32 defconf;
        u32 caps;
        bool connected;
        int type;
};

/**
 * virtsnd_jack_get_label() - Get the name string for the jack.
 * @vjack: VirtIO jack.
 *
 * Returns the jack name based on the default pin configuration value (see HDA
 * specification).
 *
 * Context: Any context.
 * Return: Name string.
 */
static const char *virtsnd_jack_get_label(struct virtio_jack *vjack)
{
        unsigned int defconf = vjack->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";
        }
}

/**
 * virtsnd_jack_get_type() - Get the type for the jack.
 * @vjack: VirtIO jack.
 *
 * Returns the jack type based on the default pin configuration value (see HDA
 * specification).
 *
 * Context: Any context.
 * Return: SND_JACK_XXX value.
 */
static int virtsnd_jack_get_type(struct virtio_jack *vjack)
{
        unsigned int defconf = vjack->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;
        }
}

/**
 * virtsnd_jack_parse_cfg() - Parse the jack configuration.
 * @snd: VirtIO sound device.
 *
 * This function is called during initial device initialization.
 *
 * Context: Any context that permits to sleep.
 * Return: 0 on success, -errno on failure.
 */
int virtsnd_jack_parse_cfg(struct virtio_snd *snd)
{
        struct virtio_device *vdev = snd->vdev;
        struct virtio_snd_jack_info *info;
        u32 i;
        int rc;

        virtio_cread_le(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 = kzalloc_objs(*info, snd->njacks);
        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_exit;

        for (i = 0; i < snd->njacks; ++i) {
                struct virtio_jack *vjack = &snd->jacks[i];

                vjack->nid = le32_to_cpu(info[i].hdr.hda_fn_nid);
                vjack->features = le32_to_cpu(info[i].features);
                vjack->defconf = le32_to_cpu(info[i].hda_reg_defconf);
                vjack->caps = le32_to_cpu(info[i].hda_reg_caps);
                vjack->connected = info[i].connected;
        }

on_exit:
        kfree(info);

        return rc;
}

/**
 * virtsnd_jack_build_devs() - Build ALSA controls for jacks.
 * @snd: VirtIO sound device.
 *
 * Context: Any context that permits to sleep.
 * Return: 0 on success, -errno on failure.
 */
int virtsnd_jack_build_devs(struct virtio_snd *snd)
{
        u32 i;
        int rc;

        for (i = 0; i < snd->njacks; ++i) {
                struct virtio_jack *vjack = &snd->jacks[i];

                vjack->type = virtsnd_jack_get_type(vjack);

                rc = snd_jack_new(snd->card, virtsnd_jack_get_label(vjack),
                                  vjack->type, &vjack->jack, true, true);
                if (rc)
                        return rc;

                if (vjack->jack)
                        vjack->jack->private_data = vjack;

                snd_jack_report(vjack->jack,
                                vjack->connected ? vjack->type : 0);
        }

        return 0;
}

/**
 * virtsnd_jack_event() - Handle the jack event notification.
 * @snd: VirtIO sound device.
 * @event: VirtIO sound event.
 *
 * Context: Interrupt context.
 */
void virtsnd_jack_event(struct virtio_snd *snd, struct virtio_snd_event *event)
{
        u32 jack_id = le32_to_cpu(event->data);
        struct virtio_jack *vjack;

        if (jack_id >= snd->njacks)
                return;

        vjack = &snd->jacks[jack_id];

        switch (le32_to_cpu(event->hdr.code)) {
        case VIRTIO_SND_EVT_JACK_CONNECTED:
                vjack->connected = true;
                break;
        case VIRTIO_SND_EVT_JACK_DISCONNECTED:
                vjack->connected = false;
                break;
        default:
                return;
        }

        snd_jack_report(vjack->jack, vjack->connected ? vjack->type : 0);
}