为支撑 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 诊断。
70 lines
2.7 KiB
Swift
70 lines
2.7 KiB
Swift
//
|
||
// AMRCodec.swift
|
||
// ylgamehall
|
||
//
|
||
// AMR-NB ↔ WAV 文件级编解码的 Swift 薄 wrapper。
|
||
//
|
||
// ## 为何如此
|
||
//
|
||
// AMR-NB 是 H5 语音功能的跨端契约硬约束(iOS / Android / 旧版本客户端都按
|
||
// AMR-NB 解码 audiourl),格式不能换。详见 Vendor/OpenCoreAMR/README.md。
|
||
//
|
||
// 实际编解码由 daoqi 沿用的 `amrFileCodec.mm`(C++ 文件级操作 — 解析 WAV
|
||
// header、按 PCM 帧调 opencore-amr,写 AMR 文件头/帧)执行,本文件只是 Swift
|
||
// 侧的薄包装。
|
||
//
|
||
// - 不在 Swift 层重写 amrFileCodec:那几百行 C 风格 byte 流操作(FILE* /
|
||
// memcpy / 字节序解析)是 Swift 最别扭的领域,重写收益小风险大;daoqi 版
|
||
// 本已上线 N 年稳定运行
|
||
// - amrFileCodec.{h,mm} 放在 Source/Audio/AMRFileCodec/,由 synchronized
|
||
// group 自动编译;amrFileCodec.h 通过 ylgamehall-Bridging-Header.h 暴露
|
||
// 给 Swift
|
||
//
|
||
// ## 用途
|
||
//
|
||
// - 录音链路(prepareaudio):`wavToAmr(_:to:)` 把 AVAudioRecorder 录的 wav
|
||
// 转 amr 后上传七牛
|
||
// - 播放链路(mediaTypeAudio):`amrToWav(_:to:)` 把七牛下载的 amr 转 wav 后
|
||
// 交 AVAudioPlayer 播
|
||
//
|
||
// 契约:docs/H5-Native-Contract.md §3.1 [4][5] + §3.2 [9]
|
||
//
|
||
|
||
import Foundation
|
||
|
||
public enum AMRCodec {
|
||
|
||
public enum Error: Swift.Error, Sendable {
|
||
/// EncodeWAVEFileToAMRFile 返回 0(fopen 失败或写 0 帧)
|
||
case wavToAmrFailed(wav: String, amr: String)
|
||
/// DecodeAMRFileToWAVEFile 返回 0(amr 文件不存在 / 头校验失败 / wav 创建失败)
|
||
case amrToWavFailed(amr: String, wav: String)
|
||
}
|
||
|
||
/// WAV → AMR。
|
||
///
|
||
/// 录音的 wav 必须满足 8kHz / 16bit / mono(VoiceRecorderBaseVC.m 的
|
||
/// `getAudioRecorderSettingDict` 输出,msext 多年约定);否则
|
||
/// amrFileCodec 的 ReadPCMFrame 拿不到完整 160 sample 帧,返回 0。
|
||
///
|
||
/// - Throws: `AMRCodec.Error.wavToAmrFailed` 当底层 C 函数返回 0
|
||
public static func wavToAmr(_ wav: URL, to amr: URL) throws {
|
||
let frames = EncodeWAVEFileToAMRFile(wav.path, amr.path, 1, 16)
|
||
guard frames > 0 else {
|
||
throw Error.wavToAmrFailed(wav: wav.path, amr: amr.path)
|
||
}
|
||
}
|
||
|
||
/// AMR → WAV。
|
||
///
|
||
/// 解码后的 wav 是 8kHz / 16bit / mono,可直接交 AVAudioPlayer 播放。
|
||
///
|
||
/// - Throws: `AMRCodec.Error.amrToWavFailed` 当底层 C 函数返回 0
|
||
public static func amrToWav(_ amr: URL, to wav: URL) throws {
|
||
let frames = DecodeAMRFileToWAVEFile(amr.path, wav.path)
|
||
guard frames > 0 else {
|
||
throw Error.amrToWavFailed(amr: amr.path, wav: wav.path)
|
||
}
|
||
}
|
||
}
|