Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop

This commit is contained in:
杨航
2026-03-13 09:12:35 +08:00
5 changed files with 17 additions and 414 deletions
+5 -6
View File
@@ -287,7 +287,6 @@
tempText.value = '';
allText.value = '';
ossKey.value = '';
pressTimer = setTimeout(() => {
if (!isRecording.value) {
console.log('定时器被取消还是进入了');
@@ -349,7 +348,7 @@
tempText.value = '';
allText.value = allText.value + body;
} else if (msgType === MessageType.TIP_MESSAGE) {
tempText.value += body as string;
tempText.value = body as string;
} else if (msgType === MessageType.CONTROL_CMD) {
// 返回结束消息
const controlBody = body as ControlBody;
@@ -418,17 +417,17 @@
}
}
});
}, 140);
}, 120);
};
// 结束录音
const closePopup = () => {
startRecordingTimer(false);
isRecording.value = false;
popupRef.value.close();
};
// 结束ws
const closeWS = () => {
startRecordingTimer(false);
if (ws.value) {
ws.value.close({
code: 1000,
@@ -484,11 +483,11 @@
status.value = 'voice_send_ing'; //主状态设置为语音发送中
}
if ((_isOutStatus !== '03' || recordDurationMs <= props.minRecordDuration) && recordDurationMs < 640) {
if ((_isOutStatus !== '03' || recordDurationMs <= props.minRecordDuration) && recordDurationMs < 700) {
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 640 - recordDurationMs);
}, 700 - recordDurationMs);
});
}
@@ -1,9 +1,7 @@
import { StartAudioRecordOptions, AudioStartRes } from '../interface.uts';
import { StartAudioRecordFailImpl } from '../unierror.uts';
/**
* 引用Android音频相关系统库
*/
//引用Android音频相关系统库
import AudioRecord from "android.media.AudioRecord";
import MediaRecorder from "android.media.MediaRecorder";
import AudioFormat from "android.media.AudioFormat";
@@ -42,6 +40,7 @@ export function startAudioRecord(options : StartAudioRecordOptions) : void {
}
audioRecord = _audioRecord;
isRecording.set(true)
console.log('开始采集');
const _recordThread = new Thread(() => {
_audioRecord.startRecording(); // 开始采集
const recordId = `${new Date().getTime()}_${Math.floor(Math.random() * 10000)}`;
@@ -1,251 +0,0 @@
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); // 比如40962048帧×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;
}
}
@@ -5,34 +5,22 @@ import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudio
import { NSData, NSMutableData } from "Foundation";
import { Int, Int16 } from 'Swift';
/**
* 音频采集状态(全局变量,保证生命周期)
*/
//音频采集状态(全局变量,保证生命周期)
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 targetSampleRate:Double = 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) {
@@ -76,12 +64,11 @@ export function preRequestRecordPermission(callback?: (granted: boolean) => void
* @param options 采集配置及回调
*/
@UTSJS.keepAlive
export function startAudioRecord(options: StartAudioRecordOptions): void {
export function startAudioRecord(options: StartAudioRecordOptions){
// 避免重复启动
console.log('避免重复启动', isRecording);
if (isRecording) return;
try {
recordedPCMData = new Array<UInt8>();
// 获取 iOS 系统的音频会话单例对象。
const audioSession = AVAudioSession.sharedInstance();
// 设置音频会话的类别、模式和附加选项,定义 App 的音频行为。
@@ -108,18 +95,10 @@ export function startAudioRecord(options: StartAudioRecordOptions): void {
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);
console.log("硬件原生采样率:", inputFormat.sampleRate);
recordStartTime = new Date().getTime();
// 1. 生成录音唯一标识(UUID,方便多录音实例管理)
const recordId = `${new Date().getTime()}_${Math.floor(Math.random() * 10000)}`;
@@ -153,14 +132,9 @@ export function startAudioRecord(options: StartAudioRecordOptions): void {
let convertError: NSError | null = null;
const converter = AVAudioConverter.init(from= inputFormat, to= targetFormat!);
let convertStatus = converter!.convert(to=outputBufferOpt!,error=UTSiOS.getPointer(convertError),withInputFrom=inputBlock)
// console.log(convertStatus);
// console.log(AVAudioConverterOutputStatus.haveData);
// console.log(convertStatus == AVAudioConverterOutputStatus.haveData);
const int16Ptr = outputBufferOpt!.int16ChannelData;
const dataPtr = int16Ptr![0];
// const floatChannelData = outputBufferOpt!.floatChannelData
// const dataPtr = floatChannelData![0];
let _recordedPCMData: UInt8[] = [];
for (let i: Int = 0; i < outputFrameCount; i++) {
const int16Value = dataPtr[i];
@@ -201,7 +175,7 @@ export function startAudioRecord(options: StartAudioRecordOptions): void {
* @param options 回调配置
*/
export function stopAudioRecord(): void {
export function stopAudioRecord() {
console.log('停止音频采集');
if (audioEngine === null) { // 解包:先判断是否为nil
isRecording = false;
@@ -218,133 +192,15 @@ export function stopAudioRecord(): void {
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;
}
}
// 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);
// }
// }
// 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;
// console.log(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); // 比如40962048帧×2字节)
// console.log("声道数:", audioBuffer.mNumberChannels); // 1
// console.log("二进制数据指针:", audioBuffer.mData); // 指向16位PCM数据的内存地址
@@ -136,7 +136,7 @@
},
data() {
return {
duration: 300,
duration: 200,
ani: [],
showPopup: false,
showTrans: false,
@@ -260,7 +260,7 @@
this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
}
if (this.animation) {
this.duration = 300
this.duration = 200
} else {
this.duration = 0
}