[PATCH] ALSA: hda/generic: Add mic autoswitch support for dyn_adc_switch mode

From: Zhang Heng

Date: Sun May 03 2026 - 07:45:33 EST


When auto_mic is not available but dyn_adc_switch mode is enabled
(e.g., on laptops with both front and rear mic jacks), this patch
enables automatic microphone switching based on jack detection.

The patch includes three changes:

1. In check_dyn_adc_switch(): Register jack detect callbacks for
all input pins when dyn_adc_switch is enabled and auto_mic is
not available.

2. In call_mic_autoswitch(): Add handling for dyn_adc_switch mode
to check imux_pins[] for jack presence, searching from back to
front (last inserted wins).

3. In mux_select(): Notify Capture Source controls after switching
to sync with user-space (PulseAudio/PipeWire).

Problem description:
On Ubuntu 20.04 (with older PulseAudio/PipeWire):
- Front mic is unplugged
- Plug in rear mic
- Jack event is reported correctly
- Volume control (PulseAudio/PipeWire) recognizes the rear mic
- But alsamixer does NOT switch to rear mic
- Codec also does NOT perform the switch

The root cause is that in dyn_adc_switch mode without auto_mic,
the jack detect callback was not properly set up to trigger the
mic autoswitch. The call_mic_autoswitch() function only calls
mic_autoswitch_hook or snd_hda_gen_mic_autoswitch(), which rely
on auto_mic being enabled.

Additionally, after mux_select() performs the switch, user-space
(PulseAudio/PipeWire) may not be aware of the path change,
causing Capture Switch to show 'off'.

This patch fixes both issues by:
1. Registering jack detect callbacks for all input pins in
dyn_adc_switch mode
2. Notifying Capture Source controls after switching

Tested on SN6186 codec with Ubuntu 20.04 and 25.10.

Question to the community: Is this approach correct? Should additional
changes be made to handle mute state preservation, or is this purely
a user-space issue that requires updating PulseAudio/PipeWire?

Testing and feedback are welcome.

Signed-off-by: Zhang Heng <zhangheng@xxxxxxxxxx>
---
sound/hda/codecs/generic.c | 82 ++++++++++++++++++++++++++++++++++++--
1 file changed, 79 insertions(+), 3 deletions(-)

diff --git a/sound/hda/codecs/generic.c b/sound/hda/codecs/generic.c
index 660a9f2c0ded..c536de10d8b8 100644
--- a/sound/hda/codecs/generic.c
+++ b/sound/hda/codecs/generic.c
@@ -27,6 +27,10 @@
#include "hda_beep.h"
#include "generic.h"

+/* Forward declaration */
+static void call_mic_autoswitch(struct hda_codec *codec,
+ struct hda_jack_callback *jack);
+

/**
* snd_hda_gen_spec_init - initialize hda_gen_spec struct
@@ -3238,6 +3242,19 @@ static int check_dyn_adc_switch(struct hda_codec *codec)
if (!spec->dyn_adc_switch && spec->multi_cap_vol)
spec->num_adc_nids = 1;

+ /* Enable mic autoswitch for dyn_adc_switch mode when auto_mic is not available */
+ if (!spec->auto_mic && imux->num_items > 1) {
+ int i;
+ for (i = 0; i < imux->num_items; i++) {
+ hda_nid_t pin = spec->imux_pins[i];
+ if (!is_jack_detectable(codec, pin))
+ continue;
+ snd_hda_jack_detect_enable_callback(codec, pin,
+ call_mic_autoswitch);
+ }
+ codec_dbg(codec, "Enable mic autoswitch for input sources\n");
+ }
+
return 0;
}

@@ -4107,9 +4124,38 @@ static int mux_select(struct hda_codec *codec, unsigned int adc_idx,
path = get_input_path(codec, adc_idx, idx);
if (!path)
return 0;
- if (path->active)
- return 0;
- snd_hda_activate_path(codec, path, true, false);
+ if (path->active) {
+ codec_err(codec, "[MUX] mux_select: path already active, skip activate but still notify\n");
+ } else {
+ snd_hda_activate_path(codec, path, true, false);
+ }
+
+ /* Notify Input Source / Capture Source controls that the path has changed */
+ if (!spec->auto_mic && spec->input_mux.num_items > 1) {
+ struct snd_card *card = codec->card;
+ struct snd_kcontrol *kctl;
+ struct snd_ctl_elem_id id;
+ int notified = 0;
+
+ codec_err(codec, "[MUX] mux_select: notifying controls, num_items=%d\n", spec->input_mux.num_items);
+
+ if (card) {
+ list_for_each_entry(kctl, &card->controls, list) {
+ if (kctl->id.iface != SNDRV_CTL_ELEM_IFACE_MIXER)
+ continue;
+ codec_err(codec, "[MUX] mux_select: found control '%s'\n", kctl->id.name);
+ if (strncmp(kctl->id.name, "Capture Source", 14) == 0) {
+ id = kctl->id;
+ snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &id);
+ codec_err(codec, "[MUX] mux_select: notified '%s'\n", kctl->id.name);
+ notified = 1;
+ }
+ }
+ }
+ if (!notified)
+ codec_err(codec, "[MUX] mux_select: no Input/Capture Source control found!\n");
+ }
+
if (spec->cap_sync_hook)
spec->cap_sync_hook(codec, NULL, NULL);
path_power_down_sync(codec, old_path);
@@ -4602,6 +4648,36 @@ static void call_mic_autoswitch(struct hda_codec *codec,
struct hda_jack_callback *jack)
{
struct hda_gen_spec *spec = codec->spec;
+ const struct hda_input_mux *imux;
+ int i;
+
+ /* Handle dyn_adc_switch mode - use imux_pins[] */
+ if (!spec->auto_mic && spec->dyn_adc_switch) {
+ imux = &spec->input_mux;
+ if (imux->num_items <= 1)
+ goto fallback;
+
+ codec_err(codec, "[AUTO_MIC] call_mic_autoswitch: dyn_adc_switch mode, checking %d inputs\n", imux->num_items);
+
+ /* Search from back to front (last inserted wins) */
+ for (i = imux->num_items - 1; i >= 0; i--) {
+ hda_nid_t pin = spec->imux_pins[i];
+ int det = is_jack_detectable(codec, pin);
+ int state = snd_hda_jack_detect_state(codec, pin);
+ codec_err(codec, "[AUTO_MIC] call_mic_autoswitch: imux[%d] pin=0x%02x, det=%d, state=%s\n",
+ i, pin, det, state == HDA_JACK_PRESENT ? "PRESENT" : "ABSENT");
+ if (det && state == HDA_JACK_PRESENT) {
+ codec_err(codec, "[AUTO_MIC] call_mic_autoswitch: switching to imux %d\n", i);
+ mux_select(codec, 0, i);
+ return;
+ }
+ }
+ /* No jack present, keep current selection (don't switch to imux 0) */
+ codec_err(codec, "[AUTO_MIC] call_mic_autoswitch: no jack present, keeping current\n");
+ return;
+ }
+
+fallback:
if (spec->mic_autoswitch_hook)
spec->mic_autoswitch_hook(codec, jack);
else
--
2.25.1