OpenBLIP25/wave-shim

GitHub: OpenBLIP25/wave-shim

一个 Win32 shim 工具,通过 stdio 协议将专有 DLL 中的声码器对象暴露给 Linux 主机调用,实现跨平台的二进制互操作。

Stars: 0 | Forks: 0

# wave-shim 从 Win32 控制台进程中调用 `W7K_UA_SDK.dll` 内部的 vocoder 对象, 并通过长度前缀的 stdio 协议将其暴露出来,以便 Linux 主机可以 驱动它们。 **范围。** 针对专有 DLL 的二进制互操作。codec 作为黑盒调用。这里的任何内容都没有对 codec 内部的工作原理进行建模、重新实现、音译或解释,在这里了解到的任何内容都不属于 codec 实现。本项目特意与任何 MBE/AMBE 工作保持分离。 ## 这不是什么 —— 在你做出任何假设之前请阅读此内容 下面的每一行都是经过测量的,而不是推断的,并且每一行都链接到带有 证据的章节。其中大多数会*安静且看似合理地*失败,而不是发出明显的报错, 这就是为什么它们被收集在这里的原因。 **1. 这不是 AMBE-3000 芯片的 codec,也不是 DVSI 的参考编码器。** 它是 WAVE 7000 PTT 软客户端的 vocoder —— 同一 codec 系列的一个同级集成。 与三个独立参考(物理芯片、DVSI 的 STD 配置、DVSI 的 RC 配置)对比测量, 编码器最多只有百分之几的帧结果一致。解码器在纯音上完全准确, 但在语音上会出现偏差。仅作为 **WAVE 桌面目标** 的有效 oracle。 不要将其用作无线电芯片的替代品。 → *此 DLL 与芯片相同吗?* **2. 没有 FEC。DLL 完全信任其输入。** 49 个 payload bit 中的每一个 都直接进入合成;没有任何东西被纠正。喂给它一个损坏的帧,你会得到损坏的音频, 没有错误、没有标志也没有抱怨。`processFrame` 的 FEC 错误日志字符串存在于二进制文件中, 但在此路径上并未被执行 —— 不要将它们视为已纠正的证据。 → *此路径上没有 FEC* **3. 隐藏(Concealment)必须在此 shim 之外进行。** 没有坏帧/BFI 输入,也无法告诉 DLL“这个帧丢失了” —— 它会乐于从中合成垃圾。 如果你的 bitstream 带有隐藏/擦除标志(P25 捕获数据有),主机必须检测到它并在调用*之前* 替换为重复或静音。合成坏帧听起来就是静电噪声的样子。 → *此路径上没有 FEC* **4. 空中(Off-air)帧不能按原样接受。** P25 rate-34 码字在空中传输时 是 3 路列交织的;此 DLL 需要自然的 info-vector 顺序。喂入原始字节,它不会失败 —— 它会产生自信的、类似语音的垃圾, 声音比正确解码的结果*更大*。 → *已通过真实的空中音频验证* **5. 错误通常是响亮的,而不是静音的。** 错误的帧布局、错误的 bit 顺序或错误的 vector 配对都会产生看似合理的音频,有时其 RMS 甚至 比正确答案还要高。**通过包络相关性来判断。** RMS 无法区分它们,而波形相关性会低估*正确*的结果,因为参数化 vocoder 不保留相位。 → *发现* **6. 它不是无延迟的。** 编码器延迟一帧(解码器则不会)。 没有 flush 调用 —— 推入一个额外的静音帧以清空尾部。 并且将解码后的音频与原始音频对齐与将 bit 与输入帧对齐是*不同*的数值。 → *发现* **7. 实例创建不是线程安全的。** 已确认在一个线程中运行多个实例是 bit 级精确的。 按帧路径由 DLL 进行锁保护,但实例创建会触及一个未受保护的进程全局注册表, 并且从未测试过来自多个线程的并发调用。请串行化 OPEN/CLOSE。 → *多个同时存在的实例* **8. 绑定仅对确切的一个 DLL 构建版本有效。** 不同的构建版本,每个 偏移量都是错误的,你将在执行任意字节。此 shim 会对 DLL 进行哈希处理, 并在不匹配时拒绝绑定。 → `binding.h` **9. `mode=stub` 不是真正的 codec。** stub 的存在是为了通过已知答案验证调用约定和编组(marshalling)。执行真正工作的主机应该在 `HELLO` 报告 `mode=stub` 时拒绝继续执行。 **10. 永远不要从重置后的前几帧判断行为。** 它们是启动瞬态, 即使一切正确,解码出的结果也会接近静音。这个假象已经在这里造成了一轮错误的诊断。 ## 状态 | | | |---|---| | 步骤 1 —— stub DLL,约定 + 编组已验证 | **完成,22/22 项检查通过** | | 步骤 2 —— 真实 DLL:所有四个类均构造并运行 | **完成** | | 往返(Round trip),帧布局,延迟特性 | **完成,已测量** | | 已通过 14 个真实的空中 P25 捕获数据验证 | **完成,14/14** | | 与芯片 + DVSI 参考 vector 对比 | **完成 —— 非 bit 级一致** | | FEC 存在性,多实例隔离 | **完成,已测量** | | 持续负载:20,000 帧,1500 次句柄循环 | **干净,37 倍实时速度** | | 为主机作者提供的协议规范 | `PROTOCOL.md` | `make checkall` 会运行下面的所有套件:stub、真实 DLL、空中捕获、参考 vector、行为探针和压力测试。 步骤 1 已经发挥了作用:它捕获了绑定表中错误的 `out_bytes`(Tdma 解码器每次调用返回 640 字节,而不是 320 字节),避免了这个错误被误认为是 codec 问题。 ## 构建和运行 此 shim 是一个 PE32 (i386) 控制台二进制文件,主机是纯 Python 3,通过 stdin/stdout 与它通信。协议或 shim 中没有任何东西与某一个 操作系统绑定;只有*启动方式*不同。 ``` make # build PE32 exe + stub.dll into build/ make test # step-1 self-test against stub.dll — no real DLL needed make checkall # every suite: stub, real DLL, off-air, vectors, probes, soak ``` `make test` 可以在 `make` 之后立即在干净的检出上运行:stub 提供 已知答案,因此无需专有 DLL 即可验证调用约定和编组。 ### 三种支持的环境 | | 构建 | 启动 | 暂存 | |---|---|---|---| | **原生 Windows** (MSYS2/MinGW) | `make CC=gcc` | 直接 | 无 | | **WSL** | `make` (交叉编译) | 直接,通过 `binfmt_misc` 互操作 | `make stage` | | **Linux + Wine** | `make` (交叉编译) | 通过 `wine` | 无 | 主机脚本会检测它们当前处于哪种环境,并相应地解析 shim 路径、 DLL 路径和测试数据路径。每个默认值都是可覆盖的: | 变量 | 含义 | |---|---| | `SHIM_CMD` | 启动 shim 的完整命令行(优先于 `WINSTAGE`) | | `WINSTAGE` | 包含 `wave-shim.exe` / `stub.dll` 的目录 | | `DLL_WIN`, `STUB_WIN` | **shim 解析时使用**的 DLL 路径 | | `DATA_DIR` | 测试数据的根目录;或单独设置 `CAPS`, `TV_*`, `CHIP`, `DEC` | | `PCM` | 用于往返、压力测试和延迟套件的源 PCM | **在 WSL 上**,PE 二进制文件通过 `binfmt_misc` 原生执行,因此 shim 是一个 真正的 Windows 进程,管道原封不动地跨越了边界。这比在这里使用 Wine 更好 —— Wine 重新实现了 Win32 表面,当专有 DLL 行为不当时,你不会希望有两个未知数。因为 shim 是一个 Windows 进程,你交给它的任何路径都必须是 *Windows* 路径(`C:\...`,而不是 `/mnt/c/...`);主机脚本会自动转换。`make stage` 会将二进制文件复制到 `/mnt/c/temp/wave-shim`,以便它们位于 Windows 可见的路径上。 **在原生 Windows 和 Wine 下**,不会发生暂存 —— `make stage` 会报告这一点,脚本将直接从 `build/` 启动。如果你想强制进行暂存,请显式设置 `WINSTAGE`。 MinGW 之所以重要,原因值得了解:DLL 可能会抛出 MinGW 构建的二进制文件**无法捕获**的 MSVC C++ 异常(参见 `binding.h`,provider 备注)。此 shim 采取的路线完全避免了该问题。 ### 在 Windows (MSYS2) 上构建 设计上没有预构建的二进制文件 —— 请参阅下文的*为什么没有发布版本*。此 构建除了 32 位 MinGW gcc 之外没有其他依赖项,因此非常简短: 1. 安装 [MSYS2](https://www.msys2.org/),然后从 **MSYS2 MINGW32** shell(不是普通的 MSYS shell —— 环境决定了目标架构)运行: pacman -S --needed mingw-w64-i686-gcc make git 2. 构建并运行自测: git clone https://github.com/OpenBLIP25/wave-shim cd wave-shim make CC=gcc # 在 MINGW32 中,普通的 `gcc` 就是 i686 编译器 make test `make test` 需要 Python 3;参考 vector 和空中捕获套件额外 需要 NumPy。无论是 MSYS2 的 Python(`pacman -S mingw-w64-i686-python-numpy`)还是 python.org 的安装版本都可以 —— shim 是一个独立的进程,因此主机解释器的架构不必匹配。如果你的 Python 名为 `python` 而不是 `python3`,Makefile 会检测到;如果猜错了,可以使用 `make test PYTHON=/c/Python312/python.exe` 进行覆盖。 无需暂存步骤:在 Windows 上,脚本直接启动 `build\wave-shim.exe`。 ### 提供 DLL 和测试数据 这两者都不在此 repo 中,构建时也不需要它们。 ``` cp /path/to/W7K_UA_SDK.dll / ``` `W7K_UA_SDK.dll` 随 WAVE 7000 PTT 软客户端一起发布,**此处不重新分发** —— 请提供你自己的授权副本。shim 会检查其 SHA256,并拒绝绑定到 `binding.h` 描述的那一个构建之外的任何内容。 套件默认使用的夹具(`vectors/clean.pcm`, `vectors/voiced.pcm`, `vectors/chip_io/`, `vectors/ambe-samples/`)同样不存在;将上面的变量指向你自己的副本。 与其每次手动设置这些,不如在 Makefile 旁边放一个 **`local.mk`** —— 如果存在,它会被 `-include` 引用,并且被 gitignored,因此本地路径永远不会进入 repo: ``` DVSI_ROOT := /path/to/your/vectors PCM_CLEAN := $(DVSI_ROOT)/clean.pcm PCM_VOICED := $(DVSI_ROOT)/voiced.pcm export CHIP := $(DVSI_ROOT)/chip_io/encode export DEC := $(DVSI_ROOT)/chip_io/decode export CONV := /path/to/r33_to_info_r34 # 测试套件需要不同的源音频 realtest: export PCM := $(PCM_VOICED) soak probes: export PCM := $(PCM_CLEAN) ``` 有了它,`make checkall` 就可以在不设置环境变量的情况下运行。 `make stage-data` 会自动执行该复制,**仅在 WSL 上**,从 Windows 共享进行: ``` make stage-data DATA_SHARE='\\your-share\path' DVSI_VECTORS='\\your-share\path\DVSI Vectors' ``` 映射的网络驱动器**不会**挂载到 WSL 中 —— 网络驱动器不会自动挂载 —— 因此复制是通过 Windows 进行的。`cmd.exe` 无法处理 `"DVSI Vectors"` 中的空格;该部分使用 PowerShell。在其他地方,请自行复制目录树并设置 `DATA_DIR`。 ### 为什么没有发布版本 特意没有将预构建的二进制文件附加到此 repo。 1. **预构建的 exe 几乎对任何人都没用。** `binding.h` 仅对确切的一个 DLL 构建有效,并且 shim 在绑定前会验证其 SHA256。针对这些偏移量编译的二进制文件仅对持有相同 DLL 的人有用;其他人都会被拒绝,并且无论如何都必须重新推导偏移量并重新构建。 2. **它看起来像恶意软件,因为从结构上讲它具有相同的特征。** 一个未签名的 PE 调用 `LoadLibrary` 然后分发到原始的计算地址,这在扫描器看来就像一个 loader 或 injector。发布这样一个文件会招致 Defender 和 SmartScreen 的警告,而不得不告诉用户点击跳过 —— 这正是任何人都不应该教导的习惯。 3. **安全声明必须是可检查的。** 运行此程序的合理理由在于 shim 拒绝绑定到意外的 DLL。只有当你能够阅读执行此操作的源代码时,这才是一个有意义的保证。一个发布版本会将其变成你只能盲目信任的东西。 4. **构建只需两次编译器调用**,没有依赖项,没有配置步骤,也没有代码生成。 从源代码构建,并在针对真实的 DLL 运行它之前阅读 `binding.h`。 ## 布局 ``` binding.h THE build-specific block — RVAs, vtable indices, object sizes, frame sizes, calling convention. Valid for exactly one build; the shim hashes the DLL and refuses to bind on a mismatch unless --force-unverified-dll. protocol.h wire format, shared by shim and host shim.c the executable: LoadLibraryEx, base+RVA resolution, __thiscall dispatch, framing, crash reporting stub/stub.c step-1 target: same binary shape, known answers host/waveshim.py reference client — copy or port this host/selftest.py step-1: drives the shim against the stub, 22 properties host/realsmoke.py real DLL: all four classes, round trip, delay regression host/realprobe.py real DLL: minimal "push N frames and print them" host/roundtrip.py real DLL: speech round trip + frame-layout discrimination host/decoderdelay.py differential decoder-delay measurement host/delay_resolve.py reconciles the three different delay numbers host/otacheck.py real off-air captures vs known-correct renderings host/vectorcheck.py vs physical chip + DVSI reference vectors host/fectest.py single-bit-flip sweep: is any error correction happening? host/isolation.py many simultaneous instances, bit-exact independence host/resettest.py RESET is per-instance and equals a fresh instance host/soak.py sustained load, handle churn, memory ``` ## 协议 请求 `[u32 len][u8 op][payload]`,响应 `[u32 len][u8 status][payload]`,小端序,无分隔符。操作:`HELLO`, `OPEN(kind, rate)`, `PROCESS(handle, bytes)`, `RESET(handle)`, `CLOSE(handle)`。错误以状态 1 和人类可读的原因返回,而不是导致管道死亡`HELLO` 仅播发那些入口点实际已绑定的 rate;任何未解析的内容都会在 `unbound:` 下列出并附带原因。 ## 来自真实 DLL 的发现 - **编码器有一帧的延迟;解码器没有。** 输出帧 N 包含在第 N-1 帧输入的音频。静态阅读显示并非如此;但测量结果胜出。用额外的一帧静音清空尾部。 - **三个延迟数字,都是真实的,都不同 —— 不要将它们混为一谈。** 因果(bit)+1;解码音频能量起始 +2;能量峰值 +3。多出来的帧是合成扩散,而不是延迟:一帧的能量通过 overlap-add 合成分散到了后续帧中。将 *bit 与输入帧*对齐 1;将*解码后的 PCM 与原始 PCM* 通过包络相关性(~2-3)对齐。波形相关性延迟根本不是延迟度量 —— 因为 vocoder 丢弃了相位,它报告的是一个具有误导性的子帧数。 - **FDMA 编码器发射 36 字节,而不是 33 字节** —— 3 个 12 字节的单元(使用 11 个 + 1 个 pad),与 FDMA 解码器的 36 字节输入完全匹配。 - **`vtable[0]` 是 MSVC 的标量删除析构函数**,`(this, unsigned flags)`,`ret $4`。不带参数调用它会破坏调用者的栈。 - **每个 codec 实例都需要自己的缓冲池。** 析构函数会拆除其缓冲池,因此共享缓冲池会在第一次 CLOSE 时随之消亡。 - **往返是可以工作的,并且 TDMA pad 字节是尾随的**(payload 在前,然后是 pad)。在 180 帧的语音上:包络相关性尾随为 **+0.987**,而领先为 **-0.408**。早先的“近乎静音的解码”是在重置后直接测试两个重复帧的假象,而不是布局问题。FDMA 不需要重新打包 —— 编码器的 36 字节原封不动地喂给解码器(包络相关性 **+0.993**)。每次调用 2 个还是 4 个单元纯粹是批处理。 - 使用**包络**相关性而不是 RMS,也不是波形相关性来判断布局:错误的布局仍然会解码出类似语音的*能量*(rms 2461,而输入 rms 为 2034),同时与实际语音呈负相关。RMS 看不到这一点;波形相关性低估了正确的结果,因为参数化 vocoder 不保留相位。 ## 已通过真实的空中音频验证 14 个真实的 P25 Phase 2 捕获(271 秒,post-FEC rate-34 码字),每个都附带了相同字节的两种渲染 —— 正确的 49-bit 读取和不正确的一种,后者故意渲染成令人信服的垃圾。这使得它成为一个带标签的测试,而 DLL 就是参考实现。 | 喂入 | vs 正确渲染 | vs 垃圾渲染 | |---|---|---| | 原始空中字节 | −0.09 | **+0.91** | | 去交织后 | **+0.998** | +0.003 | 一旦去交织,14 个捕获中的 14 个都同意正确的渲染,并且 DLL 的信号统计数据与它匹配(rms 539 vs 537,无消波),而不是垃圾(rms 2486,大多数调用中都有消波)。所以:shim 正确地喂入了空中帧,而 DLL 独立地证实了两种读取中哪一种是正确的 —— 这是从相反的方向得出的。 **空中帧在被此 DLL 接受之前,必须去交织为自然的 info-vector 顺序。** 喂入原始数据时,它不会失败 —— 它会产生自信的垃圾。参见 `PROTOCOL.md` 和 `host/otacheck.py`。 ## 此 DLL 与芯片相同吗?不相同 —— 已测量,双向验证 一个自然的假设是此 DLL 与物理 AMBE-3000 是 bit 级一致的。事实并非如此,无论哪一边。`make vectortest` 使用匹配的源材料针对参考 vector 对其进行测量。 两个独立的参考对答案达成了一致。 **编码器** vs DVSI 发布的参考编码,每个都与其各自 tree 的顶层源 PCM 配对,扫描对齐和 bit 顺序: | 参考 | 词干 | 帧精确 | bit 一致性 | |---|---|---|---| | RC 配置 (`tv-rc`) | clean | 3.8% | 78.5% | | RC 配置 | dam | 1.3% | 77.3% | | RC 配置 | alert | 0.2% | 68.1% | | STD 配置 (`tv-std`) | clean | 0.2% | 70.1% | | STD 配置 | dam | 0.3% | 70.3% | | STD 配置 | noisy | 0.2% | 72.9% | **编码器** vs 物理芯片的 rate-34 bitstream,相同的 PCM 输入(`chip_io/encode/*.chip34.bit`),扫描帧对齐和 bit 顺序: | 词干 | 最佳帧精确 | 最佳 bit 一致性 | |---|---|---| | mark | 2.0% | 79.8% | | cpvbad | 1.0% | 76.8% | | dtone_10 | 0.8% | 58.4% | 两个参考给出了相同的结论:百分之几的帧精确,bit 一致性高于约 50% 的随机基准,但远未达到一致。DLL 共享格式和结构;它不共享编码器的决定。 **正确配对 vector 是这里的陷阱。** 在每个 DVSI tree 中,源 PCM 位于顶层;`rNN/*.pcm` 是以 rate NN 进行的编码-*解码*输出。将 `r34/*.bit` 与 `r33/*.pcm` 配对,是将解码音频的编码与原始音频的编码进行比较,得分接近零 —— 该测试的第一次运行正是这样做的。`tv-rc` 和 `tv-std` 是两种不同的 DVSI codec 配置(在 DVSI 自己的比较脚本中为 `-c RC` / `-c STD`);两者都包含可用的直接编码参考,它们产生不同的 bit,并且它们在相同的文件名下提供了不同的源音频。永远不要交叉使用这些 tree。 获胜的对齐是移位 0 处的 r34 交织,这顺便证实了两件事:DLL 发射**自然**顺序,而芯片的捕获是交织的,并且两个编码器都带有相同的 1 帧延迟。 **解码器** vs 芯片对相同帧的解码 —— 并且根据捕获清单,芯片的解码与 DVSI 的*已发布参考*是字节一致的,所以这实际上是与 DVSI 本身的比较: | 词干 | 样本精确 | 仅活动样本 | 最大差异 | |---|---|---|---| | dtone_10 (DTMF 音) | **98.93%** | 98.96% | **1** | | cpvbad | 25.9% | 21.8% | 649 | | tia11 | 32.6% | 2.9% | 8841 | | fambf22c | 40.5% | 2.1% | 7185 | | mark | 11.1% | 2.8% | 6473 | 在纯音上,DLL 与参考解码器的差距在一个 LSB(最低有效位)之内。在语音上则存在显著差异。纯音完美 / 语音分歧的这种分歧指向了浊音合成,而不是任何结构性的东西 —— 但追查这些属于 codec 的工作,而不是 shim 的工作。 **对于任何将其用作 oracle 的人的后果:** 它是 WAVE 桌面目标的有效参考,而它*不是*无线电芯片的 bit 级精确替代品。不要将两者混为一谈。 ## 此路径上没有 FEC —— DLL 信任其输入 在一帧中一次翻转一个 bit,并使用新实例重新解码: **49 个 payload bit 中有 49 个会改变音频,7 个 pad bit 中有 0 个会。** 没有任何东西被纠正。损坏的帧会默默地产生损坏的音频 —— 没有纠错层,也没有坏帧输入来发出信号。 这符合产品定位:这是一个位于 FNE *背后*的软客户端,因此纠错已经在上游发生,vocoder 接收的是干净的 post-FEC info bit。它还解释了 payload 大小(49 bit,没有空间容纳 rate-33 帧的 FEC bit)。 解码器的 `processFrame` 确实带有 FEC 错误日志字符串,因此代码是存在的 —— 但它并未在此入口点上被执行。应将这些字符串视为属于另一个路径或配置,而不是此处已纠正的证据。 相同的测试独立地证实了帧结构:每个单元的第 8 个字节是真正被忽略的填充。 ## 多个同时存在的实例:已确认 如果控制台一次混合多个源,则相关。六个编码器实例以轮询方式运行,每帧每个实例调用一次,产生的输出与单独运行每个流**在 bit 级完全一致**。所有四个类同时运行,并且一个实例不受邻居在流中间被打开和关闭的影响。 线程是一个单独的问题,仅部分得到了解答: | | | |---|---| | 一个线程中的多个实例 | **已确认**,bit 级精确的隔离 | | 按帧路径 | DLL 保护它们:acquire、release 和 assign 都使用临界区 | | 实例*创建* | **未受保护** —— 全局缓冲池注册表插入及其计数器没有锁 | | 来自多个线程的并发调用 | **未测试** —— shim 是单线程的 | 所以:串行化 OPEN/CLOSE。鉴于 DLL 自身的锁定,来自独立线程的按流工作是合理的,但尚未有人证明这一点。 ## 在传输之间进行 RESET:安全,且严格按实例 控制台模式 —— 在传输之间重置 vocoder —— 已确认是 bit 级精确的: - **RESET 后,实例与新打开的实例在字节上完全一致。** 状态被真正清除了,而不仅仅是微调。(与二进制文件一致:构造函数调用了与 RESET 相同的初始化例程。) - **RESET 仅触及它自己的实例。** 一个实例每 5 帧重置一次,而连续运行的邻居完全不受影响 —— 输出与单独运行时相同。没有全局重置。 - **25 个背靠背的 reset-then-transmit(重置后传输)循环每次都产生相同的输出**,并且该输出与新实例匹配。跨传输没有漂移。 - 解码器的行为方式相同。 一个预期的注意事项,而不是缺陷:RESET 后的前几帧是启动瞬态,就像新构造的实例一样。不要通过它们来衡量质量或诊断行为。 因此,你可以在传输之间进行 RESET,或者关闭并重新打开 —— 它们是等效的。RESET 成本更低,因为它避免了拆除和重建缓冲池。 ## 性能和稳定性 20,000 帧编码 + 解码:**1850 帧/秒,约 37 倍实时速度**,没有缓冲池耗尽。1500 次 open/process/reset/close 循环:没有内存增长(DLL 的析构函数确实释放了其缓冲池)。经过压力测试后,一个新实例仍然可以逐字节地重现其第一帧。 ## 从主机使用它 有关传输格式,请参见 `PROTOCOL.md`;有关参考客户端,请参见 `host/waveshim.py`。此机器上不存在 `mbe-bench`/`shim-host`,因此协议是在这里设计的而不是采用的;如果其他地方存在具有自己格式的协议,请优先使用他们的 —— 只需要更改 `shim.c` 中的 `op_*` 处理程序。 ## Provider 是如何解析的(路径 A) 构造函数接受一个参数:一个引用计数的 buffer provider,存储在 `+0x16e4`(编码器)/ `+0x7ec`(解码器)。`process()` 从它那里获取输出缓冲区并无条件地解引用它 —— 因此那里的 NULL 构造没问题,但在第一次调用时会崩溃。DLL 为你构建了一个:位于 RVA `0x1baf00` 的缓冲池工厂接受一个 `{vtable, bytes_per_buffer}` 策略对象并返回包装器。它触及的全局注册表由 MSVC CRT 在 `DLL_PROCESS_ATTACH` 时初始化,因此仅靠 `LoadLibrary` 就足够了。 它**不是**一个可由调用者实现的接口 —— `process()` 通过对具体 DLL 内部类的*直接*调用到达它,而不是通过调用者拥有的 vtable 进行虚分派。所以 provider 必须来自 DLL,现在 shim 完全按照 DLL 自己的音频会话工厂所做的那样做: ``` cell -> N : refcount node, 0x3c bytes -> P : payload descriptor +0x00 P +0x00 void* data +0x04 refcount +0x04 uint32 length +0x0c CRITICAL_SECTION +0x08 uint32 capacity ``` `process()` 的 `param1` 是 `&cell`;三重间接是 `param1 -> N -> P`。成功后,DLL 将输出节点*分配*在你的 cell 上,并且 assign() 会首先释放之前存在的任何内容 —— 因此输入的 cell 必须持有一个**真正的缓冲池节点**,而不是一个伪造。这几乎肯定就是导致旧测试工具每帧抛出“device or resource busy”并破坏其自身 cell 的原因:它交给 DLL 的是一个伪造的节点,其 +4 和 +0xc 处都是垃圾。 每帧:从缓冲池获取,将 PCM 复制到 `P->data`,设置 `P->length`,调用 `process()`,从同一个 cell 中读回输出,将其复制出来,然后释放。没有伪造的东西,没有打补丁,没有 logger 或 cache 拦截。 ## 在信任任何输出之前 1. ~~脉冲测试~~ —— **完成,并且它推翻了静态阅读**:确实有一帧的编码器延迟。参见上文的发现。 2. ~~往返~~ —— **完成**:编码器→解码器在 DLL 中干净地完成了两种 rate 的往返(参见发现)。 3. ~~空中映射~~ —— **完成**:已通过 14 个真实的 P25 捕获验证,这是 DLL 到 DLL 的协议永远无法自行建立的。 4. 仍然悬而未决:**IMBE/FDMA 没有空中验证**(所有 14 个捕获都是 Phase 2 AMBE+2),并且**真正的多线程操作未经测试**。 完整证据,带有逐项置信度和观察与推断的标志: 一份内部笔记,`W7K_SHIM_BINDING_FACTS_2026-07-27.md`(未公开)。
标签:Win32, 标准输入输出协议, 语音编解码, 跨平台互操作, 进程桥接, 音频处理