ALSA: hda - Add the inverted digital mic workaround to Realtek codecs
authorTakashi Iwai <tiwai@suse.de>
Fri, 22 Jun 2012 12:30:29 +0000 (14:30 +0200)
committerTakashi Iwai <tiwai@suse.de>
Mon, 25 Jun 2012 07:45:54 +0000 (09:45 +0200)
Some laptops are equipped with ForteMedia digital mics that give the
differential input.  With such devices, summing stereo streams into a
mono (like PulseAudio does) results in almost silence.

This patch provides a workaround for this bug by adding a new mixer
switch to turn on/off the right channel of digital mic, just like a
similar fix for Conexant codecs.

When the new switch "Inverted Internal Mic Capture Switch" is off and
the current input source is the digital mic, the right channel of the
recording stream is muted.  When another input source is selected, the
right channel is restored.

Tested-by: Eliot Blennerhassett <eliot@blennerhassett.gen.nz>
Signed-off-by: Takashi Iwai <tiwai@suse.de>
sound/pci/hda/patch_realtek.c

index f8f4906e498ddd447ff27df2d33b3bf51282af86..a0a3cf956503accca344a5982bd6e997c77ca2e7 100644 (file)
@@ -170,6 +170,7 @@ struct alc_spec {
        hda_nid_t imux_pins[HDA_MAX_NUM_INPUTS];
        unsigned int dyn_adc_idx[HDA_MAX_NUM_INPUTS];
        int int_mic_idx, ext_mic_idx, dock_mic_idx; /* for auto-mic */
+       hda_nid_t inv_dmic_pin;
 
        /* hooks */
        void (*init_hook)(struct hda_codec *codec);
@@ -201,6 +202,8 @@ struct alc_spec {
        unsigned int vol_in_capsrc:1; /* use capsrc volume (ADC has no vol) */
        unsigned int parse_flags; /* passed to snd_hda_parse_pin_defcfg() */
        unsigned int shared_mic_hp:1; /* HP/Mic-in sharing */
+       unsigned int inv_dmic_fixup:1; /* has inverted digital-mic workaround */
+       unsigned int inv_dmic_muted:1; /* R-ch of inv d-mic is muted? */
 
        /* auto-mute control */
        int automute_mode;
@@ -298,6 +301,7 @@ static inline hda_nid_t get_capsrc(struct alc_spec *spec, int idx)
 }
 
 static void call_update_outputs(struct hda_codec *codec);
+static void alc_inv_dmic_sync(struct hda_codec *codec, bool force);
 
 /* select the given imux item; either unmute exclusively or select the route */
 static int alc_mux_select(struct hda_codec *codec, unsigned int adc_idx,
@@ -368,6 +372,7 @@ static int alc_mux_select(struct hda_codec *codec, unsigned int adc_idx,
                                          AC_VERB_SET_CONNECT_SEL,
                                          imux->items[idx].index);
        }
+       alc_inv_dmic_sync(codec, true);
        return 1;
 }
 
