Mac 平台一直缺少官方的 WO Mic 客户端,市面上也很难找到足够轻量、好用的替代方案。平时如果临时想把手机当成 Mac 的无线麦克风,往往需要复杂的配置或忍受高延迟的网页中转。

既然没有现成的工具,不如自己动手写一个。我用纯 Swift 和 SwiftUI 开发了原生客户端 MacMic,体积小、冷启动快、无须冗余的后台驻留服务。

原本以为这只是个简单的网络接收与音频解码工具——SwiftUI 界面和 Opus 解码器很快就搭建完毕。但随后在虚拟音频路由和闭源私有通信协议上踩到的深坑,远超最初的预期。整个排查与解决过程,经历了几个不同大模型的接力才得以彻底破局。

第一轮:Gemini 3.7 Flash 的驱动弯路与协议假死

最初我使用 Gemini 3.7 Flash 来搭建骨架、实现基础网络通信和音频输入管线。

在音频输入这一步,Gemini 3.7 Flash 首先建议自研一套虚拟音频 HAL(Hardware Abstraction Layer)驱动,并生成了对应的 C/C++ 驱动源码。然而在 macOS Sequoia 与 Apple Silicon 架构下,系统对内核扩展及 DriverKit / CoreAudio 插件的签名与公证有着极其严格的要求。未签名的驱动会被 coreaudiod 直接拒载,甚至可能导致系统音频服务假死。为了避免陷入驱动签名的泥潭,我果断放弃了自研驱动路线,改为调用 macOS 开源社区成熟且拥有官方公证的环回音频驱动 BlackHole 2ch。

解决了音频路由后,真正的硬骨头落在了网络协议上。

WO Mic 官方从未公开过通信协议规范。按照常规网络编程逻辑,Gemini 3.7 Flash 编写了标准的 TCP Socket 通信代码。然而每次连接 iPhone 时,程序都会卡死在握手阶段,或者刚发出数据就收到手机端的 Connection reset by peer。

通过 Wireshark 抓包确认,手机端的 8125 端口处于正常监听状态,但只要客户端发包,连接就会立刻被对端重置。调整重试机制、超时时间与包头格式均无济于事。面对无任何公开文档的非标准私有协议,常规的参数推演容易陷入反复试错的循环。

第二轮:Opus 4.6 逆向 Windows 二进制与发现分段时序

为了突破瓶颈,我提取了官方 Windows 客户端 WOMicClient.exe,转由擅长逆向分析的 Opus 4.6 处理,借助 Python 的 pefile 与 capstone 库反汇编其二进制文件。

通过逆向分析,摸清了 Windows 客户端的基础握手帧格式:

  • 握手包结构:[1 字节 cmd] [4 字节大端 length] [payload 内容]

但直接将完整数据包(例如 11 字节的 0x65 版本校验包)一次性发送至 iPhone 时,iOS 端依然会立即切断连接。Opus 在反复比对汇编逻辑与抓包数据后,捕捉到了一个关键细节:iOS 端的底层 Socket 读取采用分步阻塞机制。其逻辑是先 read(1) 读取 1 字节命令,再 read(4) 读取 4 字节长度,最后根据长度读取对应 Payload。

在 TCP 传输层面,若客户端调用单次 sendall() 将整包直接推入缓冲区,iOS 端的读取时序就会错乱,进而触发 RST 错误。

针对这一特性的解决方案十分明确:必须将单个控制包强制拆分为三次独立发送。

s.sendall(bytes([cmd_byte]))           # 1. 发送 1 字节命令
s.sendall(struct.pack(">I", length)) # 2. 发送 4 字节载荷长度(大端序)
if payload:
s.sendall(payload) # 3. 发送载荷内容

改用分段发送后,握手流程顺利走通:

  • 0x65(CheckVersion)返回版本校验通过。
  • 0x66(SetCodec)返回 0x00 确认。
  • 0x67(StartCapture)返回 0x00 确认。

然而握手虽然通过,控制通道上却迟迟没有连续的音频流传入。此时第一天的调用额度耗尽,Opus 将所有反汇编笔记与端口分析结果整理后交付。

第三轮:Gemini 3.1 Pro 的推论假象与排查误区

我将已有的逆向笔记整理后交由 Gemini 3.1 Pro 接力分析。

Gemini 3.1 Pro 分析后给出了一个推断:iOS 端在处理完 0x67 StartCapture 后会主动关闭 TCP,认为 TCP 仅为一次性信令通道,音频流必定由 UDP 传输;而 UDP 未收到数据,则是由于 macOS 系统防火墙静默拦截了终端 Python 脚本的网络访问。它建议将代码迁移进 Swift 并打包为 .app 应用,以触发系统的本地网络权限弹窗。

