JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_sit前检查功能
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 引用 iOS 系统库,示例如下:
|
||||
* import { UIDevice } from "UIKit";
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
|
||||
|
||||
/* 引入 unierror.uts 文件中定义的变量 */
|
||||
import { MyApiFailImpl } from '../unierror';
|
||||
|
||||
/**
|
||||
* 引入三方库
|
||||
* [可选实现,按需引入]
|
||||
*
|
||||
* 在 iOS 平台引入三方库有以下两种方式:
|
||||
* 1、通过引入三方库framework 或者.a 等方式,需要将 .framework 放到 ./Frameworks 目录下,将.a 放到 ./Libs 目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-平台原生配置)
|
||||
* 2、通过 cocoaPods 方式引入,将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
|
||||
*
|
||||
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import:
|
||||
* 示例:import { LottieLoopMode } from 'Lottie'
|
||||
*/
|
||||
|
||||
/**
|
||||
* UTSiOS 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 异步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApi } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApi({
|
||||
* paramA: false,
|
||||
* complete: (res) => {
|
||||
* console.log(res)
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*/
|
||||
export const myApi : MyApi = function (options : MyApiOptions) {
|
||||
|
||||
if (options.paramA == true) {
|
||||
// 返回数据
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: true,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
options.success?.(res);
|
||||
options.complete?.(res);
|
||||
|
||||
} else {
|
||||
// 返回错误
|
||||
let failResult = new MyApiFailImpl(9010001);
|
||||
options.fail?.(failResult)
|
||||
options.complete?.(failResult)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApiSync } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApiSync(true);
|
||||
*
|
||||
*/
|
||||
export const myApiSync : MyApiSync = function (paramA : boolean) : MyApiResult {
|
||||
// 返回数据,根据插件功能获取实际的返回值
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: paramA,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更多插件开发的信息详见:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html
|
||||
*/
|
||||
|
||||
@@ -96,4 +96,5 @@ export function stopAudioRecord() : void {
|
||||
}
|
||||
}
|
||||
}
|
||||
export function preRequestRecordPermission(callback?: (granted: boolean) => void) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { StartAudioRecordOptions } from '../interface.uts';
|
||||
import { StartAudioRecordFailImpl, AudioErrorCode } from '../unierror.uts';
|
||||
|
||||
|
||||
import { UTSiOS } from "DCloudUTSFoundation"
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer, AVAudioCommonFormat } from "AVFoundation";
|
||||
import { NSData, NSMutableData } from "Foundation";
|
||||
import { Int, Int16 } from 'Swift';
|
||||
|
||||
// @argumentLabel("forBus")
|
||||
/**
|
||||
* 音频采集状态(全局变量,保证生命周期)
|
||||
*/
|
||||
let isRecording = false;
|
||||
/**
|
||||
* 音频引擎实例(全局变量,避免异步回调中被释放)
|
||||
*/
|
||||
let audioEngine: AVAudioEngine | null = null;
|
||||
/**
|
||||
* 音频输入节点(全局变量,避免被释放)
|
||||
*/
|
||||
let inputNode: AVAudioInputNode | null = null;
|
||||
// 全局复用缓冲区(避免每次创建新数组)
|
||||
let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
// 新增:缓存录制的所有Int16 PCM数据
|
||||
let recordedPCMData: UInt8[] = [];
|
||||
// 3. 保存完整PCM数据到文件
|
||||
const sampleRate = 16000.0; // 和采集时的目标格式一致
|
||||
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
||||
|
||||
/**
|
||||
* 启动音频采集(iOS端核心实现)
|
||||
* @param options 采集配置及回调
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
// 避免重复启动
|
||||
console.log('避免重复启动', isRecording);
|
||||
// if (isRecording) return;
|
||||
|
||||
try {
|
||||
recordedPCMData = [];
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.playAndRecord,
|
||||
mode=AVAudioSession.Mode.spokenAudio,
|
||||
options= AVAudioSession.CategoryOptions.duckOthers // 可选:其他音频静音
|
||||
));
|
||||
// UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.playAndRecord));
|
||||
// UTSiOS.try(audioSession.setMode(AVAudioSession.Mode.spokenAudio));
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
// 闭包内处理权限结果(异步执行)
|
||||
if (granted) {
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
console.log("硬件原生采样率:", inputFormat.sampleRate);
|
||||
console.log("硬件原生声道数:", inputFormat.channelCount);
|
||||
console.log("硬件原生格式:", inputFormat.commonFormat);
|
||||
|
||||
const targetFormat = AVAudioFormat.init(
|
||||
commonFormat= AVAudioCommonFormat.pcmFormatInt16, // 16位整型(文档枚举值)
|
||||
sampleRate= inputFormat.sampleRate, // 采样率(Double类型,符合文档定义)
|
||||
channels= inputFormat.channelCount, // 声道数(AVAudioChannelCount)
|
||||
interleaved= false // 是否交错(Bool,单声道无影响)
|
||||
);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 2048, // 缓冲区大小,常用 1024/2048
|
||||
format= targetFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const frameLen = buffer.frameLength as number;
|
||||
const bufferListRawPtr = buffer.audioBufferList;
|
||||
const audioBufferList = bufferListRawPtr.pointee; // 拿到结构体本身
|
||||
const audioBufferPtr = audioBufferList.mBuffers; // AudioBuffer指针
|
||||
// // console.log(audioBufferPtr.mData);
|
||||
const mData = audioBufferPtr.mData;
|
||||
|
||||
// const mDataByteSize:Int = 9600; // 你之前看到的字节数
|
||||
// const uint8Ptr = mData!.bindMemory(to= UInt8.self, capacity= mDataByteSize);
|
||||
// const pcmNormalArray: number[] = [];
|
||||
// for (let i:Int = 0; i < mDataByteSize; i++) {
|
||||
// // 直接读取指针的第i个字节,转为0-255的普通数字
|
||||
// pcmNormalArray.push(uint8Ptr[i] as number);
|
||||
// }
|
||||
// const halfLength:Int = 4800;
|
||||
// const firstHalf = pcmNormalArray.slice(0, halfLength);
|
||||
// const secondHalf = pcmNormalArray.slice(halfLength, mDataByteSize);
|
||||
// options.onFrame?.(firstHalf);
|
||||
// options.onFrame?.(secondHalf);
|
||||
|
||||
|
||||
// options.onFrame?.(pcmNormalArray);
|
||||
// let uint8 = new Uint8Array([4, 5, 8, 12]);
|
||||
// let array = Array.from(uint8);
|
||||
// console.log(array);
|
||||
// options.onFrame?.(array);
|
||||
// const audioBuffer = audioBufferPtr.pointee; // 拿到AudioBuffer结构体
|
||||
// console.log("音频数据字节数:", audioBuffer.mDataByteSize); // 比如4096(2048帧×2字节)
|
||||
// console.log("声道数:", audioBuffer.mNumberChannels); // 1
|
||||
// console.log("二进制数据指针:", audioBuffer.mData); // 指向16位PCM数据的内存地址
|
||||
|
||||
// const int16Ptr = buffer.int16ChannelData;
|
||||
// if (int16Ptr != null && frameLen > 0) {
|
||||
// const dataPtr = int16Ptr![0];
|
||||
// const CHUNK_FRAME_SIZE = frameLen / 2; // 16位=2字节/帧 → 512帧/分片
|
||||
// // 关键2:循环拆分 9600 字节为多个小分片
|
||||
// for (let startFrame:number = 0; startFrame < frameLen; startFrame += CHUNK_FRAME_SIZE) {
|
||||
// // 计算当前分片的结束帧(避免越界)
|
||||
// const endFrame = Math.min(startFrame + CHUNK_FRAME_SIZE, frameLen);
|
||||
// let _recordedPCMData: UInt8[] = [];
|
||||
// // 只处理当前分片的帧
|
||||
// for (let i: Int = startFrame as Int; i < endFrame; i++) {
|
||||
// const int16Value = dataPtr[i];
|
||||
// let byte1 = UInt8(int16Value & 0xFF);
|
||||
// let byte2 = UInt8((int16Value >> 8) & 0xFF);
|
||||
// _recordedPCMData.push(byte1);
|
||||
// _recordedPCMData.push(byte2);
|
||||
// }
|
||||
// options.onFrame?.(_recordedPCMData);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
} else {
|
||||
// 权限被拒绝:回调错误
|
||||
isRecording = false;
|
||||
}
|
||||
});
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
// 初始化失败回调错误
|
||||
const err = new StartAudioRecordFailImpl(9010002);
|
||||
options.onError?.(err);
|
||||
// 重置状态
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止音频采集(配套停止逻辑,保证功能完整)
|
||||
* @param options 回调配置
|
||||
*/
|
||||
|
||||
export function stopAudioRecord(): void {
|
||||
console.log('停止音频采集');
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
}
|
||||
const _audioEngine = audioEngine!
|
||||
if (_audioEngine.isRunning) {
|
||||
_audioEngine.stop();
|
||||
}
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
}
|
||||
const _inputNode = inputNode!
|
||||
_inputNode.removeTap(onBus= 0); // 移除音频回调
|
||||
console.log('移除音频回调');
|
||||
|
||||
// 停用音频会话
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(false));
|
||||
}catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
console.log('数组大小',recordedPCMData.length);
|
||||
const filePath = savePCMToFile(recordedPCMData, sampleRate, channels);
|
||||
console.log('filePath', filePath);
|
||||
inputNode = null;
|
||||
audioEngine = null; // 释放全局引擎
|
||||
isRecording = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Int16 PCM数组保存为文件(iOS沙盒路径)
|
||||
* @param pcmData Int16格式的PCM数据
|
||||
* @param sampleRate 采样率
|
||||
* @param channels 声道数
|
||||
* @returns 文件路径 | null
|
||||
*/
|
||||
function savePCMToFile(pcmData: UInt8[], sampleRate: number, channels: number): string | null {
|
||||
try {
|
||||
const dataPath = UTSiOS.getDataPath();
|
||||
console.log('dataPath', dataPath);
|
||||
if (dataPath === null) {
|
||||
throw new Error("获取应用数据路径失败");
|
||||
}
|
||||
// 2. 生成唯一文件名(避免覆盖)
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const filePath = dataPath + "/" + "recording_" + timestamp + ".pcm";
|
||||
const fileURL = NSURL.fileURL(withPath= filePath);
|
||||
console.log(fileURL);
|
||||
|
||||
// 3. 将Int16数组转为NSData(字节数据)
|
||||
const byteCount = pcmData.length * 2; // 每个Int16占2字节
|
||||
const byteArray = Uint8Array.from(recordedPCMData);
|
||||
const dataOptional = NSMutableData(length= byteArray.length);
|
||||
// const dataOptional = NSMutableData(length= byteCount);
|
||||
if (dataOptional === null) throw new Error("创建NSMutableData失败");
|
||||
const data = dataOptional!;
|
||||
|
||||
for (let j:Int = 0; j < recordedPCMData.length; j++) {
|
||||
// 获取当前字节值(UInt8)
|
||||
const byteValue = recordedPCMData[j];
|
||||
// 将单个字节写入 NSMutableData 的指定位置
|
||||
data.replaceBytes(in = NSMakeRange(j, 1), withBytes= UTSiOS.getPointer(byteValue), length= 1);
|
||||
}
|
||||
|
||||
|
||||
// 4. 写入文件(UTS兼容的Swift风格)
|
||||
const writeSuccess = data.write(toFile=filePath, atomically= true);
|
||||
// console.log(data);
|
||||
if (writeSuccess) {
|
||||
console.log("PCM文件保存成功:", filePath);
|
||||
return filePath;
|
||||
} else {
|
||||
throw new Error("文件写入失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存PCM文件出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import { StartAudioRecordFailImpl, AudioErrorCode } from '../unierror.uts';
|
||||
|
||||
|
||||
import { UTSiOS } from "DCloudUTSFoundation"
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer } from "AVFoundation";
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer, AVAudioCommonFormat } from "AVFoundation";
|
||||
import { NSData, NSMutableData } from "Foundation";
|
||||
import { Int, Int16 } from 'Swift';
|
||||
|
||||
// @argumentLabel("forBus")
|
||||
/**
|
||||
* 音频采集状态(全局变量,保证生命周期)
|
||||
*/
|
||||
@@ -21,6 +21,57 @@ let audioEngine: AVAudioEngine | null = null;
|
||||
let inputNode: AVAudioInputNode | null = null;
|
||||
// 全局复用缓冲区(避免每次创建新数组)
|
||||
let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
// 新增:缓存录制的所有Int16 PCM数据
|
||||
let recordedPCMData: UInt8[] = [];
|
||||
// 3. 保存完整PCM数据到文件
|
||||
const sampleRate = 16000.0; // 和采集时的目标格式一致
|
||||
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
||||
|
||||
// 新增:录音启动时间戳(毫秒)
|
||||
let recordStartTime: number | null = null;
|
||||
// 新增:实时时长定时器(用于更新UI)
|
||||
let durationTimer: any = null;
|
||||
|
||||
// 预申请权限函数(可在App启动时调用)
|
||||
/**
|
||||
* 预申请麦克风权限(规范版,含资源管理)
|
||||
* @param callback 权限申请结果回调(可选)
|
||||
*/
|
||||
export function preRequestRecordPermission(callback?: (granted: boolean) => void) {
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
|
||||
try {
|
||||
// 1. 配置基础音频会话(仅为权限申请,用record类别)
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.record,
|
||||
mode=AVAudioSession.Mode.default
|
||||
));
|
||||
|
||||
// 2. 激活会话(权限申请建议激活,避免部分设备弹窗不触发)
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
|
||||
// 3. 发起权限申请
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
console.log("预申请麦克风权限结果:", granted);
|
||||
|
||||
// 4. 权限申请完成后,立即停用会话(核心:释放资源)
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(
|
||||
false,
|
||||
options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation
|
||||
));
|
||||
} catch (e) {
|
||||
console.warn("预申请后停用音频会话失败:", e);
|
||||
}
|
||||
|
||||
// 5. 回调权限结果(供上层处理)
|
||||
callback?.(granted);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("预申请麦克风权限失败:", error);
|
||||
callback?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动音频采集(iOS端核心实现)
|
||||
@@ -30,79 +81,136 @@ let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
// 避免重复启动
|
||||
console.log('避免重复启动', isRecording);
|
||||
// if (isRecording) return;
|
||||
|
||||
if (isRecording) return;
|
||||
try {
|
||||
recordedPCMData = new Array<UInt8>();
|
||||
// 获取 iOS 系统的音频会话单例对象。
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
// 设置音频会话的类别、模式和附加选项,定义 App 的音频行为。
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.playAndRecord,
|
||||
mode=AVAudioSession.Mode.spokenAudio,
|
||||
options= AVAudioSession.CategoryOptions.duckOthers // 可选:其他音频静音
|
||||
));
|
||||
// UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.playAndRecord));
|
||||
// UTSiOS.try(audioSession.setMode(AVAudioSession.Mode.spokenAudio));
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
isRecording = true;
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
// 闭包内处理权限结果(异步执行)
|
||||
if (granted) {
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
if(!granted){
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(false, options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation));
|
||||
} catch (e) {
|
||||
console.warn("停用音频会话失败:", e);
|
||||
}
|
||||
// 2. 重置状态
|
||||
isRecording = false;
|
||||
const err = new StartAudioRecordFailImpl(9010002); // 麦克风权限被永久拒绝,请前往设置开启
|
||||
options.onError?.(err)
|
||||
} else {
|
||||
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
console.log("硬件原生采样率:", inputFormat.sampleRate);
|
||||
console.log("硬件原生声道数:", inputFormat.channelCount);
|
||||
console.log("硬件原生格式:", inputFormat.commonFormat);
|
||||
recordStartTime = new Date().getTime();
|
||||
// 1. 生成录音唯一标识(UUID,方便多录音实例管理)
|
||||
const recordId = `${new Date().getTime()}-${Math.floor(Math.random() * 10000)}`;
|
||||
options.onStart?.({
|
||||
recordId: recordId, // 唯一标识
|
||||
sampleRate: inputFormat.sampleRate, // 采样率
|
||||
startTime: recordStartTime, // 启动时间戳
|
||||
});
|
||||
const targetFormat = AVAudioFormat.init(
|
||||
commonFormat= AVAudioCommonFormat.pcmFormatInt16, // 16位整型(文档枚举值)
|
||||
sampleRate= inputFormat.sampleRate, // 采样率(Double类型,符合文档定义)
|
||||
channels= inputFormat.channelCount, // 声道数(AVAudioChannelCount)
|
||||
interleaved= false // 是否交错(Bool,单声道无影响)
|
||||
);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 2048, // 缓冲区大小,常用 1024/2048
|
||||
format= targetFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const frameLen = buffer.frameLength as number;
|
||||
const bufferListRawPtr = buffer.audioBufferList;
|
||||
const audioBufferList = bufferListRawPtr.pointee; // 拿到结构体本身
|
||||
const audioBufferPtr = audioBufferList.mBuffers; // AudioBuffer指针
|
||||
// // console.log(audioBufferPtr.mData);
|
||||
const mData = audioBufferPtr.mData;
|
||||
|
||||
// const mDataByteSize:Int = 9600; // 你之前看到的字节数
|
||||
// const uint8Ptr = mData!.bindMemory(to= UInt8.self, capacity= mDataByteSize);
|
||||
// const pcmNormalArray: number[] = [];
|
||||
// for (let i:Int = 0; i < mDataByteSize; i++) {
|
||||
// // 直接读取指针的第i个字节,转为0-255的普通数字
|
||||
// pcmNormalArray.push(uint8Ptr[i] as number);
|
||||
// }
|
||||
// const halfLength:Int = 4800;
|
||||
// const firstHalf = pcmNormalArray.slice(0, halfLength);
|
||||
// const secondHalf = pcmNormalArray.slice(halfLength, mDataByteSize);
|
||||
// options.onFrame?.(firstHalf);
|
||||
// options.onFrame?.(secondHalf);
|
||||
|
||||
|
||||
// options.onFrame?.(pcmNormalArray);
|
||||
// let uint8 = new Uint8Array([4, 5, 8, 12]);
|
||||
// let array = Array.from(uint8);
|
||||
// console.log(array);
|
||||
// options.onFrame?.(array);
|
||||
// const audioBuffer = audioBufferPtr.pointee; // 拿到AudioBuffer结构体
|
||||
// console.log("音频数据字节数:", audioBuffer.mDataByteSize); // 比如4096(2048帧×2字节)
|
||||
// console.log("声道数:", audioBuffer.mNumberChannels); // 1
|
||||
// console.log("二进制数据指针:", audioBuffer.mData); // 指向16位PCM数据的内存地址
|
||||
|
||||
// const int16Ptr = buffer.int16ChannelData;
|
||||
// if (int16Ptr != null && frameLen > 0) {
|
||||
// const dataPtr = int16Ptr![0];
|
||||
// const CHUNK_FRAME_SIZE = frameLen / 2; // 16位=2字节/帧 → 512帧/分片
|
||||
// // 关键2:循环拆分 9600 字节为多个小分片
|
||||
// for (let startFrame:number = 0; startFrame < frameLen; startFrame += CHUNK_FRAME_SIZE) {
|
||||
// // 计算当前分片的结束帧(避免越界)
|
||||
// const endFrame = Math.min(startFrame + CHUNK_FRAME_SIZE, frameLen);
|
||||
// let _recordedPCMData: UInt8[] = [];
|
||||
// // 只处理当前分片的帧
|
||||
// for (let i: Int = startFrame as Int; i < endFrame; i++) {
|
||||
// const int16Value = dataPtr[i];
|
||||
// let byte1 = UInt8(int16Value & 0xFF);
|
||||
// let byte2 = UInt8((int16Value >> 8) & 0xFF);
|
||||
// _recordedPCMData.push(byte1);
|
||||
// _recordedPCMData.push(byte2);
|
||||
// }
|
||||
// options.onFrame?.(_recordedPCMData);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 1024, // 缓冲区大小,常用 1024/2048
|
||||
format= inputFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const floatData = buffer.floatChannelData;
|
||||
const channelData = floatData![0];
|
||||
const frameLength = buffer.frameLength as number;
|
||||
const bytesPerSample = 2; // 16位整型=2字节/样本
|
||||
const totalBytes = frameLength * bytesPerSample;
|
||||
|
||||
console.log(channelData);
|
||||
console.log(totalBytes);
|
||||
const pcmData = NSMutableData.dataWithCapacity(20); // 可变Data,支持写入
|
||||
|
||||
// const data = new Float32Array(int32Array.byteLength);
|
||||
// data.set(int32Array.buffer, 1); // 内存块拷贝,替代逐字节写入
|
||||
|
||||
options.onFrame?.(pcmData);
|
||||
// if (floatData !== null) {
|
||||
// // 单声道取第0个通道(立体声可取[0]左声道、[1]右声道)
|
||||
// const channelData = floatData[0];
|
||||
|
||||
// options.onFrame?.(channelData)
|
||||
// }
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
} else {
|
||||
// 权限被拒绝:回调错误
|
||||
isRecording = false;
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
isRecording = true;
|
||||
|
||||
} catch (error) {
|
||||
console.log('报错了', error);
|
||||
// 初始化失败回调错误
|
||||
const err = new StartAudioRecordFailImpl(9010002);
|
||||
options.onError?.(err);
|
||||
@@ -117,6 +225,7 @@ export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
*/
|
||||
|
||||
export function stopAudioRecord(): void {
|
||||
console.log('停止音频采集');
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
@@ -131,7 +240,7 @@ export function stopAudioRecord(): void {
|
||||
}
|
||||
const _inputNode = inputNode!
|
||||
_inputNode.removeTap(onBus= 0); // 移除音频回调
|
||||
|
||||
console.log('移除音频回调');
|
||||
|
||||
// 停用音频会话
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
@@ -141,8 +250,62 @@ export function stopAudioRecord(): void {
|
||||
}catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
// console.log('数组大小',recordedPCMData.length);
|
||||
// const filePath = savePCMToFile(recordedPCMData, sampleRate, channels);
|
||||
// console.log('filePath', filePath);
|
||||
inputNode = null;
|
||||
audioEngine = null; // 释放全局引擎
|
||||
isRecording = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Int16 PCM数组保存为文件(iOS沙盒路径)
|
||||
* @param pcmData Int16格式的PCM数据
|
||||
* @param sampleRate 采样率
|
||||
* @param channels 声道数
|
||||
* @returns 文件路径 | null
|
||||
*/
|
||||
function savePCMToFile(pcmData: UInt8[], sampleRate: number, channels: number): string | null {
|
||||
try {
|
||||
const dataPath = UTSiOS.getDataPath();
|
||||
console.log('dataPath', dataPath);
|
||||
if (dataPath === null) {
|
||||
throw new Error("获取应用数据路径失败");
|
||||
}
|
||||
// 2. 生成唯一文件名(避免覆盖)
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const filePath = dataPath + "/" + "recording_" + timestamp + ".pcm";
|
||||
const fileURL = NSURL.fileURL(withPath= filePath);
|
||||
console.log(fileURL);
|
||||
|
||||
// 3. 将Int16数组转为NSData(字节数据)
|
||||
const byteCount = pcmData.length * 2; // 每个Int16占2字节
|
||||
const byteArray = Uint8Array.from(recordedPCMData);
|
||||
const dataOptional = NSMutableData(length= byteArray.length);
|
||||
// const dataOptional = NSMutableData(length= byteCount);
|
||||
if (dataOptional === null) throw new Error("创建NSMutableData失败");
|
||||
const data = dataOptional!;
|
||||
|
||||
for (let j:Int = 0; j < recordedPCMData.length; j++) {
|
||||
// 获取当前字节值(UInt8)
|
||||
const byteValue = recordedPCMData[j];
|
||||
// 将单个字节写入 NSMutableData 的指定位置
|
||||
data.replaceBytes(in = NSMakeRange(j, 1), withBytes= UTSiOS.getPointer(byteValue), length= 1);
|
||||
}
|
||||
|
||||
|
||||
// 4. 写入文件(UTS兼容的Swift风格)
|
||||
const writeSuccess = data.write(toFile=filePath, atomically= true);
|
||||
// console.log(data);
|
||||
if (writeSuccess) {
|
||||
console.log("PCM文件保存成功:", filePath);
|
||||
return filePath;
|
||||
} else {
|
||||
throw new Error("文件写入失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存PCM文件出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
export type AudioStartRes = {
|
||||
recordId ?: string; // 录音唯一标识(方便多录音实例管理)
|
||||
sampleRate ?: number; // 采样率(如16000)
|
||||
format ?: string; // 录音格式(如pcm/mp3)
|
||||
startTime : number; // 开启时间戳(毫秒)
|
||||
};
|
||||
|
||||
@@ -51,7 +50,7 @@ export type StartAudioRecordOptions = {
|
||||
onVolume ?: (volume : number) => void; // 实时音量回调(如每秒5次,按设备采集频率)
|
||||
|
||||
// 【一次性状态回调】- 特定节点仅触发一次
|
||||
onStart ?: (res : AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
|
||||
onStart ?: (res: AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
|
||||
onError ?: (res : AudioErrorRes) => void; // 录音错误回调(任意阶段出错都触发,仅1次)
|
||||
onStop ?: () => void; // 录音结束的纯状态回调(仅通知结束,无返回,1次)
|
||||
onFile ?: (res : AudioFileRes) => void; // 录音结束后,返回录音文件的回调(1次,核心)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export function startAudioRecord(callback) {}
|
||||
export function stopAudioRecord() {}
|
||||
export function stopAudioRecord() {}
|
||||
export function preRequestRecordPermission() {}
|
||||
Reference in New Issue
Block a user