Commit 4b8ea38f authored by Jan Schär's avatar Jan Schär Committed by Takashi Iwai
Browse files

ALSA: usb-audio: Support jack detection on Dell dock



The Dell WD15 dock has a headset and a line out port. Add support for
detecting if a jack is inserted into one of these ports.
For the headset jack, additionally determine if a mic is present.

The WD15 contains an ALC4020 USB audio controller and ALC3263 audio codec
from Realtek. It is a UAC 1 device, and UAC 1 does not support jack
detection. Instead, jack detection works by sending HD Audio commands over
vendor-type USB messages.

I found out how it works by looking at USB captures on Windows.
The audio codec is very similar to the one supported by
sound/soc/codecs/rt298.c / rt298.h, some constant names and the mic
detection are adapted from there. The realtek_add_jack function is adapted
from build_connector_control in sound/usb/mixer.c.

I tested this on a WD15 dock with the latest firmware.

Signed-off-by: default avatarJan Schär <jan@jschaer.ch>
Link: https://lore.kernel.org/r/20220627171855.42338-1-jan@jschaer.ch


Signed-off-by: default avatarTakashi Iwai <tiwai@suse.de>
parent 4a1e6ac7
Loading
Loading
Loading
Loading
+167 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@
#include <sound/asoundef.h>
#include <sound/core.h>
#include <sound/control.h>
#include <sound/hda_verbs.h>
#include <sound/hwdep.h>
#include <sound/info.h>
#include <sound/tlv.h>
@@ -1934,6 +1935,169 @@ static int snd_soundblaster_e1_switch_create(struct usb_mixer_interface *mixer)
					  NULL);
}

/*
 * Dell WD15 dock jack detection
 *
 * The WD15 contains an ALC4020 USB audio controller and ALC3263 audio codec
 * from Realtek. It is a UAC 1 device, and UAC 1 does not support jack
 * detection. Instead, jack detection works by sending HD Audio commands over
 * vendor-type USB messages.
 */

#define HDA_VERB_CMD(V, N, D) (((N) << 20) | ((V) << 8) | (D))

#define REALTEK_HDA_VALUE 0x0038

#define REALTEK_HDA_SET		62
#define REALTEK_HDA_GET_OUT	88
#define REALTEK_HDA_GET_IN	89

#define REALTEK_LINE1			0x1a
#define REALTEK_VENDOR_REGISTERS	0x20
#define REALTEK_HP_OUT			0x21

#define REALTEK_CBJ_CTRL2 0x50

#define REALTEK_JACK_INTERRUPT_NODE 5

#define REALTEK_MIC_FLAG 0x100

static int realtek_hda_set(struct snd_usb_audio *chip, u32 cmd)
{
	struct usb_device *dev = chip->dev;
	u32 buf = cpu_to_be32(cmd);

	return snd_usb_ctl_msg(dev, usb_sndctrlpipe(dev, 0), REALTEK_HDA_SET,
			       USB_RECIP_DEVICE | USB_TYPE_VENDOR | USB_DIR_OUT,
			       REALTEK_HDA_VALUE, 0, &buf, sizeof(buf));
}

static int realtek_hda_get(struct snd_usb_audio *chip, u32 cmd, u32 *value)
{
	struct usb_device *dev = chip->dev;
	int err;
	u32 buf = cpu_to_be32(cmd);

	err = snd_usb_ctl_msg(dev, usb_sndctrlpipe(dev, 0), REALTEK_HDA_GET_OUT,
			      USB_RECIP_DEVICE | USB_TYPE_VENDOR | USB_DIR_OUT,
			      REALTEK_HDA_VALUE, 0, &buf, sizeof(buf));
	if (err < 0)
		return err;
	err = snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), REALTEK_HDA_GET_IN,
			      USB_RECIP_DEVICE | USB_TYPE_VENDOR | USB_DIR_IN,
			      REALTEK_HDA_VALUE, 0, &buf, sizeof(buf));
	if (err < 0)
		return err;

	*value = be32_to_cpu(buf);
	return 0;
}

