pttman: System-wide push-to-talk for Linux
Contents
The Problem
Most push-to-talk implementations live inside individual apps. Discord has one, Teams has one, Zoom has a setting buried somewhere in its preferences. But if you want push-to-talk behavior everywhere on Linux, across every app that touches your microphone, you’re out of luck. There’s no system-level toggle that mutes and unmutes your mic on a key press/release in a way that actually works reliably.
The naive approach is a shell script that calls pactl set-source-mute on key
down and key up. That works until you press and release the key quickly, or hold
it and release while another press event is still in flight. PipeWire processes
those pactl calls concurrently, and the mute commands interleave: you end up
with a mic that’s stuck open or stuck muted, depending on which process finished
last.
What pttman Does
pttman↗ is a small Python daemon that provides push-to-talk microphone control for PipeWire. It runs as a user service, listens on a Unix datagram socket, and serializes mute/unmute requests so that rapid key presses never race each other.
Two properties make it useful as a system-wide solution:
- It operates at the PipeWire/PulseAudio layer, so every app that uses your microphone sees the mute state change. No per-app configuration needed.
- By default, it controls all audio sources on the system. If you have a USB headset and a webcam mic, both get muted and unmuted together. You can also lock it to a single source if you prefer.
The client side is a fire-and-forget datagram send. When you press a key, a
lightweight client process sends "unmute" to the socket and exits. On release,
it sends "mute". The daemon drains all pending messages and applies the last
one, so if events pile up during a rapid press/release, the final state is
always correct.
If the daemon isn’t running, client commands fall back to direct pactl
execution. You don’t lose functionality; you just lose the race-condition
protection.
Setup
Install with uv↗ and start the service:
uv tool install pttman
pttman install-service
systemctl --user start pttman.service
install-service detects your init system automatically. It supports systemd
(user service) and OpenRC (user service on 0.60+, system service on older
versions).
By default, pttman controls all audio sources. To restrict it to one:
pttman list-sources
pttman set-default-source alsa_input.usb-046d_BRIO-03.pro-input-0
set-default-source writes to ~/.config/pttman.conf and signals the running
daemon to reload, so the change takes effect immediately.
Key Binding
You need something to send press/release events. I use xremap↗, which supports distinct actions for key press and key release:
modmap:
- name: Push-to-talk
remap:
F9:
skip_key_event: true
press: { launch: ["/home/your-user/.local/bin/pttman", "unmute"] }
release: { launch: ["/home/your-user/.local/bin/pttman", "mute"] }
Pressing F9 unmutes, releasing it mutes. On some laptop keyboards,
F9 has a mic icon, which makes it a natural fit.
You can also route your compositor’s mic-mute key through pttman for a toggle
instead of push-to-talk. For example, in niri’s keybinds.kdl:
XF86AudioMicMute allow-when-locked=true { spawn "/home/your-user/.local/bin/pttman" "toggle"; }
How It Works
The daemon is a single-threaded Python process (no external dependencies beyond
the standard library) that listens on $XDG_RUNTIME_DIR/pttman.sock. When a
message arrives, it drains the socket non-blocking and coalesces: only the last
queued command executes. This means a burst of unmute, mute, unmute, mute
collapses to a single mute call to pactl.
A background thread runs pactl subscribe to watch for audio source changes. If
you plug in a new USB mic while the daemon is running and you’re in all-sources
mode, pttman picks it up automatically and applies the current mute state to it.
Configuration changes are picked up via SIGHUP, which set-default-source
sends automatically.