diff --git a/.gitignore b/.gitignore index 710e163..ad20358 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ DerivedData/ *.so.[0-9]* *.la *.a +# Vendor 下的 .a 是项目依赖(如 libWeChatSDK.a / libopencore-amrnb.a),必须跟踪 +# —— 否则其他人 clone 后链接失败。覆盖上面 *.a 的通配规则。 +!Vendor/**/*.a *.pyc *.swp *~.nib diff --git a/Vendor/OpenCoreAMR/README.md b/Vendor/OpenCoreAMR/README.md new file mode 100644 index 0000000..fdff4cf --- /dev/null +++ b/Vendor/OpenCoreAMR/README.md @@ -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 fat:i386 + 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-20:opencore-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,跨 + 编 iOS(autoconf)容易踩坑,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 Architectures(iphonesimulator):`arm64` + +wrapper 源码(`amrFileCodec.{h,mm}` + `AMRCodec.swift`)放在 +`ylgamehall/Source/Audio/AMRFileCodec/` 与 `ylgamehall/Source/Audio/`, +由项目同步组(PBXFileSystemSynchronizedRootGroup)自动加入编译,无需在 +pbxproj 中显式声明。 diff --git a/Vendor/OpenCoreAMR/v0.1.x/interf_dec.h b/Vendor/OpenCoreAMR/v0.1.x/interf_dec.h new file mode 100755 index 0000000..98051f4 --- /dev/null +++ b/Vendor/OpenCoreAMR/v0.1.x/interf_dec.h @@ -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 diff --git a/Vendor/OpenCoreAMR/v0.1.x/interf_enc.h b/Vendor/OpenCoreAMR/v0.1.x/interf_enc.h new file mode 100755 index 0000000..b89b0c7 --- /dev/null +++ b/Vendor/OpenCoreAMR/v0.1.x/interf_enc.h @@ -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 diff --git a/Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a b/Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a new file mode 100755 index 0000000..90cfc91 Binary files /dev/null and b/Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a differ diff --git a/Vendor/WechatSDK/v1.8.2/libWeChatSDK.a b/Vendor/WechatSDK/v1.8.2/libWeChatSDK.a new file mode 100755 index 0000000..dbad6eb Binary files /dev/null and b/Vendor/WechatSDK/v1.8.2/libWeChatSDK.a differ diff --git a/ylgamehall.xcodeproj/project.pbxproj b/ylgamehall.xcodeproj/project.pbxproj index 9f9327f..1ad7a07 100644 --- a/ylgamehall.xcodeproj/project.pbxproj +++ b/ylgamehall.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 1A2955002FEA48CD009E703E /* libWeChatSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A2954F92FEA0B59009E703E /* libWeChatSDK.a */; }; 1A2955012FEA48D9009E703E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A2954FA2FEA0BA8009E703E /* libz.tbd */; }; 1A2955022FEA48E4009E703E /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A2954FB2FEA0BB9009E703E /* libsqlite3.tbd */; }; + 1A2955102FEAF514009E703E /* libopencore-amrnb.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A29550F2FEAF514009E703E /* libopencore-amrnb.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +33,7 @@ 1A2954FB2FEA0BB9009E703E /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 1A2954FC2FEA0BCE009E703E /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 1A2954FE2FEA0BD8009E703E /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 1A29550F2FEAF514009E703E /* libopencore-amrnb.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libopencore-amrnb.a"; path = "Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a"; sourceTree = ""; }; 1A714BD22FE8047A00913338 /* ylgamehall.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ylgamehall.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -70,6 +72,7 @@ 1A2954EF2FE9BA6E009E703E /* CoreTelephony.framework in Frameworks */, 1A2954E32FE99C53009E703E /* AMapFoundationKit.framework in Frameworks */, 1A2954DA2FE97D61009E703E /* Qiniu in Frameworks */, + 1A2955102FEAF514009E703E /* libopencore-amrnb.a in Frameworks */, 1A2954ED2FE9BA49009E703E /* SystemConfiguration.framework in Frameworks */, 1A2954FD2FEA0BCE009E703E /* Security.framework in Frameworks */, 1A2954F12FE9BA7C009E703E /* ExternalAccessory.framework in Frameworks */, @@ -82,6 +85,7 @@ 1A2954DB2FE99B2F009E703E /* Frameworks */ = { isa = PBXGroup; children = ( + 1A29550F2FEAF514009E703E /* libopencore-amrnb.a */, 1A2954FE2FEA0BD8009E703E /* CFNetwork.framework */, 1A2954FC2FEA0BCE009E703E /* Security.framework */, 1A2954FB2FEA0BB9009E703E /* libsqlite3.tbd */, @@ -212,7 +216,11 @@ "$(PROJECT_DIR)/Vendor/AMap", ); GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2", + "$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x", + "$(PROJECT_DIR)/ylgamehall/Source/Audio/AMRFileCodec", + ); INFOPLIST_FILE = ylgamehall/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -227,6 +235,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2", + "$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x", ); MARKETING_VERSION = 1.0; OTHER_LDFLAGS = "-ObjC"; @@ -256,7 +265,11 @@ "$(PROJECT_DIR)/Vendor/AMap", ); GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2", + "$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x", + "$(PROJECT_DIR)/ylgamehall/Source/Audio/AMRFileCodec", + ); INFOPLIST_FILE = ylgamehall/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -271,6 +284,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2", + "$(PROJECT_DIR)/Vendor/OpenCoreAMR/v0.1.x", ); MARKETING_VERSION = 1.0; OTHER_LDFLAGS = "-ObjC"; diff --git a/ylgamehall/Source/Audio/AMRCodec.swift b/ylgamehall/Source/Audio/AMRCodec.swift new file mode 100644 index 0000000..d4f7940 --- /dev/null +++ b/ylgamehall/Source/Audio/AMRCodec.swift @@ -0,0 +1,69 @@ +// +// 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) + } + } +} diff --git a/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.h b/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.h new file mode 100755 index 0000000..4a98a4b --- /dev/null +++ b/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.h @@ -0,0 +1,90 @@ +// +// amrFileCodec.h +// amrDemoForiOS +// +// Created by Tang Xiaoping on 9/27/11. +// Copyright 2011 test. All rights reserved. +// +#ifndef amrFileCodec_h +#define amrFileCodec_h +#include +#include +#include +#include "interf_dec.h" +#include "interf_enc.h" + +#define AMR_MAGIC_NUMBER "#!AMR\n" + +#define PCM_FRAME_SIZE 160 // 8khz 8000*0.02=160 +#define MAX_AMR_FRAME_SIZE 32 +#define AMR_FRAME_COUNT_PER_SECOND 50 + +typedef struct +{ + char chChunkID[4]; + int nChunkSize; +}XCHUNKHEADER; + +typedef struct +{ + short nFormatTag; + short nChannels; + int nSamplesPerSec; + int nAvgBytesPerSec; + short nBlockAlign; + short nBitsPerSample; +}WAVEFORMAT; + +typedef struct +{ + short nFormatTag; + short nChannels; + int nSamplesPerSec; + int nAvgBytesPerSec; + short nBlockAlign; + short nBitsPerSample; + short nExSize; +}WAVEFORMATX; + +typedef struct +{ + char chRiffID[4]; + int nRiffSize; + char chRiffFormat[4]; +}RIFFHEADER; + +typedef struct +{ + char chFmtID[4]; + int nFmtSize; + WAVEFORMAT wf; +}FMTBLOCK; + +// extern "C" 在 ObjC++ (.mm) 编译时禁用 C++ name mangling, +// 保证 .o 里函数符号是 _EncodeWAVEFileToAMRFile / _DecodeAMRFileToWAVEFile(C 链接), +// 与 Swift bridging-header 从 amrFileCodec.h 推导出的 C 函数签名匹配。 +// +// daoqi msext 原版无 extern "C" 包裹,因为调用方 VoiceConverter.mm 也是 ObjC++, +// 双方都按 C++ name mangling 链接能匹配上。新外壳从 Swift 通过 bridging-header +// 调用,Swift 按 C 函数解析,必须显式标 C linkage。 +#ifdef __cplusplus +extern "C" { +#endif + +// WAVE音频采样频率是8khz +// 音频样本单元数 = 8000*0.02 = 160 (由采样频率决定) +// 声道数 1 : 160 +// 2 : 160*2 = 320 +// bps决定样本(sample)大小 +// bps = 8 --> 8位 unsigned char +// 16 --> 16位 unsigned short +int EncodeWAVEFileToAMRFile(const char* pchWAVEFilename, const char* pchAMRFileName, int nChannels, int nBitsPerSample); + +// 将AMR文件解码成WAVE文件 +int DecodeAMRFileToWAVEFile(const char* pchAMRFileName, const char* pchWAVEFilename); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.mm b/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.mm new file mode 100755 index 0000000..e6151cf --- /dev/null +++ b/ylgamehall/Source/Audio/AMRFileCodec/amrFileCodec.mm @@ -0,0 +1,393 @@ +// +// amrFileCodec.cpp +// amrDemoForiOS +// +// Created by Tang Xiaoping on 9/27/11. +// Copyright 2011 test. All rights reserved. +// + +#include "amrFileCodec.h" +int amrEncodeMode[] = {4750, 5150, 5900, 6700, 7400, 7950, 10200, 12200}; // amr 编码方式 +// 从WAVE文件中跳过WAVE文件头,直接到PCM音频数据 +void SkipToPCMAudioData(FILE* fpwave) +{ + RIFFHEADER riff; + FMTBLOCK fmt; + XCHUNKHEADER chunk; + WAVEFORMATX wfx; + int bDataBlock = 0; + + // 1. 读RIFF头 + fread(&riff, 1, sizeof(RIFFHEADER), fpwave); + + // 2. 读FMT块 - 如果 fmt.nFmtSize>16 说明需要还有一个附属大小没有读 + fread(&chunk, 1, sizeof(XCHUNKHEADER), fpwave); + if ( chunk.nChunkSize>16 ) + { + fread(&wfx, 1, sizeof(WAVEFORMATX), fpwave); + } + else + { + memcpy(fmt.chFmtID, chunk.chChunkID, 4); + fmt.nFmtSize = chunk.nChunkSize; + fread(&fmt.wf, 1, sizeof(WAVEFORMAT), fpwave); + } + + // 3.转到data块 - 有些还有fact块等。 + while(!bDataBlock) + { + fread(&chunk, 1, sizeof(XCHUNKHEADER), fpwave); + if ( !memcmp(chunk.chChunkID, "data", 4) ) + { + bDataBlock = 1; + break; + } + // 因为这个不是data块,就跳过块数据 + fseek(fpwave, chunk.nChunkSize, SEEK_CUR); + } +} + +// 从WAVE文件读一个完整的PCM音频帧 +// 返回值: 0-错误 >0: 完整帧大小 +int ReadPCMFrame(short speech[], FILE* fpwave, int nChannels, int nBitsPerSample) +{ + int nRead = 0; + int x = 0, y=0; +// unsigned short ush1=0, ush2=0, ush=0; + + // 原始PCM音频帧数据 + unsigned char pcmFrame_8b1[PCM_FRAME_SIZE]; + unsigned char pcmFrame_8b2[PCM_FRAME_SIZE<<1]; + unsigned short pcmFrame_16b1[PCM_FRAME_SIZE]; + unsigned short pcmFrame_16b2[PCM_FRAME_SIZE<<1]; + + if (nBitsPerSample==8 && nChannels==1) + { + + nRead = fread(pcmFrame_8b1, (nBitsPerSample/8), PCM_FRAME_SIZE*nChannels, fpwave); + + for(x=0; x> 1; + //speech[y] = (short)((short)ush << 7); + } + } + else + if (nBitsPerSample==16 && nChannels==1) + { + nRead = fread(pcmFrame_16b1, (nBitsPerSample/8), PCM_FRAME_SIZE*nChannels, fpwave); + for(x=0; x> 1; + } + } + + // 如果读到的数据不是一个完整的PCM帧, 就返回0 + if (nRead 8位 unsigned char +// 16 --> 16位 unsigned short +int EncodeWAVEFileToAMRFile(const char* pchWAVEFilename, const char* pchAMRFileName, int nChannels, int nBitsPerSample) +{ + FILE* fpwave; + FILE* fpamr; + + /* input speech vector */ + short speech[160]; + + /* counters */ + int byte_counter, frames = 0, bytes = 0; + + /* pointer to encoder state structure */ + void *enstate; + + /* requested mode */ + enum Mode req_mode = MR122; + int dtx = 0; + + /* bitstream filetype */ + unsigned char amrFrame[MAX_AMR_FRAME_SIZE]; + + fpwave = fopen(pchWAVEFilename, "rb"); + if (fpwave == NULL) + { + return 0; + } + + // 创建并初始化amr文件 + fpamr = fopen(pchAMRFileName, "wb"); + if (fpamr == NULL) + { + fclose(fpwave); + return 0; + } + /* write magic number to indicate single channel AMR file storage format */ + bytes = fwrite(AMR_MAGIC_NUMBER, sizeof(char), strlen(AMR_MAGIC_NUMBER), fpamr); + + /* skip to pcm audio data*/ + SkipToPCMAudioData(fpwave); + + enstate = Encoder_Interface_init(dtx); + + while(1) + { + // read one pcm frame + if (!ReadPCMFrame(speech, fpwave, nChannels, nBitsPerSample)) break; + + frames++; + + /* call encoder */ + byte_counter = Encoder_Interface_Encode(enstate, req_mode, speech, amrFrame, 0); + + bytes += byte_counter; + fwrite(amrFrame, sizeof (unsigned char), byte_counter, fpamr ); + } + + Encoder_Interface_exit(enstate); + + fclose(fpamr); + fclose(fpwave); + + return frames; +} + + + + +#pragma mark - Decode +//decode +void WriteWAVEFileHeader(FILE* fpwave, int nFrame) +{ + char tag[10] = ""; + + // 1. 写RIFF头 + RIFFHEADER riff; + strcpy(tag, "RIFF"); + memcpy(riff.chRiffID, tag, 4); + riff.nRiffSize = 4 // WAVE + + sizeof(XCHUNKHEADER) // fmt + + sizeof(WAVEFORMATX) // WAVEFORMATX + + sizeof(XCHUNKHEADER) // DATA + + nFrame*160*sizeof(short); // + strcpy(tag, "WAVE"); + memcpy(riff.chRiffFormat, tag, 4); + fwrite(&riff, 1, sizeof(RIFFHEADER), fpwave); + + // 2. 写FMT块 + XCHUNKHEADER chunk; + WAVEFORMATX wfx; + strcpy(tag, "fmt "); + memcpy(chunk.chChunkID, tag, 4); + chunk.nChunkSize = sizeof(WAVEFORMATX); + fwrite(&chunk, 1, sizeof(XCHUNKHEADER), fpwave); + memset(&wfx, 0, sizeof(WAVEFORMATX)); + wfx.nFormatTag = 1; + wfx.nChannels = 1; // 单声道 + wfx.nSamplesPerSec = 8000; // 8khz + wfx.nAvgBytesPerSec = 16000; + wfx.nBlockAlign = 2; + wfx.nBitsPerSample = 16; // 16位 + fwrite(&wfx, 1, sizeof(WAVEFORMATX), fpwave); + + // 3. 写data块头 + strcpy(tag, "data"); + memcpy(chunk.chChunkID, tag, 4); + chunk.nChunkSize = nFrame*160*sizeof(short); + fwrite(&chunk, 1, sizeof(XCHUNKHEADER), fpwave); +} + +const int myround(const double x) +{ + return((int)(x+0.5)); +} + +// 根据帧头计算当前帧大小 +int caclAMRFrameSize(unsigned char frameHeader) +{ + int mode; + int temp1 = 0; + int temp2 = 0; + int frameSize; + + temp1 = frameHeader; + + // 编码方式编号 = 帧头的3-6位 + temp1 &= 0x78; // 0111-1000 + temp1 >>= 3; + + mode = amrEncodeMode[temp1]; + + // 计算amr音频数据帧大小 + // 原理: amr 一帧对应20ms,那么一秒有50帧的音频数据 + temp2 = myround((double)(((double)mode / (double)AMR_FRAME_COUNT_PER_SECOND) / (double)8)); + + frameSize = myround((double)temp2 + 0.5); + return frameSize; +} + +// 读第一个帧 - (参考帧) +// 返回值: 0-出错; 1-正确 +int ReadAMRFrameFirst(FILE* fpamr, unsigned char frameBuffer[], int* stdFrameSize, unsigned char* stdFrameHeader) +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsizeof-array-argument" +#pragma clang diagnostic ignored "-Wsizeof-pointer-memaccess" + memset(frameBuffer, 0, sizeof(frameBuffer)); +#pragma clang diagnostic pop + + // 先读帧头 + fread(stdFrameHeader, 1, sizeof(unsigned char), fpamr); + if (feof(fpamr)) return 0; + + // 根据帧头计算帧大小 + *stdFrameSize = caclAMRFrameSize(*stdFrameHeader); + + // 读首帧 + frameBuffer[0] = *stdFrameHeader; + fread(&(frameBuffer[1]), 1, (*stdFrameSize-1)*sizeof(unsigned char), fpamr); + if (feof(fpamr)) return 0; + + return 1; +} + +// 返回值: 0-出错; 1-正确 +int ReadAMRFrame(FILE* fpamr, unsigned char frameBuffer[], int stdFrameSize, unsigned char stdFrameHeader) +{ + int bytes = 0; + unsigned char frameHeader; // 帧头 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsizeof-array-argument" +#pragma clang diagnostic ignored "-Wsizeof-pointer-memaccess" + memset(frameBuffer, 0, sizeof(frameBuffer)); +#pragma clang diagnostic pop + + // 读帧头 + // 如果是坏帧(不是标准帧头),则继续读下一个字节,直到读到标准帧头 + while(1) + { + bytes = fread(&frameHeader, 1, sizeof(unsigned char), fpamr); + if (feof(fpamr)) return 0; + if (frameHeader == stdFrameHeader) break; + } + + // 读该帧的语音数据(帧头已经读过) + frameBuffer[0] = frameHeader; + bytes = fread(&(frameBuffer[1]), 1, (stdFrameSize-1)*sizeof(unsigned char), fpamr); + if (feof(fpamr)) return 0; + + return 1; +} + +// 将AMR文件解码成WAVE文件 +int DecodeAMRFileToWAVEFile(const char* pchAMRFileName, const char* pchWAVEFilename) +{ + + + FILE* fpamr = NULL; + FILE* fpwave = NULL; + char magic[8]; + void * destate; + int nFrameCount = 0; + int stdFrameSize; + unsigned char stdFrameHeader; + + unsigned char amrFrame[MAX_AMR_FRAME_SIZE]; + short pcmFrame[PCM_FRAME_SIZE]; + +// NSString * path = [[NSBundle mainBundle] pathForResource: @"test" ofType: @"amr"]; +// fpamr = fopen([path cStringUsingEncoding:NSASCIIStringEncoding], "rb"); + fpamr = fopen(pchAMRFileName, "rb"); + + if ( fpamr==NULL ) return 0; + + // 检查amr文件头 + fread(magic, sizeof(char), strlen(AMR_MAGIC_NUMBER), fpamr); + if (strncmp(magic, AMR_MAGIC_NUMBER, strlen(AMR_MAGIC_NUMBER))) + { + fclose(fpamr); + return 0; + } + + // 创建并初始化WAVE文件 +// NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); +// NSString *documentPath = [paths objectAtIndex:0]; +// NSString *docFilePath = [documentPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%s", pchWAVEFilename]]; +// NSLog(@"documentPath=%@", documentPath); +// +// fpwave = fopen([docFilePath cStringUsingEncoding:NSASCIIStringEncoding], "wb"); + fpwave = fopen(pchWAVEFilename,"wb"); + + WriteWAVEFileHeader(fpwave, nFrameCount); + + /* init decoder */ + destate = Decoder_Interface_init(); + + // 读第一帧 - 作为参考帧 + memset(amrFrame, 0, sizeof(amrFrame)); + memset(pcmFrame, 0, sizeof(pcmFrame)); + ReadAMRFrameFirst(fpamr, amrFrame, &stdFrameSize, &stdFrameHeader); + + // 解码一个AMR音频帧成PCM数据 + Decoder_Interface_Decode(destate, amrFrame, pcmFrame, 0); + nFrameCount++; + fwrite(pcmFrame, sizeof(short), PCM_FRAME_SIZE, fpwave); + + // 逐帧解码AMR并写到WAVE文件里 + while(1) + { + memset(amrFrame, 0, sizeof(amrFrame)); + memset(pcmFrame, 0, sizeof(pcmFrame)); + if (!ReadAMRFrame(fpamr, amrFrame, stdFrameSize, stdFrameHeader)) break; + + // 解码一个AMR音频帧成PCM数据 (8k-16b-单声道) + Decoder_Interface_Decode(destate, amrFrame, pcmFrame, 0); + nFrameCount++; + fwrite(pcmFrame, sizeof(short), PCM_FRAME_SIZE, fpwave); + } + printf("frame = %d\n", nFrameCount); + Decoder_Interface_exit(destate); + + fclose(fpwave); + + // 重写WAVE文件头 +// fpwave = fopen([docFilePath cStringUsingEncoding:NSASCIIStringEncoding], "r+"); + fpwave = fopen(pchWAVEFilename, "r+"); + WriteWAVEFileHeader(fpwave, nFrameCount); + fclose(fpwave); + + return nFrameCount; +} \ No newline at end of file diff --git a/ylgamehall/ylgamehall-Bridging-Header.h b/ylgamehall/ylgamehall-Bridging-Header.h index 633f339..2abacb2 100644 --- a/ylgamehall/ylgamehall-Bridging-Header.h +++ b/ylgamehall/ylgamehall-Bridging-Header.h @@ -4,12 +4,20 @@ // // Objective-C → Swift 桥接头文件。 // -// 当前唯一用途:微信 OpenSDK 1.8.2(与 daoqi msext 同款,避免 2.x 强制 -// Universal Link 的运维负担)。1.x SDK 是 .a 静态库 + 头文件,没有 Swift -// module(不能 `import WechatOpenSDK`),必须通过 Bridging Header 导入。 +// 当前用途: +// 1. 微信 OpenSDK 1.8.2(.a + .h,无 Swift module) +// 2. AMR-NB ↔ WAV 文件级编解码 wrapper(amrFileCodec.{h,mm} + libopencore-amrnb.a) // -// 详见 docs/SDK-Integration-Guide.md §A.1 / Vendor/WechatSDK/README.md。 +// 详见: +// - docs/SDK-Integration-Guide.md §A.1 +// - Vendor/WechatSDK/README.md +// - Vendor/OpenCoreAMR/README.md // +// 微信 OpenSDK 1.8.2 #import "WXApi.h" #import "WXApiObject.h" + +// AMR-NB ↔ WAV 文件级编解码(Source/Audio/AMRFileCodec/,由 synchronized +// group 自动编译;Swift 侧通过 AMRCodec.swift 调用其 C 函数) +#import "amrFileCodec.h"