static int realtek_ctl_connector_get(struct snd_kcontrol *kcontrol,
				     struct snd_ctl_elem_value *ucontrol)
{
	struct usb_mixer_elem_info *cval = kcontrol->private_data;
	struct snd_usb_audio *chip = cval->head.mixer->chip;
	u32 pv = kcontrol->private_value;
	u32 node_id = pv & 0xff;
	u32 sense;
	u32 cbj_ctrl2;
	bool presence;
	int err;

	err = snd_usb_lock_shutdown(chip);
	if (err < 0)
		return err;
	err = realtek_hda_get(chip,
			      HDA_VERB_CMD(AC_VERB_GET_PIN_SENSE, node_id, 0),
			      &sense);
	if (err < 0)
		goto err;
	if (pv & REALTEK_MIC_FLAG) {
		err = realtek_hda_set(chip,
				      HDA_VERB_CMD(AC_VERB_SET_COEF_INDEX,
						   REALTEK_VENDOR_REGISTERS,
						   REALTEK_CBJ_CTRL2));
		if (err < 0)
			goto err;
		err = realtek_hda_get(chip,
				      HDA_VERB_CMD(AC_VERB_GET_PROC_COEF,
						   REALTEK_VENDOR_REGISTERS, 0),
				      &cbj_ctrl2);
		if (err < 0)
			goto err;
	}
err:
	snd_usb_unlock_shutdown(chip);
	if (err < 0)
		return err;

	presence = sense & AC_PINSENSE_PRESENCE;
	if (pv & REALTEK_MIC_FLAG)
		presence = presence && (cbj_ctrl2 & 0x0070) == 0x0070;
	ucontrol->value.integer.value[0] = presence;
	return 0;
}

static const struct snd_kcontrol_new realtek_connector_ctl_ro = {
	.iface = SNDRV_CTL_ELEM_IFACE_CARD,
	.name = "", /* will be filled later manually */
	.access = SNDRV_CTL_ELEM_ACCESS_READ,
	.info = snd_ctl_boolean_mono_info,
	.get = realtek_ctl_connector_get,
};

static int realtek_resume_jack(struct usb_mixer_elem_list *list)
{
	snd_ctl_notify(list->mixer->chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
		       &list->kctl->id);
	return 0;
}

static int realtek_add_jack(struct usb_mixer_interface *mixer,
			    char *name, u32 val)
{
	struct usb_mixer_elem_info *cval;
	struct snd_kcontrol *kctl;

	cval = kzalloc(sizeof(*cval), GFP_KERNEL);
	if (!cval)
		return -ENOMEM;
	snd_usb_mixer_elem_init_std(&cval->head, mixer,
				    REALTEK_JACK_INTERRUPT_NODE);
	cval->head.resume = realtek_resume_jack;
	cval->val_type = USB_MIXER_BOOLEAN;
	cval->channels = 1;
	cval->min = 0;
	cval->max = 1;
	kctl = snd_ctl_new1(&realtek_connector_ctl_ro, cval);
	if (!kctl) {
		kfree(cval);
		return -ENOMEM;
	}
	kctl->private_value = val;
	strscpy(kctl->id.name, name, sizeof(kctl->id.name));
	kctl->private_free = snd_usb_mixer_elem_free;
	return snd_usb_mixer_add_control(&cval->head, kctl);
}

static int dell_dock_mixer_create(struct usb_mixer_interface *mixer)
{
	int err;

	err = realtek_add_jack(mixer, "Line Out Jack", REALTEK_LINE1);
	if (err < 0)
		return err;
	err = realtek_add_jack(mixer, "Headphone Jack", REALTEK_HP_OUT);
	if (err < 0)
		return err;
	err = realtek_add_jack(mixer, "Headset Mic Jack",
			       REALTEK_HP_OUT | REALTEK_MIC_FLAG);
	if (err < 0)
		return err;
	return 0;
}

static void dell_dock_init_vol(struct snd_usb_audio *chip, int ch, int id)
{
	u16 buf = 0;
@@ -3245,6 +3409,9 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
		err = snd_soundblaster_e1_switch_create(mixer);
		break;
	case USB_ID(0x0bda, 0x4014): /* Dell WD15 dock */
		err = dell_dock_mixer_create(mixer);
		if (err < 0)
			break;
		err = dell_dock_mixer_init(mixer);
		break;