通过 SSH 使用 Claude Code 语音模式
当服务器没有麦克风的时候
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
字号
行距
字体
缺失的麦克风
Claude Code 的语音模式需要一个麦克风。在通过 SSH 接入的远程 Linux 服务器上,这样的硬件并不存在。在那里打开语音模式,音频栈抛出:
cannot find card '0'麦克风并没有坏。服务器出厂通常不带音频硬件。解决办法:通过 SSH 把本地麦克风转发过去,让远程进程像使用自己的设备一样使用它。
为什么会失败
这条错误采用的是 ALSA 的格式:card '0' 是这一层期望找到的内核级音频设备。不管语音模式走哪条路径去读麦克风,最终都会落到 ALSA。ALSA 扫描物理设备,找不到任何一个,然后返回上面那条错误。
通常夹在应用和硬件之间的用户态守护进程,PulseAudio 和 PipeWire,在一台典型的服务器安装里同样不在运行。哪怕它们真的在跑,也没有什么可以管理的。
问题并不是配置错了。这台机器上根本就没有音频这一层。
为什么它仍然有解
让它看起来无解的,是这样一个假设:语音采集必须发生在本地硬件上。在 Linux 上这不成立:守护进程是一个用户态服务,硬件只是藏在它后面。PipeWire 和 PulseAudio 都通过网络套接字对外提供同一套线协议,而不仅仅是本地 Unix 套接字。只要配置得当,一台机器上的应用可以从另一台机器的麦克风录音。
整套思路就是这样:在本地守护进程上打开 TCP,把端口通过 SSH 转发过去,让远程进程指向它。
还有哪些选项
在 Linux 上,把音频跑在网络上有几种做法。这条路线的邻居:
- PipeWire 的 RTP 模块(
module-rtp-source/module-rtp-sink)。通过 UDP 传输原始 PCM、Opus 或 MIDI。更常用于 LAN 多播(默认组224.0.0.56),而不是点对点,延迟潜力比 TCP 更低,没有内建加密,两端都需要匹配的地址与格式配置。 - PulseAudio 的
module-tunnel-source。 远程端加载这个模块,指向本地机器的 PulseAudio 服务器(或 PipeWire 的 Pulse 兼容层)。一样的原生协议,不走 SSH 隧道。代价也在这里:没有加密,认证 cookie 以明文走过网络。 - X11 转发。 在关于 SSH 音频的讨论里经常有人提议,但
ssh -X并不承载音频。那些把音频绑到 X 上的方案,依然需要图形会话加上一条独立的音频传输通道:PulseAudio 的module-x11-publish(通过 X11 属性公告 PA 服务器地址),或 NX/x2go(在 X 会话之外另加一条音频通道)。在没有图形输出的服务器上,这些都用不上。 - 干脆在本地跑 Claude Code。 永远是一个选项。代价是放弃远程机器的资源、随处可达的能力,以及一份自己掌控的基础设施。
这里选的 TCP over SSH 路径在两端都不引入新东西(PipeWire 已经在笔记本上,OpenSSH 两端都有),并且因为走 SSH 隧道而天然加密。
整套搭法
三个活动部件:
- 本地音频守护进程在一个 TCP 端口上监听,外加它平常的 Unix 套接字。
- SSH 通过一条反向隧道把这个端口带到远程机器。
- 远程端的音频栈(既包括懂 PulseAudio 的应用,也包括只用 ALSA 的那些)都被指向这条隧道。
步骤 1:让本地音频服务器接受 TCP
在本地机器上,把 PipeWire 的默认 PulseAudio 配置复制到用户目录:
cp /usr/share/pipewire/pipewire-pulse.conf ~/.config/pipewire/pipewire-pulse.conf打开 ~/.config/pipewire/pipewire-pulse.conf,找到 server.address 块,把 TCP 地址加进去:
server.address = [ "unix:native" "tcp:127.0.0.1:4713" # 仅 localhost,用于 SSH 音频转发]重启 PipeWire 让改动生效:
systemctl --user restart pipewire pipewire-pulsePipeWire 现在在 TCP 端口 4713 上监听,但只限 localhost。从网络上无法到达它。因为改动写在配置文件里,所以重启之后依然有效。
步骤 2:通过 SSH 加反向隧道登入
不用简单的 ssh user@server,而是加上 -R 参数:
ssh -R 4713:127.0.0.1:4713 user@your-server-R 参数告诉 SSH:任何连到远程机器 4713 端口的东西,都应当被转发到本地机器的 4713 端口。音频流量走在加密的 SSH 连接里。
步骤 3:把远程端指向音频服务器
在远程端登入之后,设置这个环境变量:
export PULSE_SERVER="tcp:127.0.0.1:4713"任何使用 PulseAudio(或 PipeWire 的 Pulse 兼容层)的应用,现在都会连到这个端口,而这个端口直通回本地麦克风。
步骤 4:让 ALSA 走 PulseAudio
有些应用直接去找 ALSA,而不是走 PulseAudio 的环境变量。从最初那条错误信息的格式看,Claude Code 的语音模式就是其中之一(cannot find card '0' 是一条 ALSA 消息)。对这类应用,ALSA 需要它自己的一条路由规则。在远程机器上创建 ~/.asoundrc:
pcm.!default { type pulse }ctl.!default { type pulse }这是在告诉 ALSA:有应用要默认的音频设备时,就把它走 PulseAudio,而不是去找硬件。安装所需的软件包:
sudo apt install libasound2-plugins pulseaudio-utils alsa-utilsalsa-utils 提供 arecord 以及语音采集路径需要的录音端 ALSA 二进制。没有这个包,音频路径可以完全跑得通(隧道已起,pactl info 里能看到麦克风),语音模式仍然会失败,因为没有一个录音二进制来从源头读取数据。那种悄悄藏起来的缺口:栈里每一层单独测试都没问题,但真正从麦克风读字节的那一块缺席了。在这套环境里,正是安装这个软件包把语音模式带回了生活;碰到同样死胡同的人,alsa-utils 会因沙安拉填上这个缺口。那个闭源语音客户端大概调用的是 arecord 或它的某个兄弟,但严格来讲只能说:alsa-utils 就是当时缺的那块拼图。
步骤 5:测试
还在远程端,执行:
pactl info如果一切正常,输出里显示的是本地机器的音频服务器信息,包括作为默认源列出的本地麦克风。这就是确认。
全貌
从头到尾,音频路径是这样:
%%{init: {"flowchart": {"useMaxWidth": false}} }%%
graph TD
A["Claude Code (远程)"] --> B["tcp:127.0.0.1:4713 在远程端"]
B --> C["SSH 反向隧道"]
C --> D["本地 PipeWire 在端口 4713"]
D --> E["物理麦克风"] 声音从硬件 → PipeWire → TCP 套接字 → SSH 隧道 → 远程服务器 → Claude Code 一路走过。远程机器从头到尾都不需要自己的音频硬件。
代价
它的代价:
- 延迟。 麦克风流经过 PipeWire、一个 TCP 套接字、一次 SSH 加密处理,再原路返回。在家庭网络连接到附近的服务器时,对听写来说并不明显;在慢速链路或远距离服务器上,就会明显起来。
- 带宽。 流是未压缩的 PCM。语音模式以短促的爆发为主,所以实际数据量不大,但共享热点或有流量上限的连接还是会感觉到。
- 信任范围。 只要 SSH 会话开着,远程进程就能拿到实时的麦克风。在同一个用户下跑在远程机器上的任何其他东西,在隧道存在期间也能从同一个源读取。对任何被转发的设备这都是常态,但值得留意。
- 单机假设。 这套方案在远程端绑定到 localhost,这也正是它安全的原因。与此同时也意味着,只有这一台远程机器上的进程能用这个麦克风,通过同一跳到达的第二台机器用不了。
让它持续生效
上面的搭法只对当前一次会话有效。要让它长期生效:
在远程端。 PULSE_SERVER 应该写进 ~/.zshrc(或 ~/.bashrc):
echo "export PULSE_SERVER='tcp:127.0.0.1:4713'" >> ~/.zshrc对于 SSH。 把 RemoteForward 加到本地机器的 ~/.ssh/config 里,这样就不用每次都写 -R 参数:
Host your-server HostName your-server-ip User your-user RemoteForward 4713 127.0.0.1:4713一条普通的 ssh your-server 现在就会自动把隧道搭起来。
对于本地 TCP 模块。 步骤 1 已经处理过了。4713 端口在每次登录时都会打开。
副作用:什么时候会冒出 RemoteForward 警告
一旦 RemoteForward 4713 127.0.0.1:4713 住进了 ~/.ssh/config,到这台主机的每一次 SSH 连接都会尝试建起音频转发。有两种场景下这件事做不成,并且两种场景产生同一条警告:
Warning: remote port forwarding failed for listen port 4713连接本身仍然会通过(部署能完成,shell 也会正常打开),但这条警告看起来很吓人。两种不同的场景共享同一个症状。
场景 1:CI 脚本继承了这条指令
Woodpecker CI 跑在本地机器上。一个轻量、自托管的 CI 系统,它通过 webhook 接收 git push,然后直接在宿主机上跑构建步骤(没有容器)。部署步骤用 ssh 和 rsync 把构建好的站点推到远程服务器。这两条命令连接的都是同一个主机别名,而这个别名的 SSH 配置里现在带着 RemoteForward 4713。
每次部署都开始打印那条警告。部署本身一切正常:rsync 跑完、软链接换好、站点上线。但在 CI 日志里,这种级别的警告属于那种哪怕一切如常你也会想停下来看看的类型。
CI 进程跑在一个没有本地用户音频运行时的环境里:用户不同,没有交互式 shell,而本地的 PipeWire 套接字从 CI 代理运行的位置过不去。转发无处着陆,SSH 就放弃,并打印那条警告。
解决:让脚本跳出转发
SSH 提供了 ClearAllForwardings,它的作用是让 SSH 忽略配置文件里所有的 LocalForward 和 RemoteForward 指令。用 -o 按命令来设:
ssh -o ClearAllForwardings=yes g12 "mkdir -p /some/path"对一个带多次 SSH 调用的部署脚本,在最上面定义一次:
SSH="ssh -o ClearAllForwardings=yes"export RSYNC_RSH="$SSH"RSYNC_RSH 告诉 rsync 要用哪条 SSH 命令。脚本里每一次 $SSH 调用、每一次 rsync 传输,现在都不会再尝试任何端口转发。警告就此消失。交互式的 ssh g12 会话仍然能拿到音频隧道,因为它们不会使用这个覆盖。
场景 2:向同一台主机开第二个交互会话
另一种情况出现在两个终端(或同一个终端的两个不同时刻)都执行 ssh g12 的时候。第一个会话把 RemoteForward 建起来,并在远程端占住 4713 端口。第二个会话读的是同一份配置,也试图在远程端占住 4713,却发现它已经被占了。
警告出现在第二个会话上。出乎意料的是,语音模式在第二个会话里照样工作:第一个会话的隧道被透明共享,因为被占住的端口其实通回的是同一个本地 PipeWire。音频照样流动。那条警告其实在误导:它让人以为整个转发都失败了,实际上只是这一个具体会话想要也去占用它的尝试失败了。
真正的后果藏在背后:第一个会话一旦关闭,隧道对两边都断了。第二个会话看上去像是还连着,但语音模式要等到有人重新开出第一个会话才会恢复。
解决:SSH 连接复用
OpenSSH 自带连接复用,靠的是 ControlMaster。第一个会话打开一条主控连接;后续到同一台主机的会话共享它,而不是新开一条 TCP 连接。只有一份 RemoteForward,只有一条隧道,后面的会话上不再出警告。
在 ~/.ssh/config 的 Host 块里加上:
Host your-server HostName your-server-ip User your-user RemoteForward 4713 127.0.0.1:4713 ControlMaster auto ControlPath ~/.ssh/cm_%r@%h:%p ControlPersist 10mControlMaster auto 启用共享。ControlPath 决定复用连接的套接字文件放在哪里;%r、%h、%p 会被展开成用户、主机、端口。ControlPersist 10m 让主控连接在最后一个会话关闭之后再活十分钟,这样开新终端时就不用再付一次 SSH 握手的代价。
有了这一段,就可以并行开任意多个 ssh g12 终端:底下共用一条隧道,后续会话上没有警告,语音模式会因沙安拉从其中任意一个都能到达。关掉其中一个终端,也不会连累其他终端跟着断。这是把 OpenSSH 标准的复用模式套用到音频隧道场景上;底层机制本身文档充足,不过在真正依赖它之前,还是值得在自己的环境里先验证一下。
两种场景之间一刀切得干净:脚本跳出转发,交互式会话共享同一条隧道。
总结
| 做什么 | 在哪里 | 命令 |
|---|---|---|
| TCP 模块(持久) | 本地(~/.config/pipewire/pipewire-pulse.conf) | 把 "tcp:127.0.0.1:4713" 加进 server.address |
| SSH 隧道(持久) | 本地(~/.ssh/config) | 在 Host your-server 下写 RemoteForward 4713 127.0.0.1:4713 |
| 多会话复用 | 本地(~/.ssh/config) | ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m |
| 设置音频服务器 | 远程(~/.zshrc) | export PULSE_SERVER='tcp:127.0.0.1:4713' |
| 配置 ALSA | 远程(~/.asoundrc) | pcm.!default { type pulse } |
| 安装软件包 | 远程 | sudo apt install libasound2-plugins pulseaudio-utils alsa-utils |
| CI/脚本覆盖 | 部署脚本 | ssh -o ClearAllForwardings=yes + RSYNC_RSH |
音频搭法五步,复用一块,加一个覆盖给那些根本不该带上转发的脚本。各个部件都已经在盒子里:PipeWire、OpenSSH、ALSA。真正的工作是把它们指到对方身上。
AI 编辑器 1 / 1
返回 AI 编辑器航空航天工程师
公开透明的道德创业者
你专注你的事业
我负责数字化的部分