3.B:vendor opencore-amr + AMRCodec Swift wrapper,打通 AMR↔WAV 编解码

为支撑 H5 的 prepareaudio / mediaTypeAudio 录音上传 + 远端语音播放
(docs/H5-Native-Contract.md §3.1 [4][5] + §3.2 [9])接入 AMR-NB
编解码能力。AMR-NB 是跨端(iOS / Android / 旧版本客户端)契约硬约束,
不可换 AAC / Opus。

## 接入路径

直接 vendor daoqi msext 现成的 fat .a(已上线多年),不打 xcframework /
不重编源码:
- Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a:daoqi 同款 5-slice fat
  库(含 2026-06-20 lipo -segalign 8 修过的对齐版本)
- Vendor/OpenCoreAMR/v0.1.x/{interf_dec,interf_enc}.h:amrnb 公开头
- Vendor/OpenCoreAMR/README.md:来源、版本、修复历史、为何不打 xcframework
- ylgamehall/Source/Audio/AMRFileCodec/{amrFileCodec.h,amrFileCodec.mm}:
  daoqi 同款 wrapper(文件级 WAV↔AMR 处理)
- ylgamehall/Source/Audio/AMRCodec.swift:Swift 薄 wrapper(wavToAmr /
  amrToWav 两个纯函数 + AMRCodec.Error)

理由见 Vendor/OpenCoreAMR/README.md「为何不打 xcframework / 不重编源码」
一节,本质是 opencore-amr 整个 iOS 生态没有"现代主流"分发形态,社区无
官方 SPM / xcframework 维护,强行打反而引入新风险。

## 关键技术点:extern "C" 修复 ObjC++ name mangling

amrFileCodec.h 在原项目里没有 extern "C" 包裹函数声明,daoqi 能跑是
因为调用方 VoiceConverter.mm 也是 ObjC++,双方按相同 C++ name mangling
链接能对上。

新外壳从 Swift 通过 bridging-header 调用,Swift 按 C 函数解析,期望符号
_EncodeWAVEFileToAMRFile / _DecodeWAVEFileToWAVEFile(C 链接);但
.mm 编译时默认 C++ name mangling,符号变成 __Z25Encode...,导致链接器
报 Undefined symbol。

修复:在 amrFileCodec.h 的函数声明外加 #ifdef __cplusplus / extern "C"
包裹。这是标准 C/C++ 头文件兼容写法,对 daoqi 老路径无影响(ObjC++
调用方仍能链接 C-linkage 函数)。

## .gitignore:白名单 Vendor 下的 .a

发现历史遗留问题:原 .gitignore *.a 通配规则把 libWeChatSDK.a 也排除
了,意味着别人 clone 此仓库后链接失败。本次顺带修复:

  !Vendor/**/*.a

让 Vendor 下的 .a 显式纳入跟踪。本提交同时把 libWeChatSDK.a(历史
缺失)+ libopencore-amrnb.a(本次新增)一并加入 git 跟踪。

## 工程改动

pbxproj 改动由用户在 Xcode UI 完成(4 项 Build Settings:Add .a 到
Frameworks group / Header Search Paths +2 / Library Search Paths +1;
EXCLUDED_ARCHS arm64-simulator 因真机调试场景暂未配,需要时再加)。

## 验证

