Files
youle_app_ios_v2/ylgamehall/Source/Audio/AMRCodec.swift
T
joywayer 5b38456c66 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 诊断。
2026-06-24 01:20:30 +08:00

70 lines
2.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 返回 0fopen 失败或写 0 帧)
case wavToAmrFailed(wav: String, amr: String)
/// DecodeAMRFileToWAVEFile 返回 0amr 文件不存在 / 头校验失败 / wav 创建失败)
case amrToWavFailed(amr: String, wav: String)
}
/// WAV → AMR。
///
/// 录音的 wav 必须满足 8kHz / 16bit / monoVoiceRecorderBaseVC.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)
}
}
}