按照该思路完成 Swift 移植并打包运行后,应用确实弹出了系统网络权限请求,但实际测试中暴露出更多底层问题:

  • iOS 端建立连接后,准时在第 5 秒发生断联。
  • Android 端始终显示已连接,但 Mac 客户端完全接收不到音频数据。
  • 手机端主动断开后,Mac 端界面依然停留在“已连接”状态。

事实表明,“一次性信令通道”和“防火墙拦截”的猜测并未触及核心问题。

第四轮:Opus 4.6 反编译 APK 源码彻底破局

推测无法替代事实。为了彻底搞清服务端逻辑,我将 Windows 安装包与 Android 官方 APK(app-release-4_8.apk)一同导入项目,交由恢复额度的 Opus 4.6 展开深度反编译。

借助 jadx 反编译出服务端的 Java 源码后,所有谜团迎刃而解:

1. 音频无声之谜:被误传的 UDP 端口

在反编译文件 d2/d.java 中,处理 0x66(配置编码)的逻辑揭示了协议真实定义:

this.f12091q = dataInputStream.readByte();  // 编码类型 (0x02 为 Opus)
this.f12092r = dataInputStream.readByte(); // 采样率档位 (0=8k, 1=16k, 2=48k)
this.f12093s = dataInputStream.readInt(); // 客户端用于接收音频的 UDP 端口号

此前根据常规认知,最后 4 字节被当成了采样率数值 48000(0x0000BB80)发送。手机收到后,误认为 Mac 客户端的 UDP 接收端口为 48000,便将所有音频包发往 Mac 的 48000 端口,而客户端一直在 8125 端口监听等待。

将最后 4 字节修正为客户端真实的 UDP 监听端口(8125,十六进制 0x00001FBD)后,UDP 音频数据包瞬间恢复正常接收。

同时,反编译源码进一步确认了 UDP 音频包的 11 字节头部格式:
[协议版本 (2 字节大端 0x0400)] [长度 (2 字节)] [序号 (2 字节)] [时间戳 (4 字节)] [标志 (1 字节)],第 11 字节之后即为标准的 Opus 裸音频流。

2. 五秒断联真相:指令混淆与保活机制

在 V1/l.java 与 A/g.java 中,服务端在收到 0x67 开始采集指令后,会在后台启动一个 5000ms 的超时定时器 TimerTask(V1/k.java):若 5 秒内未收到心跳保活包,服务端将判定客户端断开,并主动销毁音频采集线程。

更关键的是指令定义的混淆:

  • 0x68(104):在源码中实际对应 CMD_STOP_CAPTURE(停止采集)。
  • 0x69(105):才是真正的 CMD_HEARTBEAT(重置 5 秒倒计时的保活心跳)。

此前代码在轮询线程中每 3 秒发送一次 0x68,无异于持续要求手机停止采集;而在 Swift 客户端中仅发送了 UDP ping,未在 TCP 控制通道发送 0x69 心跳,导致服务端在第 5 秒准时触发超时自毁。

源码同时表明,服务端不会对 0x69 心跳包返回任何应答,客户端只需每隔 2 秒单向发送一次空的 0x69 控制帧即可保持连接。

3. 连接状态与断联同步

在捕获到 TCP 连接断开及心跳异常事件后,完善状态机的通知流转机制。一旦手机端挂断或异常离线,Mac 客户端能够即时感知并自动切换回待连接状态。

落地效果与验证

将上述协议逻辑完整实现于 Swift 原生客户端后,整体通信链路恢复稳定:

  • 音频传输:音频流以 50 包/秒(每帧 20ms)稳定接收并解码,长时间运行无断流与卡顿。
  • 实时监测:SwiftUI 界面上的 VU 电平表随输入声音实时动态响应,感知延迟极低。
  • 增益控制:支持 0% 至 300% 的音量平滑增益调节与一键静音切换。
  • 系统集成:音频输出目标设为 BlackHole 2ch 后,微信、飞书、腾讯会议等各类通讯软件均可直接将其选为系统默认输入麦克风。

总结与项目地址

完整项目源码与构建说明已开源在 GitHub:
👉 https://github.com/luanyufei/MacMic

在缺乏文档的私有协议逆向过程中,推断往往伴随着盲区。无论是对防火墙拦截的猜想,还是对参数含义的惯性假设,都会耗费大量的调试成本,而实际逻辑始终隐藏在服务端的二进制字节中。

在多模型协作的分工上:

  • Gemini 3.7 Flash 擅长快速搭建 UI 框架、组织模块架构与编写通用胶水代码;
  • Opus 4.6 则在二进制反汇编、反编译源码逻辑推导及协议逆向分析中表现出色,能够精准定位异常时序与字段定义。

当提供真实的代码上下文与反编译目标后,大模型基于代码语义与时序逻辑的精细排查,能够大幅提升逆向工程与协议还原的效率。