@@ -1556,14 +1561,14 @@ typedef int (*getput_call_t)(struct snd_kcontrol *kcontrol,
 
 static int alc_cap_getput_caller(struct snd_kcontrol *kcontrol,
                                 struct snd_ctl_elem_value *ucontrol,
-                                getput_call_t func, bool check_adc_switch)
+                                getput_call_t func, bool is_put)
 {
        struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
        struct alc_spec *spec = codec->spec;
        int i, err = 0;
 
        mutex_lock(&codec->control_mutex);
-       if (check_adc_switch && spec->dyn_adc_switch) {
+       if (is_put && spec->dyn_adc_switch) {
                for (i = 0; i < spec->num_adc_nids; i++) {
                        kcontrol->private_value =
                                HDA_COMPOSE_AMP_VAL(spec->adc_nids[i],
@@ -1584,6 +1589,8 @@ static int alc_cap_getput_caller(struct snd_kcontrol *kcontrol,
                                                    3, 0, HDA_INPUT);
                err = func(kcontrol, ucontrol);
        }
+       if (err >= 0 && is_put)
+               alc_inv_dmic_sync(codec, false);
  error:
        mutex_unlock(&codec->control_mutex);
        return err;
@@ -1675,6 +1682,108 @@ DEFINE_CAPMIX_NOSRC(1);
 DEFINE_CAPMIX_NOSRC(2);
 DEFINE_CAPMIX_NOSRC(3);
 
+/*
+ * Inverted digital-mic handling
+ *
+ * First off, it's a bit tricky.  The "Inverted Internal Mic Capture Switch"
+ * gives the additional mute only to the right channel of the digital mic
+ * capture stream.  This is a workaround for avoiding the almost silence
+ * by summing the stereo stream from some (known to be ForteMedia)
+ * digital mic unit.
+ *
+ * The logic is to call alc_inv_dmic_sync() after each action (possibly)
+ * modifying ADC amp.  When the mute flag is set, it mutes the R-channel
+ * without caching so that the cache can still keep the original value.
+ * The cached value is then restored when the flag is set off or any other
+ * than d-mic is used as the current input source.
+ */
+static void alc_inv_dmic_sync(struct hda_codec *codec, bool force)
+{
+       struct alc_spec *spec = codec->spec;
+       int i;
+
+       if (!spec->inv_dmic_fixup)
+               return;
+       if (!spec->inv_dmic_muted && !force)
+               return;
+       for (i = 0; i < spec->num_adc_nids; i++) {
+               int src = spec->dyn_adc_switch ? 0 : i;
+               bool dmic_fixup = false;
+               hda_nid_t nid;
+               int parm, dir, v;
+
+               if (spec->inv_dmic_muted &&
+                   spec->imux_pins[spec->cur_mux[src]] == spec->inv_dmic_pin)
+                       dmic_fixup = true;
+               if (!dmic_fixup && !force)
+                       continue;
+               if (spec->vol_in_capsrc) {
+                       nid = spec->capsrc_nids[i];
+                       parm = AC_AMP_SET_RIGHT | AC_AMP_SET_OUTPUT;
+                       dir = HDA_OUTPUT;
+               } else {
+                       nid = spec->adc_nids[i];
+                       parm = AC_AMP_SET_RIGHT | AC_AMP_SET_INPUT;
+                       dir = HDA_INPUT;
+               }
+               /* we care only right channel */
+               v = snd_hda_codec_amp_read(codec, nid, 1, dir, 0);
+               if (v & 0x80) /* if already muted, we don't need to touch */
+                       continue;
+               if (dmic_fixup) /* add mute for d-mic */
+                       v |= 0x80;
+               snd_hda_codec_write(codec, nid, 0, AC_VERB_SET_AMP_GAIN_MUTE,
+                                   parm | v);
+       }
+}
+
+static int alc_inv_dmic_sw_get(struct snd_kcontrol *kcontrol,
+                              struct snd_ctl_elem_value *ucontrol)
+{
+       struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+       struct alc_spec *spec = codec->spec;
+
+       ucontrol->value.integer.value[0] = !spec->inv_dmic_muted;
+       return 0;
+}
+
+static int alc_inv_dmic_sw_put(struct snd_kcontrol *kcontrol,
+                              struct snd_ctl_elem_value *ucontrol)
+{
+       struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+       struct alc_spec *spec = codec->spec;
+       unsigned int val = !ucontrol->value.integer.value[0];
+
+       if (val == spec->inv_dmic_muted)
+               return 0;
+       spec->inv_dmic_muted = val;
+       alc_inv_dmic_sync(codec, true);
+       return 0;
+}
+
+static const struct snd_kcontrol_new alc_inv_dmic_sw = {
+       .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+       .info = snd_ctl_boolean_mono_info,
+       .get = alc_inv_dmic_sw_get,
+       .put = alc_inv_dmic_sw_put,
+};
+
+static int alc_add_inv_dmic_mixer(struct hda_codec *codec, hda_nid_t nid)
+{
+       struct alc_spec *spec = codec->spec;
+       struct snd_kcontrol_new *knew = alc_kcontrol_new(spec);
+       if (!knew)
+               return -ENOMEM;
+       *knew = alc_inv_dmic_sw;
+       knew->name = kstrdup("Inverted Internal Mic Capture Switch", GFP_KERNEL);
+       if (!knew->name)
+               return -ENOMEM;
+       spec->inv_dmic_fixup = 1;
+       spec->inv_dmic_muted = 0;
+       spec->inv_dmic_pin = nid;
+       return 0;
+}
+
 /*
  * virtual master controls
  */
@@ -2316,6 +2425,7 @@ static int alc_resume(struct hda_codec *codec)
        codec->patch_ops.init(codec);
        snd_hda_codec_resume_amp(codec);
        snd_hda_codec_resume_cache(codec);
+       alc_inv_dmic_sync(codec, true);
        hda_call_check_power_status(codec, 0x01);
        return 0;
 }
@@ -6424,6 +6534,13 @@ static void alc272_fixup_mario(struct hda_codec *codec,
                       "hda_codec: failed to override amp caps for NID 0x2\n");
 }
 
+static void alc662_fixup_inv_dmic(struct hda_codec *codec,
+                                 const struct alc_fixup *fix, int action)
+{
+       if (action == ALC_FIXUP_ACT_PROBE)
+               alc_add_inv_dmic_mixer(codec, 0x12);
+}
+
 enum {
        ALC662_FIXUP_ASPIRE,
        ALC662_FIXUP_IDEAPAD,
@@ -6441,6 +6558,7 @@ enum {
        ALC662_FIXUP_ASUS_MODE8,
        ALC662_FIXUP_NO_JACK_DETECT,
        ALC662_FIXUP_ZOTAC_Z68,
+       ALC662_FIXUP_INV_DMIC,
 };
 
 static const struct alc_fixup alc662_fixups[] = {
@@ -6597,12 +6715,17 @@ static const struct alc_fixup alc662_fixups[] = {
                        { }
                }
        },
+       [ALC662_FIXUP_INV_DMIC] = {
+               .type = ALC_FIXUP_FUNC,
+               .v.func = alc662_fixup_inv_dmic,
+       },
 };
 
 static const struct snd_pci_quirk alc662_fixup_tbl[] = {
        SND_PCI_QUIRK(0x1019, 0x9087, "ECS", ALC662_FIXUP_ASUS_MODE2),
        SND_PCI_QUIRK(0x1025, 0x0308, "Acer Aspire 8942G", ALC662_FIXUP_ASPIRE),
        SND_PCI_QUIRK(0x1025, 0x031c, "Gateway NV79", ALC662_FIXUP_SKU_IGNORE),
+       SND_PCI_QUIRK(0x1025, 0x0349, "eMachines eM250", ALC662_FIXUP_INV_DMIC),
        SND_PCI_QUIRK(0x1025, 0x038b, "Acer Aspire 8943G", ALC662_FIXUP_ASPIRE),
        SND_PCI_QUIRK(0x103c, 0x1632, "HP RP5800", ALC662_FIXUP_HP_RP5800),
        SND_PCI_QUIRK(0x1043, 0x8469, "ASUS mobo", ALC662_FIXUP_NO_JACK_DETECT),