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: Jan Schär <jan@jschaer.ch>
Link: https://lore.kernel.org/r/20220627171855.42338-1-jan@jschaer.ch
Signed-off-by: Takashi Iwai <tiwai@suse.de>
This commit is contained in:
Jan Schär 2022-06-27 19:18:54 +02:00 committed by Takashi Iwai
parent 4a1e6ac7d5
commit 4b8ea38fab

View File

@ -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;