quine-linux/docs/imac19-2-mic-fix.md
Philip Peterson c51070b7a8 Driver
2026-09-26 01:25:14 -08:00

13 KiB

iMac19,2 internal mic: silent capture despite maxed gain

TL;DR

The internal mic shows up normally in PipeWire/ALSA/Discord, with fully adjustable gain controls, but captures almost nothing regardless of how high any gain is pushed. Root cause: the CS8409 codec driver's HDA auto-config binds the exposed capture PCM stream to the wrong ADC node, orphaning the real internal-mic ADC entirely — its mixer controls work, but nothing is ever routed through them. A fix for this exact bug, confirmed on a sibling Mac model, is packaged into this repo's build (patches/snd-hda-codec-cs8409/) but not yet verified on this exact iMac19,2 hardware — see Current verification status.

Hardware / software identification

  • Model: Apple iMac19,2 (2019, 21.5")
  • Distro: Aurora (uBlue/Fedora Kinoite bootc image), Kinoite variant 44.20260908.1
  • Kernel: 7.1.8-200.fc44.x86_64
  • Audio codec: Cirrus Logic CS8409, HDA vendor id 0x10138409, subsystem id 0x106b0f00 (confirmed via /proc/asound/card0/codec#0)
  • Secure Boot: unsupported/disabled on this hardware (mokutil --sb-state) — a documented non-goal of this fix; see Open risks.

Symptom

Mic appears as a normal PipeWire source (Built-in Audio Analog Stereo) and ALSA capture device (CS8409 Analog, card 0). All of the following were confirmed at 100% (or higher) with no effect on captured level:

  • ALSA Internal Mic capture volume: 100% (63/63), unmuted
  • ALSA Internal Mic Boost: 100% (2/2)
  • PipeWire source software volume: tested at 1.0, 4.0, and 8.0 (up to +18dB)
  • Discord's own in-app input-volume slider: maxed

Direct measurement, bypassing Discord and any GUI entirely, via pw-record --target <mic source> / arecord, analyzed with ffmpeg -af volumedetect:

Gain setting mean_volume max_volume
PipeWire source @ 1.0x (100%, all ALSA gain also maxed) -80.8 dB -71.2 dB
PipeWire source @ 4.0x -59.7 dB -50.3 dB
PipeWire source @ 8.0x -59.7 dB -50.3 dB (uncontrolled re-test, not a true ceiling — see note below)

The 1x → 4x jump is real, reproducible signal (matches the expected amplification). The identical 4x/8x reading is not evidence of a hard ceiling — it came from two takes that weren't spoken into with controlled, identical volume/distance, so it's not a clean A/B. Even at its best measured point (-50dB max), this is far too quiet for usable speech (normal speech should peak around -12 to -6dB) — consistent with "gain is working, but there's very little real signal to amplify in the first place."

Ruled out, with evidence

  • Wrong/muted output device — a real but separate issue found early on: the output sink was muted at 0% (unrelated to the mic). Fixed independently (wpctl set-mute @DEFAULT_AUDIO_SINK@ 0).
  • PipeWire/ALSA mute or wrong default device — wpctl status, pactl list sources, and amixer all independently agreed the mic source was unmuted and at 100%+ from three different vantage points.
  • Discord-side gain/config — Discord's own input-volume slider was already at maximum; its settings.json doesn't store per-account voice/mic settings in plaintext (those sync via Discord's own account storage), so this was ruled out by the maxed-slider observation rather than a config file.
  • The camera providing the audio instead of the codec — arecord -l shows exactly one ALSA capture device (CS8409 Analog, card 0); the FaceTime HD Camera (USB 05ac:8511) exposes no audio interface to Linux at all (checked via lsusb + absence from arecord -l/arecord -L).
  • Apple T2 chip audio routing — ruled out: no Apple PCI bridge/T2 device present (lspci -nn), and iMac19,2 (2019 21.5") predates T2's presence in Macs' internal-mic path regardless.
  • Missing Flatpak/portal permission for Discord's mic access — wpctl status showed Discord's own WEBRTC VoiceEngine client (verified by cross-referencing the sandboxed PID against the host process list) with an active capture stream connected to the real hardware node — a permission denial would show no connected stream at all, not merely low level.

Root cause

Traced to a specific, very recent (2026-08-19) upstream bug report and fix on a sibling Mac model:

In patch_cirrus_apple.h, function cs_8409_apple_boot_init(), the SNDRV_PCM_STREAM_CAPTURE branch only handles two cases for the capture stream's hinfo->nid:

  • hinfo->nid == spec->intmike_adc_nid — the real internal-mic ADC, set up correctly.
  • hinfo->nid == 0x1a — the headset-mic node, set up correctly.

There's no fallback for when HDA auto-config lands the exposed capture PCM stream on neither — e.g. landing on node 0x12, wired to an unconfigured external Line-In pin (0x34) with no physical signal path at all. When this happens, the real internal-mic ADC (spec->intmike_adc_nid, 0x23 for our subsystem-id group) has all its mixer/gain/switch controls correctly present and adjustable — which is exactly why raising every gain control had no effect — but is never attached to any PCM stream, so it's completely orphaned. Confirmed on the sibling model via journalctl -kf during a live arecord test: the internal-mic-specific prepare hook was never called, only the generic default path ran.

Our subsystem id (0x106b0f00) is grouped together with the sibling model's family (0x106b1000, 0x106b0f00, 0x106b0e00) in this same file's intmike_adc_nid assignment logic — all three get intmike_nid = 0x45, intmike_adc_nid = 0x23 — matching our own kernel log (snd_hda_codec_cs8409 hdaudioC0D0: ... Internal Mic=0x45). So this is a plausible direct match, though confirmed only on iMac18,1, not this exact iMac19,2 — see Open risks.

On the sibling model, applying the fix took capture from -91dB pure digital silence to a real -16.9dB mean / -0.0dB max signal.

The fix

See patches/snd-hda-codec-cs8409/0001-imac19-2-force-intmike-adc-nid-fallback.patch for the actual diff (source of truth — not duplicated here). In short: if the capture stream's hinfo->nid matches neither the internal-mic ADC nor the headset-mic node, force it onto the internal-mic ADC before the existing setup code runs.

On the sibling model, the internal-mic ADC node also came up in HDA power state D3 (suspended) after cold boot, failing capture with an I/O error until forced to D0 via hda-verb ... 0x23 SET_POWER_STATE 0x0 — not persistent across reboots. This repo includes a defensive systemd unit (cs8409-intmike-power-fixup.service, running overlay/usr/libexec/quine-linux/cs8409-intmike-power-fixup.sh) that logs the power state and retries forcing D0 a few seconds after boot. Whether iMac19,2 actually hits this at all is unconfirmed — its own journal output after the real test boot is the evidence that will settle it.

Packaging approach

The fix is built into this repo's own bootc image rather than hand-patched live on the running system, specifically to avoid a documented panic risk (see Open risks) and to get free, cheap rollback via bootc rollback/rpm-ostree rollback if anything goes wrong.

  • patches/snd-hda-codec-cs8409/vendor/ — a verbatim copy of the 16 files from the pinned upstream commit (daviddoll/snd-hda-codec-cs8409 @ 14e00de4290e090afd7632bc6d5c724131a19e7b), vendored rather than fetched at build time because this is an unmerged personal-fork branch that could be force-pushed or deleted with no notice. See PROVENANCE.md for the full origin/license/rationale writeup (license: GPL-2.0-or-later, one file GPL-2.0-only, per each file's own original SPDX header).
  • patches/snd-hda-codec-cs8409/0001-imac19-2-force-intmike-adc-nid-fallback.patch — the actual functional change, kept separate from the vendored copy so it's easy to re-diff if/when PR #30 lands upstream and this vendoring can be dropped.
  • Containerfile — a new kmod-builder stage installs kernel headers matching the exact kernel shipped in the aurora:stable base image (pinned by digest, shared between both stages via a single ARG, so they can't drift apart), applies the patch, and builds the module with the vendored Makefile. The final stage copies the resulting .ko into /usr/lib/modules/<kernel-version>/extra/ (the same convention Fedora's own kmod-* subpackages use to override an in-tree module of the same name/modalias — no blacklisting or renaming needed) and re-runs depmod against the final image's own module tree.
  • DKMS was deliberately not used: it exists to rebuild modules at boot/install time when a kernel updates later on a mutable host. On this bootc image, a new kernel only ever arrives bundled inside a freshly built image, at which point the kmod-builder stage reruns from scratch anyway — DKMS's rebuild-on-boot machinery is redundant weight here.

Current verification status

Not yet tested on real hardware as of 2026-09-26. The patch has been verified to apply cleanly (git apply --check) and produce the intended diff against the vendored source. The Containerfile has been written and a local podman build sanity check (module compiles, vermagic matches the target kernel) is the next step. Actually switching this machine to boot the built image (bootc switch/rpm-ostree rebase + reboot) is a separate, explicitly confirmed step, not yet taken, given this hardware/kernel/fix combination is entirely unverified. This section will be updated with real results (measured dB, Discord input-meter behavior, and whether the D3 power-state unit actually fired) once that test happens.

Open risks / unknowns

  • Kernel API mismatch risk: the vendored fork targets Linux 6.17+ HDA API; our kernel is 7.1.8. Untested whether it compiles cleanly without further porting — but this fails safely at podman build time, before ever touching the running system.
  • Unconfirmed on this exact model: the fix is confirmed only on iMac18,1, not this iMac19,2/subsystem-id combination, even though the intmike_adc_nid code path is shared across both per direct source inspection.
  • D3 power-state issue unconfirmed on this model — only confirmed on the sibling; the defensive unit's journal is the intended way to settle this.
  • Module search-path precedence (extra/ overriding in-tree kernel/...) is asserted from documented Fedora/kmod convention, not yet verified on this image — check modinfo -F filename snd_hda_codec_cs8409 after the real test boot.
  • Live-unbind panic risk: a live unbind/rmmod/modprobe/bind cycle of this module while PipeWire has the sink active caused an unrecoverable freeze on the sibling model. This is exactly why all testing here goes through a full image build + reboot, never a live module reload.
  • Secure Boot non-goal: this approach assumes Secure Boot stays disabled on this hardware. If it's ever enabled, this entire approach breaks (an unsigned out-of-tree .ko gets rejected at load) and would need MOK enrollment + module signing added to the builder stage.
  • Personal-fork provenance risk: the ultimate origin of the 6.17+ compatibility work is an unmerged personal-fork PR (#30). Vendoring mitigates the "it could disappear" risk but doesn't change that this hasn't gone through upstream review.
  • quine-linux's own rpm-ostree-cleanup.timer prunes the rollback deployment weekly — worth being aware of before testing an unverified change on this hardware, though no action has been taken on this yet.

References