Pi’s dictation shortcut (Ctrl+Alt+Z, via the pi-transcribe extension) records audio on the machine where pi runs. If pi runs on a headless server, there is no microphone — recording starts, but captures only silence. SSH can’t help: it forwards keystrokes, never audio.

The fix: tunnel your laptop’s microphone to the server as a virtual audio device. Both machines on a Tailscale network make the connection private by default1 .

pi (server) ← ALSA ← PipeWire ←── tunnel over Tailscale ──→ PulseAudio on laptop → laptop mic

Server setup (Ubuntu, one time)

  1. Install the audio stack (PipeWire server, its policy manager, and the ALSA/Pulse compatibility layers):
sudo apt install --no-install-recommends pipewire pipewire-pulse pipewire-alsa wireplumber
  1. Keep user services alive after you log out:
loginctl enable-linger
  1. Enable and start:
systemctl --user enable --now pipewire pipewire-pulse wireplumber
  1. Create the tunnel config at ~/.config/pipewire/pipewire.conf.d/10-tunnel-source.conf (replace the IP with your laptop’s Tailscale IP2 ):
context.modules = [
  {   name = libpipewire-module-pulse-tunnel
      flags = [ nofail ]
      args = {
          tunnel.mode = source
          pulse.server.address = <LAPTOP_TAILSCALE_IP>:4713
          stream.props = {
              node.name = "remote_mic"
              media.class = "Audio/Source"
          }
      }
  }
]

Laptop setup

macOS

brew install pulseaudio

pulseaudio --load="module-native-protocol-tcp auth-ip-acl=100.64.0.0/10 listen=<LAPTOP_TAILSCALE_IP> port=4713" \
           --exit-idle-time=-1 --daemon
  • auth-ip-acl=100.64.0.0/10 restricts access to Tailscale-range addresses only3 .
  • Approve the macOS microphone permission prompt when it appears — skipping it gives you perfect silence, the hardest failure to spot.

Linux

Even simpler — your desktop already runs PipeWire/PulseAudio, so just load the module:

pactl load-module module-native-protocol-tcp auth-ip-acl=100.64.0.0/10 listen=<LAPTOP_TAILSCALE_IP> port=4713

(If the server can’t connect, check the laptop firewall: sudo ufw allow in on tailscale0 to any port 4713.)

Connect and verify

The tunnel connects when the server’s PipeWire starts, so restart it once the laptop is serving. Skip this restart if the tunnel is already up and working — it tears the tunnel down and back up, and kills any dictation in flight:

systemctl --user restart pipewire

wpctl status | grep -A3 Sources
#  ├─ Sources:
#  │  *   36. (null)   [vol: 1.00]     ← working

The (null) name is normal for a tunnel source. The * means it’s the default — which is exactly what pi-transcribe’s system-default microphone uses. No pi configuration needed.

Quick end-to-end test — record 5 seconds and check it’s not silence4 :

pw-record --target remote_mic --rate 16000 /tmp/test.wav & PID=$!; sleep 5; kill $PID
python3 -c "
import wave, struct
w = wave.open('/tmp/test.wav'); raw = w.readframes(w.getnframes())
f = struct.unpack(f'<{len(raw)//2}h', raw[:len(raw)//2*2])
print('SILENT — check mic permission' if max(f) < 500 else 'real audio ✓')
"

Then press Ctrl+Alt+Z in pi, speak, press it again.

Troubleshooting

SymptomCauseFix
PvRecorder failed to initializeTunnel source missingLaptop serving? Then systemctl --user restart pipewire
Records but transcript is emptymacOS mic permission deniedSystem Settings → Privacy & Security → Microphone
Capture goes to the wrong deviceLaptop’s PulseAudio default is not what you expectpactl -s tcp:<LAPTOP_TAILSCALE_IP>:4713 list sources short, then set-default-source <n>
Services die after logoutLinger not enabledloginctl enable-linger