BuildProject 通过,0 错误,符号链接正确。XcodeRefreshCodeIssuesInFile
对 AMRCodec.swift 0 诊断。
This commit is contained in:
joywayer
2026-06-24 01:20:30 +08:00
parent 5463b07fb6
commit 5b38456c66
11 changed files with 776 additions and 6 deletions
+109
View File
@@ -0,0 +1,109 @@
# OpenCoreAMR
`opencore-amr` 是 AMR-NB / AMR-WB 语音编解码的 C 库(OpenCORE 项目,Martin
Storsjo 维护)。新外壳沿用 daoqi msext 时期的同一份二进制 + 公开头,**不打
xcframework 不重新编译**。理由见下文「ADR」一节。
## 用途
支撑 H5 的语音功能(`prepareaudio` 录音 → AMR 编码 → 七牛上传;
`mediaTypeAudio` 下载 → AMR 解码 → 播放)。AMR-NB 格式是跨端(iOS / Android /
旧版本客户端)契约硬约束,不可换成 AAC / Opus。详见
`docs/H5-Native-Contract.md` §3.1 4][5 + §3.2 9]。
## 来源
- 上游:https://sourceforge.net/projects/opencore-amr/
- 当前镜像:https://github.com/BelledonneCommunications/opencore-amr
- 二进制版本:daoqi 沿用,约 2014 年打包,对应上游 **0.1.3** 时间点(v0.1.x 目
录命名取自此)。具体小版本无可考——`.a` 二进制无版本号字段,public 头
(`interf_enc.h` / `interf_dec.h`) 自 0.1.0 以来 ABI 未变
- daoqi 项目路径:`daoqi/msext/Class/Common/VoiceConvert/{lib/, opencore-amrnb/}`
## 目录
```
v0.1.x/
├── libopencore-amrnb.a 5 slice fati386 + x86_64 + armv7 + armv7s + arm64-device
├── interf_dec.h AMR-NB 解码 C API
└── interf_enc.h AMR-NB 编码 C API
```
不 vendor `libopencore-amrwb.a`——daoqi `amrFileCodec.mm` 实际只用 NB(窄带,
8kHz 单声道),WB 路径源码注释掉了,新外壳同样只用 NB。
## fat header 对齐补丁
daoqi `.a` 在 2026-06-20 用 `lipo -segalign 8` 重新打包过,所有 5 个 slice 的
fat header `align = 2^3 (8)`。这是为了适配 Xcode 26+ 的新链接器
`ld_prime` 对 fat 库 architecture slice 对齐字段的硬性要求(≥ 2^3);不修则
会要 `-ld_classic` 过渡选项(已确认是定时炸弹)。
详细背景与修复命令见 `youle_app_ios/CLAUDE.md` 「2026-06-20opencore-amr 静
态库 fat header 重新对齐」一节。
**已知遗留链接器警告(无害)**
- `No platform load command found in ... assuming: iOS`(每个 .o 一行,约 154
行)— 2014 年 .o 不带 `LC_VERSION_MIN_IPHONEOS` / `LC_BUILD_VERSION` load
command;新链接器假定为 iOS,行为正确
- `Reducing alignment of section __DATA,__common from 0x8000 to 0x4000`
要彻底消除这两类警告需要重编 opencore-amr 源码,违反「稳定 > 一切」原
则——**接受现状**。
## simulator-arm64 缺失
`.a` 是 2014 年打包,**没有 arm64-simulator slice**(这是 2020 年 Apple
Silicon 上市后才有的概念)。M1+ Mac 上 simulator 编译需要在 Build Settings 设
置:
```
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
```
让 simulator 退回 x86_64 切片(通过 Rosetta 跑)。**仅影响开发期 simulator**
release device 是 arm64 完整可用。
## 为何不打 xcframework / 不重编源码?
ADR:选 vendor 现成 .a 而非现代化 xcframework / 源码重编。
- **依据**`opencore-amr` 整个 iOS 生态都是"老 vendor 模式"——各家大厂(微
信、QQ、网易云信、声网 IM 等)都内部 vendor `.a`,没人公开维护 SPM / 官方
xcframework。社区无"现代主流"形态可参照
- **稳定性**daoqi 这份 `.a` 已上线 N 年稳定运行;现代化"形态"xcframework
本质上仍是 vendor 同一份二进制字节,形态收益边际,引入新打包/编译路径反而
增加风险
- **跨编辑成本**:要完整 xcframework 必须从源码编出 arm64-simulator slice,跨
编 iOSautoconf)容易踩坑,1+ 小时调试 vs vendor 几分钟落地
- **CLAUDE.md 原则一致**:项目「稳定 > 一切」+「daoqi 是工作参照实现」精神
要求避免"为现代化而现代化"。本目录的现代化收益体现在**组织规范 + 文档完
备**(与 WechatSDK Vendor 并列、README 来源/版本/修复历史齐全),而不是打包
形态
未来若 Xcode 大版本彻底干掉对 fat `.a` 的支持(被动升级),再走 xcframework
化或源码重编路线,按 CLAUDE.md「Xcode 26.5+ 兼容性 Build Settings」一节的
patch 形式记录即可。
## 替换 / 升级流程(未来用)
1. 从 SourceForge / GitHub mirror 拉新版本源码(如 0.1.6
2.`xcrun -sdk iphoneos clang` + `-arch arm64` 跨编 device slice
3.`xcrun -sdk iphonesimulator clang` + `-arch arm64`/`-arch x86_64` 跨编
simulator slice
4. `lipo -create -segalign 8` 合并为新 fat `.a`
5. 替换 `v0.1.x/` 下文件,更新本 README、提交
## 链接配置
- Build Phases → Link Binary With Libraries:加 `libopencore-amrnb.a`
- Build Settings → Header Search Paths:追加
`$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x`
- Build Settings → Library Search Paths:追加
`$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x`
- Build Settings → Excluded Architecturesiphonesimulator):`arm64`
wrapper 源码(`amrFileCodec.{h,mm}` + `AMRCodec.swift`)放在
`ylgamehall/Source/Audio/AMRFileCodec/``ylgamehall/Source/Audio/`
由项目同步组(PBXFileSystemSynchronizedRootGroup)自动加入编译,无需在
pbxproj 中显式声明。
+34
View File
@@ -0,0 +1,34 @@
/* ------------------------------------------------------------------
* Copyright (C) 2009 Martin Storsjo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
* -------------------------------------------------------------------
*/
#ifndef OPENCORE_AMRNB_INTERF_DEC_H
#define OPENCORE_AMRNB_INTERF_DEC_H
#ifdef __cplusplus
extern "C" {
#endif
void* Decoder_Interface_init(void);
void Decoder_Interface_exit(void* state);
void Decoder_Interface_Decode(void* state, const unsigned char* in, short* out, int bfi);
#ifdef __cplusplus
}
#endif
#endif
+50
View File
@@ -0,0 +1,50 @@
/* ------------------------------------------------------------------
* Copyright (C) 2009 Martin Storsjo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
* -------------------------------------------------------------------
*/
#ifndef OPENCORE_AMRNB_INTERF_ENC_H
#define OPENCORE_AMRNB_INTERF_ENC_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef AMRNB_WRAPPER_INTERNAL
/* Copied from enc/src/gsmamr_enc.h */
enum Mode {
MR475 = 0,/* 4.75 kbps */
MR515, /* 5.15 kbps */
MR59, /* 5.90 kbps */
MR67, /* 6.70 kbps */
MR74, /* 7.40 kbps */
MR795, /* 7.95 kbps */
MR102, /* 10.2 kbps */
MR122, /* 12.2 kbps */
MRDTX, /* DTX */
N_MODES /* Not Used */
};
#endif
void* Encoder_Interface_init(int dtx);
void Encoder_Interface_exit(void* state);
int Encoder_Interface_Encode(void* state, enum Mode mode, const short* speech, unsigned char* out, int forceSpeech);
#ifdef __cplusplus
}
#endif
#endif
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.