JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_开发语音采集uts插件

This commit is contained in:
田岩
2026-02-09 14:06:53 +08:00
parent ff0f104e37
commit da77f3fd40
39 changed files with 1225 additions and 662 deletions
+83
View File
@@ -0,0 +1,83 @@
{
"id": "ty-recording",
"displayName": "ty-recording",
"version": "1.0.0",
"description": "ty-recording",
"keywords": [
"ty-recording"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-android": "u",
"app-ios": "u",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
# ty-recording
### 开发文档
[UTS 语法](https://uniapp.dcloud.net.cn/tutorial/syntax-uts.html)
[UTS API插件](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
[UTS uni-app兼容模式组件](https://uniapp.dcloud.net.cn/plugin/uts-component.html)
[UTS 标准模式组件](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-vue-component.html)
[Hello UTS](https://gitcode.net/dcloud/hello-uts)
@@ -0,0 +1,3 @@
{
"minSdkVersion": "35"
}
@@ -0,0 +1,109 @@
import { StartAudioRecord, StartAudioRecordOptions } from '../interface.uts';
import { StartAudioRecordFailImpl,AudioErrorCode } from '../unierror.uts';
/**
* 引用Android音频相关系统库
*/
import AudioRecord from "android.media.AudioRecord";
import MediaRecorder from "android.media.MediaRecorder";
import AudioFormat from "android.media.AudioFormat";
import { UTSAndroid } from "io.dcloud.uts";
import HandlerThread from "android.os.HandlerThread";
import Handler from "android.os.Handler";
import AtomicBoolean from "java.util.concurrent.atomic.AtomicBoolean";
const isRecording = new AtomicBoolean(false); // 原子布尔值,保证线程安全
// 1. 音频采集参数(极简配置,适配大部分设备)
const sampleRate : Int = 16000; // 采样率
const channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
const audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位PCM
console.log('channelConfig', channelConfig);
console.log('audioFormat', audioFormat);
const frameSize = 1024; // 对应JS的frameSize:1024
const gain = 1.0; // 对应JS的gain:1.0
const fromSampleRate = 48000; // 设备原始采样率(JS的fromSampleRate:48000
const frameByteSize = frameSize * 2 * 1;
const minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// 最终缓冲区大小:取最小缓冲区和帧字节数的最大值(避免缓冲区过小)
const bufferSize = minBufferSize * 2;
console.log('bufferSize', bufferSize);
let audioRecord : AudioRecord | null = null;
let recordThread : Thread | null = null;
// let isRecording : boolean = false;
// ========== 2. 对外暴露的便捷方法(符合UTS导出规范) ==========
@UTSJS.keepAlive
export function startAudioRecord(options : StartAudioRecordOptions) : void {
console.log('启动', isRecording.get());
if (isRecording.get()) return; // 避免重复启动
const _audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
if (_audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
const err = new StartAudioRecordFailImpl(AudioErrorCode.AUDIO_FORMAT_ILLEGAL);
options.onError?.(err);
return;
}
audioRecord = _audioRecord;
isRecording.set(true)
const _recordThread = new Thread(() => {
_audioRecord.startRecording(); // 开始采集
console.log('AudioRecord已启动采集');
const buffer = new ByteArray(bufferSize); // 缓冲区只创建一次,避免重复分配内存
while (isRecording.get()) {
const readSize = _audioRecord.read(buffer, 0, bufferSize);
if (readSize > 0) {
const data = new ByteArray(readSize);
java.lang.System.arraycopy(buffer, 0, data, 0, readSize);
options.onFrame?.(data)
}
}
// 停止采集
if (_audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
console.log('停止采集xxxxxxxxx');
_audioRecord.stop();
options.onStop?.()
}
});
_recordThread.start();
recordThread = _recordThread
}
export function stopAudioRecord() : void {
console.log('停止麦克风采集', isRecording.get());
if (!isRecording.get()) return;
isRecording.set(false)
// 释放AudioRecord(局部常量暂存)
const _audioRecord = audioRecord;
if (_audioRecord != null) {
try {
if (_audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
_audioRecord.stop();
}
_audioRecord.release();
} catch (e) { /* 忽略异常 */ }
}
audioRecord = null;
// 等待线程结束
console.log('等待线程结束');
const _recordThread = recordThread;
if (_recordThread != null) {
try {
_recordThread.join(1000); // 等待1秒,超时则放弃(避免卡死)
console.log("后台线程已停止");
} catch (e) {
console.warn("停止后台线程时出现异常:", e);
} finally {
recordThread = null;
}
}
}
@@ -0,0 +1,3 @@
{
"deploymentTarget": "13"
}
@@ -0,0 +1,11 @@
/**
* 引用 iOS 系统库,示例如下:
* import { UIDevice } from "UIKit";
* [可选实现,按需引入]
*/
/* 引入 interface.uts 文件中定义的变量 */
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
/* 引入 unierror.uts 文件中定义的变量 */
import { MyApiFailImpl } from '../unierror';
@@ -0,0 +1,64 @@
/**
* interface.uts
* uts插件接口定义文件,按规范定义接口文件可以在HBuilderX中更好的做到语法提示
*/
import { AudioErrorCode } from "./unierror.uts"
// 成功开启录音的回调参数:返回录音基础配置
export type AudioStartRes = {
recordId : string; // 录音唯一标识(方便多录音实例管理)
sampleRate : number; // 采样率(如16000
format : string; // 录音格式(如pcm/mp3
startTime : number; // 开启时间戳(毫秒)
};
// 录音结束并返回文件的回调参数:核心是录音文件信息
export type AudioFileRes = {
filePath : string; // 录音文件本地路径(UTS/Uniapp 通用路径)
fileSize : number; // 文件大小(字节)
duration : number; // 录音时长(秒)
format : string; // 文件格式(如pcm/mp3
endTime : number; // 结束时间戳(毫秒)
};
/**
* 错误码
* 根据uni错误码规范要求,建议错误码以90开头,以下是错误码示例:
* - 9010001 错误信息1
* - 9010002 错误信息2
*/
// export type AudioErrorCode = 9010001 | 9010002;
/**
* 录音错误的回调参数:带错误码+错误信息,方便问题定位
*/
export interface AudioErrorRes extends IUniError {
errCode : number
};
/**
* 开启录音的完整参数配置
* 所有回调职责边界清晰,无模糊命名
*/
export type StartAudioRecordOptions = {
autoStopDuration ?: number; // 自动关闭时长,单位【秒】;传正数值则超过该时间自动关闭,不传/非正数不开启
// 【实时持续回调】- 录制过程中高频触发
onFrame ?: (frameData : any) => void; // 实时音频帧回调(每帧一次)
onVolume ?: (volume : number) => void; // 实时音量回调(如每秒5次,按设备采集频率)
// 【一次性状态回调】- 特定节点仅触发一次
onStart ?: (res : AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
onError ?: (res : AudioErrorRes) => void; // 录音错误回调(任意阶段出错都触发,仅1次)
onStop ?: () => void; // 录音结束的纯状态回调(仅通知结束,无返回,1次)
onFile ?: (res : AudioFileRes) => void; // 录音结束后,返回录音文件的回调(1次,核心)
};
/* 函数定义 */
export type StartAudioRecord = (options : StartAudioRecordOptions) => void
@@ -0,0 +1,146 @@
/* 此规范为 uni 规范,可以按照自己的需要选择是否实现 */
import { AudioErrorRes } from "./interface.uts"
/**
* 错误主题
* 注意:错误主题一般为插件名称,每个组件不同,需要使用时请更改。
* [可选实现]
*/
export const UniErrorSubject = 'ty-recording';
/**
* 错误信息
* @UniError
*/
/**
* 音频录制错误码枚举
* 前缀901:专属音频模块错误码
* 分段规则:9010001-9010009(权限)、9010010-9010019(初始化)、9010020-9010029(采集)、9010030-9010039(线程)、9010040-9010049(资源)、9010050-9010059(参数)、9010060-9010069(系统/硬件)、9010070+(预留)
* @UniError
*/
export enum AudioErrorCode {
// 权限相关(9010001-9010009
MIC_PERMISSION_NONE = 9010001, // 麦克风权限未授予
MIC_PERMISSION_DENIED = 9010002, // 麦克风权限被永久拒绝
MIC_PERMISSION_GET_FAIL = 9010003,// 获取麦克风权限状态失败
// 初始化相关(9010010-9010019
AUDIO_RECORD_INIT_FAIL = 9010010, // AudioRecord实例初始化失败
AUDIO_RECORD_NOT_INIT = 9010011, // AudioRecord未初始化就调用录制/停止
AUDIO_RECORD_CREATE_FAIL = 9010012,// AudioRecord实例创建失败(系统资源不足)
// 采集过程相关(9010020-9010029
RECORD_READ_DATA_FAIL = 9010020, // 音频采集读取数据失败
RECORD_INTERRUPTED = 9010021, // 音频采集被中断(如麦克风被抢占)
RECORD_BUFFER_EMPTY = 9010022, // 采集缓冲区无有效数据
RECORD_START_FAIL = 9010023, // 调用startRecording启动采集失败
// 线程相关(9010030-9010039
RECORD_THREAD_CREATE_FAIL = 9010030,// 采集线程创建失败
RECORD_THREAD_START_FAIL = 9010031, // 采集线程启动失败
RECORD_THREAD_INTERRUPT_FAIL = 9010032,// 采集线程中断失败
RECORD_THREAD_CRASH = 9010033, // 采集线程运行崩溃
// 资源释放相关(9010040-9010049
RECORD_STOP_FAIL = 9010040, // 调用stop停止采集失败
AUDIO_RECORD_RELEASE_FAIL = 9010041,// AudioRecord资源释放失败
RECORD_REPEAT_RELEASE = 9010042, // 音频资源重复释放
RECORD_RESOURCE_LEAK = 9010043, // 音频资源泄漏(未正常释放)
// 参数配置相关(9010050-9010059
SAMPLE_RATE_NOT_SUPPORT = 9010050, // 采样率不被设备支持
CHANNEL_CONFIG_ILLEGAL = 9010051, // 声道配置非法/不支持
AUDIO_FORMAT_ILLEGAL = 9010052, // 音频格式非法/不支持
BUFFER_SIZE_TOO_SMALL = 9010053, // 采集缓冲区大小过小(低于系统最小值)
BUFFER_SIZE_ILLEGAL = 9010054, // 采集缓冲区大小配置非法
// 系统/硬件/外部冲突(9010060-9010069
MIC_OCCUPIED = 9010060, // 麦克风被其他应用占用
AUDIO_HARDWARE_ERROR = 9010061, // 音频硬件故障
SYSTEM_RESOURCE_INSUFFICIENT = 9010062,// 系统资源不足(无法创建音频实例)
AUDIO_SERVICE_EXCEPTION = 9010063,// 系统音频服务异常/未启动
// 通用业务异常(9010070-9010079
RECORD_REPEAT_START = 9010070, // 重复启动音频录制
RECORD_NOT_RUNNING = 9010071, // 未在录制状态却调用停止/暂停
BYTE_ARRAY_COPY_FAIL = 9010072, // 音频数据缓冲区拷贝失败
// 预留扩展(9010080+
RESERVED_01 = 9010080
}
/**
* 音频录制错误码-错误信息映射表
* 与AudioErrorCode枚举一一对应,提供友好的用户/开发提示
* @UniError
*/
export const AudioErrors : Map<AudioErrorCode, string> = new Map([
// 权限相关
[AudioErrorCode.MIC_PERMISSION_NONE, '麦克风权限未授予,请先在设置中开启'],
[AudioErrorCode.MIC_PERMISSION_DENIED, '麦克风权限被永久拒绝,请前往应用设置手动开启'],
[AudioErrorCode.MIC_PERMISSION_GET_FAIL, '获取麦克风权限状态失败,请检查应用权限配置'],
// 初始化相关
[AudioErrorCode.AUDIO_RECORD_INIT_FAIL, '音频录制实例初始化失败,可能是参数不支持或系统异常'],
[AudioErrorCode.AUDIO_RECORD_NOT_INIT, '音频录制实例未初始化,无法执行操作'],
[AudioErrorCode.AUDIO_RECORD_CREATE_FAIL, '创建音频录制实例失败,系统音频资源不足'],
// 采集过程相关
[AudioErrorCode.RECORD_READ_DATA_FAIL, '音频采集读取数据失败,建议停止后重新启动'],
[AudioErrorCode.RECORD_INTERRUPTED, '音频采集被中断,麦克风可能被其他应用抢占'],
[AudioErrorCode.RECORD_BUFFER_EMPTY, '音频采集缓冲区无有效数据,采集异常'],
[AudioErrorCode.RECORD_START_FAIL, '启动音频采集失败,实例状态异常'],
// 线程相关
[AudioErrorCode.RECORD_THREAD_CREATE_FAIL, '创建音频采集线程失败,系统资源不足'],
[AudioErrorCode.RECORD_THREAD_START_FAIL, '启动音频采集线程失败,线程状态异常'],
[AudioErrorCode.RECORD_THREAD_INTERRUPT_FAIL, '中断音频采集线程失败,线程已异常'],
[AudioErrorCode.RECORD_THREAD_CRASH, '音频采集线程运行崩溃,具体原因请查看日志'],
// 资源释放相关
[AudioErrorCode.RECORD_STOP_FAIL, '停止音频采集失败,录制实例状态异常'],
[AudioErrorCode.AUDIO_RECORD_RELEASE_FAIL, '释放音频录制资源失败,可能存在资源泄漏'],
[AudioErrorCode.RECORD_REPEAT_RELEASE, '音频录制资源已释放,请勿重复执行释放操作'],
[AudioErrorCode.RECORD_RESOURCE_LEAK, '音频录制资源未正常释放,建议检查停止逻辑'],
// 参数配置相关
[AudioErrorCode.SAMPLE_RATE_NOT_SUPPORT, '当前设备不支持该采样率,建议切换为16000/44100Hz'],
[AudioErrorCode.CHANNEL_CONFIG_ILLEGAL, '声道配置非法或当前设备不支持,建议使用单声道'],
[AudioErrorCode.AUDIO_FORMAT_ILLEGAL, '音频格式非法或当前设备不支持,建议使用PCM_16BIT'],
[AudioErrorCode.BUFFER_SIZE_TOO_SMALL, '采集缓冲区过小,已自动调整为系统最小缓冲区'],
[AudioErrorCode.BUFFER_SIZE_ILLEGAL, '采集缓冲区大小配置非法,已使用系统默认值'],
// 系统/硬件/外部冲突
[AudioErrorCode.MIC_OCCUPIED, '麦克风被其他应用占用,请关闭后重新尝试'],
[AudioErrorCode.AUDIO_HARDWARE_ERROR, '音频硬件故障,请检查设备麦克风是否正常'],
[AudioErrorCode.SYSTEM_RESOURCE_INSUFFICIENT, '系统资源不足,无法创建音频录制实例'],
[AudioErrorCode.AUDIO_SERVICE_EXCEPTION, '系统音频服务异常,请重启设备后重新尝试'],
// 通用业务异常
[AudioErrorCode.RECORD_REPEAT_START, '当前正在录制中,请勿重复启动音频录制'],
[AudioErrorCode.RECORD_NOT_RUNNING, '未在音频录制状态,无需执行停止操作'],
[AudioErrorCode.BYTE_ARRAY_COPY_FAIL, '音频数据缓冲区拷贝失败,采集数据可能损坏'],
// 预留扩展
[AudioErrorCode.RESERVED_01, '预留错误码,暂未定义']
]);
/**
* 错误对象实现
*/
export class StartAudioRecordFailImpl extends UniError implements AudioErrorRes {
/**
* 错误对象构造函数
*/
constructor(errCode : AudioErrorCode) {
super();
this.errSubject = UniErrorSubject;
// @ts-ignore 忽略"value属性不存在"的假报错(运行时存在)
this.errCode = errCode.value as number;
this.errMsg = AudioErrors.get(errCode) ?? "";
}
}
@@ -0,0 +1,2 @@
export function startAudioRecord(callback) {}
export function stopAudioRecord() {}