audio: ALSA encendido en el kernel — y el muro real es libudev-zero, no el stack de audio

ALSA =y en linux-metal (b3:613bca15): HDA legacy + codecs (lo que emula QEMU y usan las
máquinas pre-SOF), SOF para TigerLake (el laptop del target NO suena por la ruta legacy:
Intel movió el audio al DSP) y SND_USB_AUDIO. Todo =y porque el producto es monolítico.
⚠ Falta inyectar el FIRMWARE de SOF en la imagen (intel/sof/*.ri, sof-tplg/*.tplg), igual
que metal-firmware.sh hace con los tgl_* de i915: sin blobs el driver carga y falla.

FUNCIONÓ LA MITAD: en la VM `/dev/snd` ya trae controlC0 + pcmC0D0p/c y /sys/class/sound
lista card0. El kernel detecta la tarjeta. Pero wpctl sigue con `Devices:` vacío.

**Y LA CAUSA ES ESTRUCTURAL, no una propiedad que falte.** `get_card_nr` de SPA
(alsa-udev.c:178-183) sólo acepta el device CARD —`/sys/.../sound/card0`—, que es un
device de CLASE: sin major:minor, sin nodo en /dev. Y `udev_enumerate_scan_devices` de
libudev-zero recorre EXCLUSIVAMENTE /sys/dev/block y /sys/dev/char (udev_enumerate.c:281),
o sea sólo devices CON nodo; el udev real enumera /sys/class entero. **El device que SPA
necesita nunca entra en la enumeración**: no hay guarda que sacar, falta la mitad del
espacio de búsqueda. Salidas: (a) enseñarle a libudev-zero a recorrer /sys/class —radio 51
sellados— o (b) empaquetar eudev. Es decisión de arquitectura, no parche.

CORRIJO ALGO QUE ESCRIBÍ ANTES: el parche `pipewire-sound-initialized.patch` se hizo
creyendo que SOUND_INITIALIZED era el bloqueo, y NO lo era — con el parche aplicado el
resultado no cambió. Se conserva porque la incompatibilidad que describe es real (sin
udevd nadie pone esa propiedad y SPA descartaría toda tarjeta en cuanto la enumeración
funcione), pero su prosa ahora empieza avisando que no es la solución. Sacarlo del camino
importa: dejarlo presentado como el arreglo mandaría al próximo a buscar en el lugar
equivocado.

Dos arreglos de andamiaje que costaron un ciclo cada uno:
- **`-nic none` por defecto.** Agregar la tarjeta de sonido CORRIÓ LAS RUTAS PCI, la entrada
  de disco cacheada en las VARS de OVMF dejó de coincidir, y la firmware se fue al orden por
  defecto: PXE v4, PXE v6, HTTP… minutos de timeouts. Con un TIMEOUT corto eso se lee como
  «la VM no arrancó» y el serial sólo dice `PXE-E16: No valid offer received`.
- El filtro del log de wireplumber era `alsa|udev|sound|card|monitor` con `tail -40`, y se
  llenó de bluez/v4l2/libcamera —tres monitores que avisan que su plugin no está— dejando
  fuera justo las líneas de ALSA. **Un filtro ancho con cola corta es peor que ninguno.**
  Ahora filtra `alsa` y además copia el log entero a la raíz ext4 (tmpfs muere con la VM).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 21:59:18 -04:00
co-authored by Claude Opus 5
parent c8bb73bae6
commit 8c3bd323d5
5 changed files with 157 additions and 2 deletions
@@ -0,0 +1,83 @@
⚠ ESTE PARCHE NO ERA EL BLOQUEO — leer antes de sacar conclusiones.
Se escribió creyendo que `SOUND_INITIALIZED` era lo que impedía enumerar, y **no lo era**: con el
parche aplicado, `wpctl status` seguía dando `Devices:` vacío. La causa real está más abajo en el mismo
fichero y es ESTRUCTURAL, no una propiedad que falte (ver la nota al final).
El parche se conserva porque la incompatibilidad que describe es REAL y volvería a morder en cuanto la
enumeración funcione: sin él, SPA descarta toda tarjeta en un sistema sin udevd. Pero no es la
solución del problema de audio.
pipewire/SPA: no exigir `SOUND_INITIALIZED`, una propiedad que sólo pone udevd con sus reglas.
EL SÍNTOMA, y por qué es caro de leer: con ALSA encendido en el kernel, la tarjeta detectada y
`/dev/snd/controlC0` presente, `wpctl status` seguía dando
Audio
├─ Devices: ← vacío
├─ Sinks: * 33. Dummy Output
o sea el MISMO cuadro que cuando no había kernel con sonido. Tres cosas distintas —no hay ALSA, no hay
tarjeta, hay tarjeta y el stack no la ve— se ven idénticas en esa salida.
LA CAUSA, en la fuente: `spa/plugins/alsa/alsa-udev.c:167`
if (udev_device_get_property_value(udev_device, "SOUND_INITIALIZED") == NULL)
return SPA_ID_INVALID; /* descarta la tarjeta */
`SOUND_INITIALIZED` no es una propiedad del kernel: **la pone udevd al ejecutar sus reglas**
(`78-sound-card.rules` de systemd/eudev marca la tarjeta cuando todos sus devices están listos). Esta
distro usa **libudev-zero**, que lee /sys directo y NO EJECUTA REGLAS por diseño — su
`udev_enumerate_scan_devices` recorre `/sys/dev/block` y `/sys/dev/char`, así que la tarjeta SÍ se
enumera, pero llega sin esa propiedad. Resultado: SPA descarta todas las tarjetas, siempre.
POR QUÉ SE PARCHEA PIPEWIRE Y NO libudev-zero: sintetizar `SOUND_INITIALIZED` en libudev-zero sería
igual de correcto, pero `yupana radio libudev-zero` da **51 artefactos sellados cayendo a deuda** —
está en el cierre de las cinco imágenes. El radio de pipewire son 2. Se paga el barato.
POR QUÉ QUITAR EL GUARDA ES LEGÍTIMO Y NO UN ATAJO: ese `if` existe para no correr una carrera contra
udevd —evitar tomar una tarjeta cuyos devices todavía no aparecieron—. **Con libudev-zero no hay
udevd, así que no hay carrera que perder**: cuando `/sys/dev/char` lista el device, el device existe.
Se elimina una comprobación cuya premisa no se cumple acá, no una que nos moleste.
Lo que NO se toca: los otros tres guardas del mismo bloque (`ACP_IGNORE`, `SOUND_CLASS=modem`,
`DEVPATH`). Los dos primeros son opt-out explícitos del administrador y el tercero es un dato real del
kernel — ésos siguen valiendo.
diff -Naur a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c
--- a/spa/plugins/alsa/alsa-udev.c
+++ b/spa/plugins/alsa/alsa-udev.c
@@ -164,8 +164,13 @@
if ((str = udev_device_get_property_value(udev_device, "SOUND_CLASS")) && spa_streq(str, "modem"))
return SPA_ID_INVALID;
- if (udev_device_get_property_value(udev_device, "SOUND_INITIALIZED") == NULL)
- return SPA_ID_INVALID;
+ /* hammer: NO exigir SOUND_INITIALIZED. Esa propiedad la pone udevd al correr
+ * 78-sound-card.rules, y esta distro usa libudev-zero, que lee /sys directo y no
+ * ejecuta reglas. El guarda existe para no competir con udevd por una tarjeta a
+ * medio inicializar; sin udevd no hay carrera: si /sys/dev/char lista el device,
+ * el device esta. Ver recipes/incoming-gnome/pipewire-sound-initialized.patch.
+ * Los otros guardas de este bloque (ACP_IGNORE, SOUND_CLASS=modem, DEVPATH) se
+ * respetan tal cual: son opt-out del administrador o datos reales del kernel. */
if ((str = udev_device_get_property_value(udev_device, "DEVPATH")) == NULL)
return SPA_ID_INVALID;
══ LO QUE SÍ BLOQUEA, medido después ═════════════════════════════════════════════════════════════
`get_card_nr` sigue con dos comprobaciones más (alsa-udev.c:178-183):
if ((e = strrchr(str, '/')) == NULL) return SPA_ID_INVALID;
if (strlen(e) <= 5 || strncmp(e, "/card", 5) != 0) return SPA_ID_INVALID;
o sea que **sólo acepta el device CARD** (`/sys/.../sound/card0`), no `controlC0` ni `pcmC0D0p`. Y
`card0` es un device de CLASE: no tiene major:minor, no tiene nodo en /dev.
`udev_enumerate_scan_devices` de libudev-zero recorre **exclusivamente** `/sys/dev/block` y
`/sys/dev/char` (udev_enumerate.c:281) — o sea sólo devices CON nodo. El udev real enumera
`/sys/class/*` entero, con nodo o sin él.
⇒ **El device que SPA necesita nunca entra en la enumeración.** No hay guarda que sacar: falta la
mitad del espacio de búsqueda. Las salidas son (a) enseñarle a libudev-zero a recorrer `/sys/class`
—`yupana radio libudev-zero` = 51 sellados a deuda— o (b) empaquetar eudev. Es una decisión de
arquitectura, no un parche, y queda para quien la tome.
+8
View File
@@ -8,6 +8,13 @@
# habla el protocolo de PulseAudio. O sea que gnome-shell sigue usando libpulse sin saber que del otro
# lado hay PipeWire. Elegir PipeWire NO obligó a tocar una línea del cliente.
#
# ⚠ LLEVA UN PARCHE: `pipewire-sound-initialized.patch`. SPA descartaba TODAS las tarjetas de sonido
# porque exige la propiedad `SOUND_INITIALIZED`, que **no la pone el kernel sino udevd al correr sus
# reglas** — y acá el udev es libudev-zero, que lee /sys directo y no ejecuta reglas por diseño. El
# síntoma era un `wpctl status` con `Devices:` vacío, IDÉNTICO al de no tener ALSA en el kernel. Ver el
# parche: dice por qué se toca pipewire (radio 2) y no libudev-zero (radio 51), y por qué quitar ese
# guarda es legítimo (protege de una carrera contra udevd que sin udevd no existe).
#
# ⚠ SIN GESTOR DE SESIÓN todavía (`-Dsession-managers=[]`). PipeWire arranca y acepta clientes —lo que
# quita el `Gvc-WARNING: Failed to connect context: Connection refused` del shell— pero sin
# wireplumber no enumera ni enruta dispositivos: no hay política de qué entra y qué sale. Para una VM
@@ -43,6 +50,7 @@ version = "1.2.7"
[source]
tarball = "https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/1.2.7/pipewire-1.2.7.tar.gz"
sha256 = "e75568ed18bcbe75e9779af57cb9cc256fd7ebfaadc12bb347a0717055d1d3a9"
patches = ["pipewire-sound-initialized.patch"]
[build]
compiler = "zig-cc"
+30 -1
View File
@@ -9,6 +9,28 @@
# +AUDIT (la jaula del anfitrión kikin J4b / harkaq), IO_URING (kikin J4 se diseña sobre él) y
# BPF_SYSCALL (horizonte observabilidad) van EXPLÍCITOS: «al azar del defconfig» no es un contrato.
#
# AUDIO: ALSA ENCENDIDO (2026-07-29, decisión del usuario tras elegir PipeWire como servidor).
# Estaba `-d SOUND -d SND` — apagado a propósito cuando el escritorio no existía todavía. El precio
# se cobró al cerrar wireplumber: `wpctl status` daba `Devices:` VACÍO con un único sink `auto_null`,
# y **no era un fallo del stack de audio sino la ausencia de /dev/snd**. Se comprobó dándole a QEMU
# una `ich9-intel-hda` emulada: con tarjeta y sin ALSA en el kernel, el resultado no cambiaba.
#
# Todo =y y no =m, por la razón de siempre en esta receta: el producto es monolítico, sin
# udev/modprobe en el camino. Tres frentes, y ninguno es opcional para el target real:
# · HDA legacy (SND_HDA_INTEL + codecs Realtek/HDMI/generic) — es lo que emula QEMU
# (`ich9-intel-hda`) y lo que usan las máquinas pre-SOF. Con esto solo, la VM ya tiene qué
# enumerar.
# · SOF para TigerLake (SND_SOC_SOF_TIGERLAKE + HDA_AUDIO_CODEC/HDA_LINK) — el laptop del target
# concreto NO suena por la ruta legacy: Intel movió el audio de TGL al DSP, y sin el firmware SOF
# cargado no hay salida. Es el mismo patrón que i915 con su DMC/GuC.
# · SND_USB_AUDIO — auriculares y micrófonos USB, que es como la mayoría de la gente escucha.
# `SND_DYNAMIC_MINORS` porque con varias tarjetas (HDA + HDMI + USB) los minors fijos se agotan.
#
# ⚠ FALTA EL FIRMWARE DE SOF en la imagen. El kernel trae el driver; los blobs
# (`intel/sof/sof-tgl.ri`, `intel/sof-tplg/*.tplg`) los tiene que inyectar `metal-firmware.sh`, igual
# que hace con los `tgl_*` de i915. Sin ellos SOF carga y falla, y en el laptop no habrá sonido
# aunque el driver esté. En QEMU no importa: ahí manda la ruta HDA legacy.
#
# PERFIL ESCRITORIO/JUEGO (plan-jaula-juegos T1 + joyas-reusables #1/#4, 2026-07-16):
# - NTSYNC: primitivas de sincronización NT en kernel (wine/proton moderno, reemplaza esync/fsync).
# Sin deps de Kconfig (verificado drivers/misc/Kconfig del tag); tristate ⇒ =y con MODULES=off.
@@ -108,7 +130,14 @@ scripts/config -d MODULE_SIG -d MODULE_SIG_ALL -d DEBUG_INFO_BTF -d DEBUG_INFO -
-e NET_VENDOR_REALTEK -e R8169 -e NET_VENDOR_BROADCOM -e TG3 \
-e USB_NET_DRIVERS -e USB_USBNET -e USB_NET_CDCETHER -e USB_NET_CDC_NCM -e USB_NET_RNDIS_HOST \
-d DRM_AMDGPU -d DRM_NOUVEAU -d DRM_RADEON -d AGP \
-d SOUND -d SND -d MEDIA_SUPPORT -d INFINIBAND -d BT -d NFC -d CAN \
-e SOUND -e SND -e SND_PCM -e SND_TIMER -e SND_HRTIMER -e SND_DYNAMIC_MINORS \
-e SND_PCI -e SND_HDA -e SND_HDA_INTEL -e SND_HDA_GENERIC -e SND_HDA_PATCH_LOADER \
-e SND_HDA_CODEC_REALTEK -e SND_HDA_CODEC_HDMI -e SND_HDA_CODEC_GENERIC \
-e SND_INTEL_DSP_CONFIG -e SND_SOC -e SND_SOC_INTEL_SOF_PCI_DEV \
-e SND_SOC_SOF_TOPLEVEL -e SND_SOC_SOF_PCI -e SND_SOC_SOF_INTEL_TOPLEVEL \
-e SND_SOC_SOF_TIGERLAKE -e SND_SOC_SOF_HDA_AUDIO_CODEC -e SND_SOC_SOF_HDA_LINK \
-e SND_USB -e SND_USB_AUDIO \
-d MEDIA_SUPPORT -d INFINIBAND -d BT -d NFC -d CAN \
-d WATCHDOG -d THUNDERBOLT -d FIREWIRE -d DEBUG_WX \
-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS -d GFS2_FS -d NTFS3_FS && \
scripts/config --set-str CMDLINE "console=tty0 console=ttyS0,115200 quiet loglevel=3 vt.global_cursor_default=0 efi=novamap split_lock_detect=off initrd=/initramfs.cpio.gz rdinit=/init" && \
+25 -1
View File
@@ -304,6 +304,13 @@ esperar_nombre() {
# resultado fue DOS wireplumber vivos peleándose el grafo — visible en `wpctl status` como dos clientes
# «WirePlumber» con pids distintos. El síntoma no es un error: es una lista de clientes que se lee
# normal si no se cuenta.
# ANTES de arrancar nada: ¿hay hardware que enumerar? Es la pregunta que separa «el stack de audio
# falla» de «no hay tarjeta», y las dos se ven igual en un `wpctl status` con `Devices:` vacío. El
# cmdline del kernel lleva `quiet loglevel=3`, así que los mensajes de ALSA NO salen por el serial —
# mirar /dev y /sys es la única forma directa.
say "audio: /dev/snd = $(ls /dev/snd 2>/dev/null | tr "\n" " ")"
say "audio: /sys/class/sound = $(ls /sys/class/sound 2>/dev/null | tr '\n' ' ')"
say "audio: HDA en el bus PCI = $(grep -ci 'audio' /proc/bus/pci/devices 2>/dev/null || echo '?')"
if [ -x /usr/bin/pipewire ]; then
/usr/bin/pipewire >/tmp/pipewire.log 2>&1 &
say "pipewire lanzado (pid $!)"
@@ -323,7 +330,10 @@ if [ -x /usr/bin/pipewire ]; then
# aplica la política (qué es entrada, qué es salida, qué se enlaza con qué), y esa política está
# escrita en Lua. Sin él el único sink es `auto_null`, el nodo de descarte.
if [ -x /usr/bin/wireplumber ]; then
/usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 &
# WIREPLUMBER_DEBUG=3: sin esto su log no dice POR QUÉ descarta una tarjeta, y «Devices: vacío»
# es indistinguible de «no hay hardware». Con el nivel de debug puesto, el monitor de ALSA
# imprime cada device que ve y el motivo del descarte.
WIREPLUMBER_DEBUG=3 /usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 &
WP_PID=$!
say "wireplumber lanzado (pid $WP_PID)"
sleep 2
@@ -333,6 +343,20 @@ if [ -x /usr/bin/pipewire ]; then
# creó: si sale vacío, wireplumber corre pero no enumeró nada.
wpctl status >/tmp/wpctl.log 2>&1 || true
dump /tmp/wpctl.log
# Y su propio log, SIEMPRE — no sólo si muere. Un wireplumber vivo que no enumera nada es el
# caso interesante, y es justo el que quedaba sin evidencia. Se filtran las líneas de alsa/udev,
# que son las que dicen qué vio y qué descartó.
# Filtro ESTRECHO: `alsa` y nada más. La primera versión filtraba
# `alsa|udev|sound|card|monitor` y el `tail -40` se llenó de bluez/v4l2/libcamera —tres
# monitores que avisan que su plugin no está, ruido garantizado— dejando fuera justo las líneas
# de ALSA. Un filtro ancho con cola corta es peor que ninguno.
say "wireplumber :: ALSA"
grep -iE "alsa" /tmp/wireplumber.log 2>/dev/null | head -40 > /tmp/wp-alsa.log
dump /tmp/wp-alsa.log
# Y el log COMPLETO a la raíz ext4, que sí se sincroniza: /tmp es tmpfs y muere con la VM.
# Se saca con `debugfs -R "dump /wireplumber.log ..."`, igual que el core.
cp /tmp/wireplumber.log /wireplumber.log 2>/dev/null || true
sync
else
say "!! wireplumber MURIÓ:"; dump /tmp/wireplumber.log
fi
+11
View File
@@ -60,6 +60,17 @@ if [ "${AUDIO:-1}" = 1 ]; then
QEMU_ARGS+=(-audiodev none,id=snd0 -device ich9-intel-hda -device hda-duplex,audiodev=snd0)
fi
# NET=none (default): SIN tarjeta de red. No es sólo ahorro — QEMU añade una e1000 por defecto, y OVMF
# la usa para crear entradas de arranque PXE/HTTP (IPv4 e IPv6). Cuando la entrada de disco que OVMF
# tenía cacheada en sus VARS deja de coincidir —y basta AGREGAR UN DISPOSITIVO PCI, como la tarjeta de
# sonido de arriba, para que las rutas se corran— la firmware cae al orden por defecto y se pasa un
# par de minutos en timeouts de PXE antes de llegar al disco. Con un TIMEOUT corto eso se ve como
# «la VM no arrancó», y el serial sólo muestra `PXE-E16: No valid offer received`. Pasó de verdad.
# Este escritorio no necesita red; `NET=user` si algún día hace falta.
if [ "${NET:-none}" = none ]; then
QEMU_ARGS+=(-nic none)
fi
echo "==> QEMU (DISP=$DISP, serial=$SERIAL)"
echo " imagen: $IMG"
if [ -n "${TIMEOUT:-}" ]; then