Skip to content

Commit be0f090

Browse files
committed
feat(audio): 进程环回采集原生模块 Phase 1 可行性验证 + ADR-001
端点环回(getDisplayMedia)在系统主音量之后截取全系统混音,导致两类 问题:跨应用杂音混入课堂转写(ASR 幻觉的可能成因之一),以及音量 0/ 设备错配时采到全零。引入 Windows 10 2004+ 进程环回作为互补音频源。 Phase 1 spike 全部硬验收通过(实测数据见 README 与 ADR): - 杂音隔离:同时刻发声进程 RMS 0.049 / 静默进程 RMS 0.000000 - Chromium 进程树覆盖(网课成败点):Electron 声源 RMS 0.055, 证明能采到 audio service 子进程的声音 - 系统静音下峰值与正常音量完全一致(连衰减都没有) - 16kHz mono float32 被直接接受,无需实现重采样 - Chromium 顶层窗口归属 browser process,窗口 PID 即进程树根 ADR-001 记录决策:双源互补而非替换——端点环回是设备视角(永不漏采但 脏),进程环回是应用视角(干净但可能漏采);虚拟声卡方案因需内核驱动 签名与用户安装摩擦被否决。 本模块独立于 client 主构建(未接入 package.json / electron-builder), 对已发布版本零影响。
1 parent b46be5d commit be0f090

19 files changed

Lines changed: 2593 additions & 0 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# process-audio — Windows 进程环回音频采集(Phase 1 spike)
2+
3+
面向课堂助手音频采集的原生模块。当前处于 **Phase 1(可行性验证)已完成**状态,
4+
尚未接入 client 主构建,对已发布版本零影响。
5+
6+
## 为什么需要它
7+
8+
现有音频采集走 Chromium 的 `getDisplayMedia({audio:'loopback'})`,即
9+
**端点环回**——在系统主音量之后截取最终混音,两个原理性缺陷:
10+
11+
| 缺陷 | 后果 |
12+
|---|---|
13+
| 采全系统混音 | QQ/微信提示音、其他视频混入课堂转写(脏数据源头,**主要动因**) |
14+
| 跟随主音量/静音 | 音量 0、每应用音量调低、默认输出设备错配 → 采到全零 |
15+
16+
**进程环回**(Windows 10 2004+ 的 `AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK`)
17+
在混音之前按进程树截取,两个缺陷同时消除。这也是 OBS 28+「应用程序音频采集」
18+
的实现路线,无需虚拟声卡驱动、无需管理员权限。
19+
20+
## Phase 1 验收结果(全部通过)
21+
22+
| 验收项 | 结果 |
23+
|---|---|
24+
| 编译链路(VS 2022 + node-gyp,Python 3.14 可用) | ✅ 零错误零警告 |
25+
| 窗口 → PID → 进程树根 | ✅ 可用。**Chromium 顶层窗口归属 browser process**,窗口 PID 直接即树根 |
26+
| **杂音隔离**(主要收益) | ✅ 发声进程 RMS 0.049 / 同时刻静默进程 RMS 0.000000 |
27+
| **Chromium 进程树覆盖**(网课成败点) | ✅ Electron 声源 RMS 0.055,证明能采到 audio service **子进程**的声音 |
28+
| 系统静音下仍可采 | ✅ 静音时峰值 0.299246 与正常音量下**完全一致**(连衰减都没有) |
29+
| 16kHz mono float32 格式协商 | ✅ 被 Windows 直接接受 → **无需实现重采样** |
30+
31+
## 目录结构
32+
33+
```
34+
src/
35+
window_finder.{h,cc} 窗口枚举 + PID → 应用根进程回溯
36+
loopback_capture.{h,cc} WASAPI 进程环回采集 + WAV 落盘 + RMS 统计
37+
addon.cc N-API 绑定(listAudioWindows / resolveRootPid / captureToWav)
38+
test/
39+
spike-windows.mjs 验证窗口/进程解析
40+
spike-capture.mjs 按窗口关键字采集并落盘(人工复核用)
41+
spike-isolation.mjs 杂音隔离对照实验
42+
spike-muted.mjs 系统静音场景验收(需手动静音)
43+
spike-chromium.mjs Chromium 进程树覆盖验证(全自动)
44+
chromium-source/ Electron 等价声源(含播放状态自检)
45+
```
46+
47+
## 本地构建与验证
48+
49+
```bash
50+
cd client/native/process-audio
51+
npm install
52+
npx node-gyp rebuild
53+
54+
node test/spike-windows.mjs # 窗口与进程解析
55+
node test/spike-chromium.mjs # 网课场景(全自动,会短暂响铃)
56+
node test/spike-muted.mjs # 静音场景(按提示手动静音)
57+
```
58+
59+
## 已知边界
60+
61+
- **仅 Windows 10 2004(build 19041)+**,低于此版本需降级到端点环回
62+
- **WASAPI 独占模式**播放的应用采不到(罕见)
63+
- **漏采风险**:只采目标进程树,用户换播放器/新开应用需重新绑定——这是
64+
与端点环回互补而非替代的根本原因(端点环回"永不漏采但脏")
65+
- macOS/Linux 无对应 API
66+
- 混音器内**每应用音量**是否衰减尚未单独实测(主音量已确认无影响)
67+
68+
## 后续阶段
69+
70+
Phase 2 集成时需把当前的同步阻塞采集改为**独立采集线程 +
71+
`Napi::ThreadSafeFunction` 流式回调**,产出符合 `AudioChunkData` 契约的
72+
16kHz mono Float32 块(5s/块),下游 VAD/ASR/幻觉过滤零改动。
73+
74+
## 过程教训
75+
76+
验证 Chromium 场景时曾连续两次采到 RMS=0,一度指向"进程环回对 Chromium 无效"
77+
的错误结论。真实原因是 **spike 声源本身没启动**(Electron 以目录方式启动需
78+
`package.json` 指定 main 入口),与被测对象无关。加入「声源自报播放状态」自检
79+
后立即定位。**测量类实验必须先自证伪声源/测量链路,再评价被测对象。**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "process_audio",
5+
"sources": [
6+
"src/addon.cc",
7+
"src/window_finder.cc",
8+
"src/loopback_capture.cc"
9+
],
10+
"include_dirs": [
11+
"<!@(node -p \"require('node-addon-api').include\")"
12+
],
13+
"defines": [
14+
"NAPI_DISABLE_CPP_EXCEPTIONS",
15+
"UNICODE",
16+
"_UNICODE"
17+
],
18+
"conditions": [
19+
[
20+
"OS=='win'",
21+
{
22+
"libraries": [
23+
"-lole32.lib",
24+
"-loleaut32.lib",
25+
"-lavrt.lib",
26+
"-lmmdevapi.lib",
27+
"-lruntimeobject.lib"
28+
],
29+
"msvs_settings": {
30+
"VCCLCompilerTool": {
31+
"AdditionalOptions": [
32+
"/utf-8"
33+
]
34+
}
35+
}
36+
}
37+
]
38+
]
39+
}
40+
]
41+
}

0 commit comments

Comments
 (0)