---
title: O fone mudo por 78 dias: um script que segue o jack P2 no PipeWire
url: https://nandomoreira.dev/o-fone-mudo-e-o-jack-p2-no-pipewire
date: 2026-10-09
---

O fone de ouvido do meu notebook ficou sem som de 3 de junho a 20 de agosto de 2026. Foram 78 dias com a entrada P2 de um Dell Latitude 3450 sem saída nenhuma, e o comando que travou tudo era o mesmo que eu tinha guardado num script, em março, para consertar o áudio.

A pergunta que eu fazia em março era como forçar o alto-falante a tocar. A pergunta certa era outra: quem escolhe o perfil da placa de som, e em que momento. A resposta virou o `audio-jack-autoswitch`, um script de 164 linhas que sobe junto com o Niri e troca o perfil quando eu ligo ou tiro o fone.

## Fone e alto-falante não cabem no mesmo perfil

O esperado seria fone e alto-falante serem duas portas da mesma saída de som, e ligar o fone só trocar a porta. Neste notebook, não. O UCM do `sof-hda-dsp` (Realtek ALC3204, Raptor Lake) declara Speaker e Headphones como conflitantes, porque os dois dividem o mesmo DAC do codec. O PipeWire então expõe os dois em perfis separados da placa:

```text
HiFi (HDMI1, HDMI2, HDMI3, Headphones, Headset, Mic1)
HiFi (HDMI1, HDMI2, HDMI3, Headset, Mic1, Speaker)
```

O segundo problema é que os quatro perfis HiFi sempre respondem `available: yes`. A disponibilidade de verdade fica nas portas, não nos perfis. O WirePlumber olha a prioridade, escolhe o perfil com Headphones (10400, contra 10300 do Speaker) e não reavalia nada quando o conector muda de estado. Ou o fone nunca toca, ou o alto-falante fica sem saída nenhuma.

## O conserto de março travava o fone

Em março eu escrevi o `fix-audio-speaker`: troca a placa para o perfil com Speaker, acha o id da saída pelo `wpctl status`, define como padrão e põe o volume em 80%.

O problema estava no efeito colateral: `pactl set-card-profile` grava a escolha em `~/.local/state/wireplumber/default-profile`. **Depois que o perfil é escolhido na mão, o WirePlumber respeita essa escolha e para de trocar sozinho.** Com o perfil do Speaker fixado, a entrada P2 ficava sem saída, sem som e sem microfone. Tanto o script quanto o painel de som do desktop fazem essa gravação, e foi nesse estado que o notebook ficou de 3 de junho a 20 de agosto.

O `fix-audio-speaker` continua no `~/.dotfiles`, mas com um aviso no topo: rodar ele é a decisão deliberada de forçar o alto-falante agora, não um conserto.

## Um vigia no lugar do botão

O script novo escuta o conector em vez de esperar que eu lembre de apertar alguma coisa. Ele lê os eventos da placa pelo `alsactl monitor`, sem polling, e a cada evento compara o estado do jack com o perfil ativo:

```bash
sync_once() {
    local desired current

    jack_is_plugged
    case $? in
        0) desired="$PROFILE_PLUGGED" ;;
        1) desired="$PROFILE_UNPLUGGED" ;;
        *) return 0 ;;   # jack ilegível: registra e não mexe em nada (trecho resumido)
    esac

    current="$(active_profile)"
    [ -z "$current" ] && return 0            # placa ainda não chegou no PipeWire
    [ "$current" = "$desired" ] && return 0  # idempotente

    pactl set-card-profile "$PW_CARD" "$desired"
}
```

Um único plug dispara vários eventos, inclusive os que a própria troca de perfil gera, e o `set-card-profile` só roda quando o perfil ativo diverge do desejado. Sem isso, cada troca provocaria outra.

O monitor roda num `coproc`, não num pipe, para o `read` poder ter timeout. Esse timeout é um heartbeat de 30 segundos. Ele existe porque um restart do WirePlumber (ou o próprio boot) escolhe o perfil de novo pela prioridade, sem evento nenhum do jack, e o alto-falante ficaria mudo até o próximo plug. O `coproc` também deixa perceber quando o monitor morre: o script espera 5 segundos e sobe outro.

No Niri, ele entra como qualquer outro serviço da sessão:

```kdl
spawn-at-startup "sh" "-c" "~/.dotfiles/bin/utils/audio-jack-autoswitch"
```

## Os detalhes que fazem o script mentir se você errar

A placa é chamada pelo nome, `sofhdadsp`, nunca pelo índice. Basta ligar um adaptador de áudio USB para ele pegar o índice 0 e empurrar a placa interna para o 1.

O controle do jack é consultado com `iface=CARD`. O `amixer cget` usa `iface=MIXER` por padrão, onde os controles de jack não existem, e responde `Cannot find the given element`. É fácil engolir essa resposta como "nada conectado". Por isso a função que lê o jack devolve 0 para conectado, 1 para desconectado e 2 para "não sei", e o vigia registra o "não sei" uma vez por transição em vez de agir em cima dele. Na mesma função, o teste procura `values=on`, porque a linha de tipo do `amixer` também carrega um `values=1`.

E só pode haver um vigia rodando. Um segundo, iniciado na mão por cima do da sessão, brigaria com o primeiro pelo perfil. A trava é um `flock` num arquivo em `$XDG_RUNTIME_DIR`, e não um `pgrep -f`, que dentro de um script encontra a si mesmo e diz que já tem outro rodando.

Os oito testes em `bats` cobrem exatamente esses pontos, com `amixer` e `pactl` falsos no `PATH`: plugar troca para Headphones, tirar volta para Speaker, perfil já certo não gera troca, a placa é chamada pelo nome, placa ausente nunca troca o perfil, a consulta usa `iface=CARD` e o jack ilegível é registrado em vez de engolido.

## O preço que eu aceitei

Qualquer escolha manual de perfil no painel de som é desfeita no próximo evento ou no próximo heartbeat, em até 30 segundos. Foi de propósito: a escolha manual é justamente o que criou a fixação e matou o outro lado. Para forçar o alto-falante por um momento, o caminho é o `fix-audio-speaker`, sabendo que o vigia desfaz isso logo depois.

O script também é deste notebook. Os nomes da placa no ALSA e no PipeWire e o do controle do jack vêm com o valor do Latitude 3450 e podem ser trocados por variável de ambiente (`AUDIO_JACK_ALSA_CARD`, `AUDIO_JACK_PW_CARD`, `AUDIO_JACK_CONTROL`). Os dois perfis estão escritos no código, porque saem do UCM desta placa. Em outra máquina, o primeiro passo é olhar o `pactl list cards` dela.

O perfil da placa agora segue o estado do conector, e escolher o perfil na mão virou exceção que dura até o próximo evento.

`kill "$MONITOR_PID"`