更新记录
0.1.0(2026-07-14)
首个公开版本:支持 PCM16LE、WAV、M4A(AAC-LC)、16k ASR 实时音频帧、dBFS、能量 VAD、分片与诊断。核心插件不联网、不内置云厂商密钥。
平台兼容性
uni-app(4.61)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | √ | √ | √ | √ | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x(4.61)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | 5.0 | 12 | × | × |
AI 语音录音引擎使用说明
为实时 ASR 设计|PCM / WAV / M4A|VAD|分片与诊断|后台录音
sy-asrrec 不是又一个只提供 start/stop 的录音插件,而是面向 App-Android 与 App-iOS 的 AI 语音录音底座:录音同时输出 16k PCM 实时帧与 PCM/WAV/M4A 文件,并把 VAD、dBFS、分片、低存储保护、丢帧统计、音频路由和系统中断都做成可观测能力。
插件目录还提供阿里云、百度智能云、腾讯云实时 ASR 的安全接入示例和无账号 Mock,适合 AI 实时听写、语音对话、会议转写、客服质检、口语练习、语音表单、实时字幕和自建 ASR WebSocket。
使用前必读
- 本插件只支持 App-Android、App-iOS,不支持 H5、小程序、HarmonyOS 或快应用。
- 插件采集的是 App 麦克风输入,不支持电话通话录音、系统内录、隐蔽录音或应用被强制结束后继续录音。
- 插件核心不联网、不提供云 ASR 账号,也不内置 AccessKey、SecretKey 或长期 Token。
- 后台录音必须由用户在 App 可见时主动开始,并始终受 Android/iOS 系统限制与可见提示约束。
- 音频能力受设备麦克风、系统音频策略、蓝牙设备和厂商 ROM 影响,购买或上线前请先使用试用版在目标真机验证。
插件信息
| 项目 | 内容 |
|---|---|
| 插件 ID | sy-asrrec |
| 插件类型 | UTS 插件 / API 插件 |
| 当前版本 | 0.1.0 |
| HBuilderX | 4.61+ |
| 支持平台 | App-Android、App-iOS |
| Android 最低版本 | API 21 / Android 5.0 |
| iOS 最低版本 | iOS 12 |
| 文件格式 | PCM16LE、WAV、M4A(AAC-LC) |
| 实时帧格式 | PCM signed 16-bit little-endian ArrayBuffer |
| 默认 ASR 参数 | 16000Hz、单声道、16bit、40ms/帧 |
| 是否申请权限 | 是,麦克风;后台录音还涉及系统前台服务/后台音频能力 |
| 是否主动联网 | 否 |
| 是否采集设备标识 | 否 |
| 是否包含广告 | 否 |
核心能力
- 实时 PCM 帧:录音过程中持续输出 PCM16LE
ArrayBuffer,可直接交给业务 WebSocket 或实时 ASR SDK。 - 三种文件格式:支持原始
pcm、带标准文件头的wav、体积更适合长录音的m4a。 - 一套跨端 API:Android 和 iOS 使用相同的 Promise 管理器、开始参数、停止结果和事件订阅方式。
- 面向 AI 语音场景:内置
asr16k、asrTts、meeting、custom四种预设。 - 实时可观测:提供 dBFS、基础能量 VAD、回调排队数、丢帧数、实际输入采样率等信息。
- 完整录音控制:支持开始、暂停、恢复、停止、取消、限时、分片、低存储保护和旧文件清理。
- 系统事件:可监听音频中断、输入路由变化、状态变化和分片完成事件。
- 后台录音:用户从前台主动开始后,可按系统规则在后台或息屏状态继续录音。
- ASR 示例:插件目录附带阿里云、百度智能云、腾讯云适配示例,以及不需要账号的本地 Mock。
和普通录音插件有什么不同
| 常见录音需求 | 本插件提供的产品化能力 |
|---|---|
| 只拿到录音文件 | 文件与 PCM 实时帧可同时输出,也可分别关闭 |
| 自己研究 ASR 音频参数 | asr16k 直接提供 16kHz / 单声道 / 16bit / 40ms PCM |
| 自己计算是否有人说话 | 帧内置 dBFS 与可配置能量 VAD,并提供状态切换事件 |
| 出问题只能看“录音失败” | 可观察实际输入采样率、帧序号、回调积压/丢帧、路由和系统中断 |
| 长录音靠业务硬撑 | M4A、自动分片、剩余空间阈值和安全停止共同保护长会话 |
| 云 ASR 凭据容易误放客户端 | 三家厂商适配示例统一要求后端下发短期 Token/签名 URL |
| 没有云账号无法联调 UI | 本地 Mock 不上传音频,也能跑通中间结果、最终结果和会话收尾 |
适用场景
- 需要边录边把 PCM 音频发送给实时语音识别服务。
- 需要记录完整 WAV/M4A 文件,同时在页面展示波形、音量或说话状态。
- 需要会议、访谈、口述内容等较长录音,并按固定时长分片。
- 需要暂停/恢复、系统中断监控、耳机或蓝牙输入变化提示。
- 希望云 ASR 厂商可替换,录音层不绑定阿里云、百度或腾讯云。
- 希望业务服务端负责短期凭据和签名,避免把云厂商长期密钥打进 App。
不适用场景
- H5、微信/支付宝/抖音等小程序、HarmonyOS、快应用。
- 电话通话录音、微信/系统内部声音录制、其他 App 声音采集。
- 无系统提示的静默录音、后台偷偷启动麦克风或强杀后继续录音。
- 直接输出 MP3、FLAC、OGG、8bit/24bit/32bit PCM。
- 内置语音转文字结果或开箱即用的免费云 ASR 服务。
- 专业回声消除(AEC)、降噪(ANS)、自动增益(AGC)、变声、混音或声纹识别。
- 由插件自动合并多个录音分片,或自动上传、转码、播放录音文件。
平台兼容性
| 平台 / 页面类型 | 支持情况 | 说明 |
|---|---|---|
| App-Android | 支持 | 最低 API 21;后台录音使用麦克风前台服务 |
| App-iOS | 支持 | 最低 iOS 12;后台录音依赖 audio 后台模式 |
| app-vue / Vue 2 | 支持目标 | 建议在目标 HBuilderX 与真机基座先试用验证 |
| app-vue / Vue 3 | 支持 | 示例工程采用 Vue 3 |
| app-nvue | 支持目标 | 使用 API 插件,不提供原生 UI 组件 |
| uni-app x / app-uvue | 支持目标 | 需 HBuilderX 4.61+,发布前请完成目标设备验证 |
| H5 / Web | 不支持 | 浏览器环境没有本插件的 UTS 原生录音实现 |
| 各类小程序 | 不支持 | 请使用对应小程序平台的录音 API |
| HarmonyOS / 鸿蒙元服务 | 不支持 | 0.1.0 未提供鸿蒙原生实现 |
“支持目标”表示源码和公共接口按对应运行时设计。原生录音在不同 HBuilderX、基座、系统和设备上的表现可能存在差异,请先使用插件市场试用能力完成自定义基座与正式云打包验证。
输出格式说明
| 输出 | 内容 | 适合场景 | 注意事项 |
|---|---|---|---|
pcm 文件 |
无文件头的 PCM16LE 原始数据 | 自定义协议、音频算法 | 普通播放器通常不能直接识别,需要已知采样率/声道 |
wav 文件 |
PCM16LE + WAV 文件头 | 语音识别、调试、通用播放 | 文件比 M4A 大 |
m4a 文件 |
AAC-LC 封装 | 会议、长录音、留档 | 不适合作为实时 ASR 原始帧 |
frameBuffer |
PCM16LE ArrayBuffer |
WebSocket、ASR、波形分析 | 不包含 WAV 文件头,不要当完整音频文件播放 |
实时帧和保存文件是两条独立输出链路:
saveToFile:false时可以只使用实时帧,但停止结果没有文件路径。enableFrameCallback:false时可以只保存文件,减少 JS 回调压力。- WAV/M4A 文件格式不会改变实时帧格式;实时帧始终为 PCM16LE。
安装
方式一:从插件市场导入
在插件市场点击“使用 HBuilderX 导入插件”,选择目标项目。导入后确认目录为:
uni_modules/sy-asrrec/
方式二:手动放入项目
将插件完整目录复制到项目的 uni_modules 下:
项目根目录/
├─ uni_modules/
│ └─ sy-asrrec/
│ ├─ package.json
│ ├─ manager.js
│ ├─ index.js
│ ├─ index.d.ts
│ ├─ docs/
│ ├─ examples/
│ └─ utssdk/
├─ pages.json
└─ manifest.json
修改或首次导入 UTS/原生代码后,请重新运行 App 自定义基座或重新云打包。仅热刷新页面不会更新 Kotlin/Swift 原生实现。
推荐引入方式
请从 manager.js 引入 Promise 管理器。它负责统一 Android/iOS 返回结构、事件订阅,以及把原生 Base64 帧恢复成 ArrayBuffer。
// #ifdef APP-PLUS
import {
ERROR_CODES,
getPluginInfo,
getRecorderManager,
} from "@/uni_modules/sy-asrrec/manager.js";
const recorder = getRecorderManager();
// #endif
插件只支持 App,公共项目中请使用条件编译包住导入和调用,避免 H5、小程序构建时加载原生模块。
1 分钟快速开始
录制一个 16kHz WAV 文件
import { getRecorderManager } from "@/uni_modules/sy-asrrec/manager.js";
const recorder = getRecorderManager();
async function startRecording() {
try {
await recorder.requestPermission();
const startResult = await recorder.start({
preset: "asr16k",
background: false,
});
console.log("录音已开始", startResult);
} catch (error) {
console.error("启动失败", error.code, error.message);
}
}
async function stopRecording() {
try {
const result = await recorder.stop();
console.log("录音时长(ms)", result.duration);
console.log("录音文件", result.tempFilePath);
} catch (error) {
console.error("停止失败", error.code, error.message);
}
}
获取实时 PCM 帧
const frameToken = recorder.onFrameRecorded((frame) => {
console.log("序号", frame.sequence);
console.log("音量(dBFS)", frame.dBFS);
console.log("说话状态", frame.vadState);
// frame.frameBuffer 是 PCM16LE ArrayBuffer。
// 可交给你自己的 WebSocket/ASR 发送逻辑。
sendPcmToYourAsr(frame.frameBuffer);
});
await recorder.start({
preset: "asr16k",
enableFrameCallback: true,
});
// 页面卸载或不再需要帧时取消监听。
recorder.offFrameRecorded(frameToken);
Vue 3 页面示例
下面是一个最小但完整的页面流程:授权 → 开始 → 观察 dBFS/VAD → 停止 → 页面卸载清理。
<script setup>
import { onUnmounted, shallowRef } from "vue";
// #ifdef APP-PLUS
import { getRecorderManager } from "@/uni_modules/sy-asrrec/manager.js";
// #endif
const state = shallowRef("idle");
const dBFS = shallowRef(-96);
const vadState = shallowRef("silence");
const duration = shallowRef(0);
const filePath = shallowRef("");
const errorMessage = shallowRef("");
// #ifdef APP-PLUS
const recorder = getRecorderManager();
const frameToken = recorder.onFrameRecorded((frame) => {
dBFS.value = frame.dBFS;
vadState.value = frame.vadState;
});
const stateToken = recorder.onStateChange((event) => {
state.value = event.state;
});
// #endif
async function startRecording() {
errorMessage.value = "";
// #ifdef APP-PLUS
try {
await recorder.requestPermission();
await recorder.start({
preset: "asr16k",
enableFrameCallback: true,
background: false,
});
} catch (error) {
errorMessage.value =
`${error.code || ""} ${error.message || "启动失败"}`.trim();
}
// #endif
}
async function stopRecording() {
// #ifdef APP-PLUS
try {
const result = await recorder.stop();
duration.value = result.duration;
filePath.value = result.tempFilePath;
} catch (error) {
errorMessage.value =
`${error.code || ""} ${error.message || "停止失败"}`.trim();
}
// #endif
}
async function cancelRecording() {
// #ifdef APP-PLUS
try {
await recorder.cancel();
filePath.value = "";
duration.value = 0;
} catch (error) {
errorMessage.value =
`${error.code || ""} ${error.message || "取消失败"}`.trim();
}
// #endif
}
onUnmounted(() => {
// #ifdef APP-PLUS
recorder.offFrameRecorded(frameToken);
recorder.offStateChange(stateToken);
const snapshot = recorder.getState();
if (
snapshot.success &&
["recording", "paused", "interrupted"].includes(snapshot.data?.state)
) {
recorder.cancel().catch(() => {});
}
// #endif
});
</script>
<template>
<view class="recorder-page">
<text>状态:{{ state }}</text>
<text>音量:{{ dBFS.toFixed(1) }} dBFS</text>
<text>VAD:{{ vadState }}</text>
<button @click="startRecording">开始录音</button>
<button @click="stopRecording">停止并保留</button>
<button @click="cancelRecording">取消并删除</button>
<text v-if="duration">时长:{{ duration }} ms</text>
<text v-if="filePath">文件:{{ filePath }}</text>
<text v-if="errorMessage">{{ errorMessage }}</text>
</view>
</template>
这个示例刻意保持为单页,便于复制验证。正式业务建议把录音状态与副作用封装到 composable,并在唯一的业务出口统一管理监听 token、停止/取消和页面卸载。
推荐调用顺序
普通文件录音
getPermissionStatus()
-> requestPermission()
-> 注册需要的事件
-> start(options)
-> pause()/resume()(可选)
-> stop()
-> 注销事件
实时 ASR
从业务后端获取短期 ASR 配置
-> await asr.connect()
-> onFrameRecorded(frame => asr.send(frame.frameBuffer))
-> await recorder.start({ preset: 'asr16k' })
-> await recorder.stop()
-> offFrameRecorded(token)
-> await asr.finish()
用户取消
用户取消 / 页面销毁
-> 注销实时帧监听
-> await recorder.cancel()
-> 关闭 ASR WebSocket
stop() 与 cancel() 不同:
stop():正常封装文件,返回停止结果,并触发stop事件。cancel():结束当前会话并删除本次临时/分片文件,不触发stop事件。
录音预设
| 预设 | 默认文件 | 实时帧 | VAD | 适用场景 |
|---|---|---|---|---|
asr16k |
16kHz / 单声道 / 16bit WAV | 40ms,开启 | 开启 | 实时 ASR、语音输入、实时字幕 |
asrTts |
16kHz / 单声道 / 16bit WAV | 40ms,开启 | 开启 | iOS 播放 TTS 时继续录音 |
meeting |
48kHz / 单声道 M4A / 64kbps | 关闭 | 关闭 | 会议、访谈、较长录音 |
custom |
未设置字段继承 asr16k |
可配置 | 可配置 | 自定义采样率、声道、格式、帧长 |
asrTts 重要说明
asrTts 主要调整 iOS 音频会话,使录音与播放可以共存。它 不提供专业 AEC,因此扬声器播放的 TTS 可能再次被麦克风录入。是否产生回声、回声大小和系统处理效果与设备、播放音量、路由和系统版本有关。
如果业务要求稳定的全双工对话、强回声消除或免提通话级体验,需要额外接入专业实时音频/RTC 方案,不能只依赖本预设。
start(options) 参数
所有参数均为可选。不传参数时等同于使用 preset:'asr16k'。
基础与音频参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
preset |
'asr16k' | 'asrTts' | 'meeting' | 'custom' |
asr16k |
录音预设 |
duration |
number |
0 |
最大录音时长,单位 ms;0 表示不自动停止 |
sampleRate |
number |
16000 或 48000 | 支持 8000、16000、24000、44100、48000 |
numberOfChannels |
1 | 2 |
1 |
单声道或双声道;设备必须支持对应输入 |
bitsPerSample |
16 |
16 |
0.1.0 只支持 16bit |
format |
'pcm' | 'wav' | 'm4a' |
预设决定 | 文件格式;不支持 MP3 |
encodeBitRate |
number |
64000 |
M4A AAC 编码码率,最低按 16000 处理 |
实时帧参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enableFrameCallback |
boolean |
ASR 开,meeting 关 | 是否派发 frameRecorded |
frameDurationMs |
number |
40 |
每帧时长;小于 20 按 20,大于 200 按 200 |
frameSize |
number |
- | 兼容字段,按 PCM 字节数换算帧长;与 frameDurationMs 同传时以后者为准 |
帧字节数计算:
frameBytes = sampleRate × numberOfChannels × 2 × frameDurationMs / 1000
例如 16kHz、单声道、16bit、40ms:
16000 × 1 × 2 × 40 / 1000 = 1280 bytes
文件与分片参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
saveToFile |
boolean |
true |
是否保存本地文件 |
segmentDurationMs |
number |
0 |
0 不分片;启用时必须不少于 60000ms |
outputDirectory |
string |
插件私有目录 | 只能使用应用沙箱内可写目录 |
fileNamePrefix |
string |
recorder |
文件名前缀;非法字符会被替换或清理 |
minFreeBytes |
number |
50MB | 启动和录制中要求保留的最小可用空间 |
后台与中断参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
background |
boolean |
false |
是否允许从前台开始后在后台继续录音 |
interruptionPolicy |
'resume' | 'stop' |
resume |
iOS 中断结束后尝试恢复,或直接停止 |
audioSessionPreset |
'record' | 'asrTts' |
预设决定 | iOS 音频会话模式;普通业务优先使用 preset |
VAD 参数
await recorder.start({
preset: "custom",
sampleRate: 16000,
format: "wav",
vad: {
enabled: true,
thresholdDb: -45,
speechStartMs: 120,
speechEndMs: 600,
},
});
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
vad.enabled |
boolean |
ASR 开,meeting 关 | 是否启用能量 VAD |
vad.thresholdDb |
number |
-45 |
高于或等于此 dBFS 视为有声 |
vad.speechStartMs |
number |
120 |
连续有声多久后进入 speech |
vad.speechEndMs |
number |
600 |
连续低于阈值多久后回到 silence |
0.1.0 保留字段
类型声明中包含 mixWithOthers 与 notification 配置,为后续版本预留。0.1.0 不应依赖这两个字段改变运行行为:
- iOS
asrTts会按预设配置可混音音频会话,mixWithOthers暂不作为独立开关。 - Android 后台通知使用插件默认频道、标题和文案,
notification.title等字段暂不生效。 - 后台录音通知和系统麦克风提示属于合规与系统要求,任何版本都不会提供隐藏开关。
方法说明
getRecorderManager()
返回全局单例录音管理器。同一时间只允许一个活动录音会话。
const recorder = getRecorderManager();
requestPermission()
请求麦克风权限,返回 Promise。
try {
const permission = await recorder.requestPermission();
console.log(permission.granted, permission.status);
} catch (error) {
if (error.code === 9011003) {
// 用户拒绝或当前环境无法再次弹出授权框。
}
}
Android 端该方法只主动请求麦克风权限。若业务启用后台录音,Android 13+ 的通知授权提示与引导应由宿主 App 按自身产品流程处理。
getPermissionStatus()
读取当前麦克风权限状态,不主动弹窗。
const permission = await recorder.getPermissionStatus();
可能包含:
| 字段 | 说明 |
|---|---|
status |
granted、denied 或运行时返回的权限状态 |
granted |
是否已授权 |
requested |
本次调用链是否发起过请求 |
canAskAgain |
当前是否具备再次请求的条件;最终以系统行为为准 |
start(options)
开始录音。成功后返回实际会话参数并触发 start 事件。
const result = await recorder.start({ preset: "asr16k" });
返回字段:
| 字段 | 说明 |
|---|---|
sessionId |
本次录音的随机会话 ID |
state |
recording |
actualInputSampleRate |
设备原始输入采样率 |
sampleRate |
插件标准化后的输出采样率 |
numberOfChannels |
输出声道数 |
bitsPerSample |
固定 16 |
format |
保存文件格式 |
actualInputSampleRate 与 sampleRate 不同不代表错误。插件会在原生层把设备实际输入转换为目标采样率。
pause()
暂停写入和实时帧输出,保留当前会话,返回最新状态快照。仅能在 recording 状态调用。
await recorder.pause();
resume()
恢复已暂停的会话。仅能在 paused 状态调用。
await recorder.resume();
stop()
正常停止并封装文件,触发 stop 事件,返回 RecorderStopResult。
const result = await recorder.stop();
cancel()
取消当前会话并删除本次文件,不触发 stop 事件。
await recorder.cancel();
getState()
同步读取状态。注意:这是管理器中少数不返回 Promise 的方法,返回统一响应外壳。
const response = recorder.getState();
if (response.success) {
console.log(response.data.state);
}
状态可能为:
idle -> preparing -> recording -> paused/interrupted -> stopping -> stopped
\-> error
getDiagnostics()
返回平台、系统版本、录音状态、剩余空间、实际输入采样率、回调排队数和丢帧数等诊断信息。
const diagnostics = await recorder.getDiagnostics();
console.log(diagnostics);
反馈问题前请对诊断信息脱敏,不要附带 Token、签名 URL、完整录音或用户语音文本。
cleanup(options)
删除插件默认录音目录中早于指定时间的旧文件。录音进行中不能调用。
const result = await recorder.cleanup({
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
});
console.log("已删除文件数", result.deleted);
如果业务把重要录音长期保存在插件默认目录,请谨慎调用。更推荐在业务层建立明确的文件保留、迁移和删除策略。
runSelfTest()
执行轻量原生自检,检查 WAV 文件头、dBFS 算法等基础能力。
const report = await recorder.runSelfTest();
console.log(report.passed, report.items);
自检不等于真实麦克风、M4A 编码、后台录音或 ASR 联调测试,不能代替真机验收。
getPluginInfo()
返回插件 ID、版本和支持的文件格式。
const info = getPluginInfo();
console.log(info);
事件订阅
所有 onXxx(callback) 都返回监听 token。建议保存 token,并使用对应 offXxx(token) 精确注销。
const token = recorder.onError((event) => {
console.error(event.code, event.message);
});
// 不再需要时:
recorder.offError(token);
不传 token 调用 offXxx() 会清除该事件下通过当前管理器注册的全部监听。
| 事件 | 快捷方法 | 触发时机 |
|---|---|---|
start |
onStart/offStart |
成功进入录音状态 |
pause |
onPause/offPause |
主动暂停 |
resume |
onResume/offResume |
主动恢复 |
stop |
onStop/offStop |
正常停止、限时停止或异常安全收尾 |
error |
onError/offError |
发生录音、存储、编码或恢复错误 |
stateChange |
onStateChange/offStateChange |
状态机变化 |
frameRecorded |
onFrameRecorded/offFrameRecorded |
产生一帧实时 PCM |
vadChange |
onVadChange/offVadChange |
silence 与 speech 切换 |
interruption |
onInterruption/offInterruption |
系统音频中断开始、结束或输入丢失 |
segment |
onSegment/offSegment |
一个录音分片封装完成 |
stats |
onStats/offStats |
约每秒派发实时统计 |
routeChange |
onRouteChange/offRouteChange |
麦克风、耳机、蓝牙、USB 等输入路由变化 |
也可以使用通用方法:
const token = recorder.on("stats", handleStats);
recorder.off("stats", token);
frameRecorded 数据
interface RecorderFrame {
event: "frameRecorded";
sessionId: string;
sequence: number;
timestampMs: number;
durationMs: number;
frameBuffer: ArrayBuffer;
base64?: string;
sampleRate: number;
numberOfChannels: number;
bitsPerSample: 16;
dBFS: number;
vadState: "silence" | "speech";
isLastFrame: boolean;
}
说明:
frameBuffer是业务首选字段,为 PCM16LEArrayBuffer。base64是兼容和排障备用字段,不建议在高频业务链路重复转码。timestampMs是相对本次录音开始的音频时间,不是 Unix 时间戳。dBFS通常接近 0 表示更响,接近-96表示接近静音。isLastFrame为协议保留字段;0.1.0 不应依赖停止时一定收到尾帧。
VAD 行为
0.1.0 使用基础能量 VAD:
- 连续达到阈值
speechStartMs后,从silence进入speech。 - 连续低于阈值
speechEndMs后,从speech回到silence。 - VAD 只报告状态,不会裁剪、跳过或自动停止录音。
- 能量 VAD 不理解语义,键盘声、碰撞声、音乐或环境噪音也可能被判断为
speech。
需要更准确的人声检测时,可继续使用本插件获取 PCM,再接入专用 VAD/降噪模型。
stats 数据
常见字段:
| 字段 | 说明 |
|---|---|
totalFrames |
已形成的实时帧序号/数量 |
pendingFrameCallbacks |
等待派发或等待 JS 消费的帧回调数 |
droppedFrames |
为控制延迟而丢弃的实时回调帧数 |
actualInputSampleRate |
设备实际输入采样率 |
当待处理帧达到内部保护上限时,插件会丢弃较旧或当前无法及时派发的 实时回调帧,避免 JS 队列无限堆积。文件写入在原生采集链路中进行,实时回调丢帧不等同于录音文件必然缺失。
不要在 onFrameRecorded 中执行大段同步计算、频繁 JSON 序列化或阻塞 UI 的操作。
stop() 返回结果
interface RecorderStopResult {
sessionId: string;
tempFilePath: string;
segments: RecorderSegment[];
duration: number;
fileSize: number;
format: "pcm" | "wav" | "m4a";
sampleRate: number;
numberOfChannels: number;
bitsPerSample: 16;
actualInputSampleRate: number;
interrupted: boolean;
stopReason: string;
totalFrames: number;
droppedFrames: number;
finalizeFailed?: boolean;
}
tempFilePath 与 segments
- 未分片且成功保存单个文件时,
tempFilePath是最终可用路径。 - 启用分片后,顶层
tempFilePath为空字符串,必须遍历segments。 saveToFile:false时,没有可用文件路径。- 虽然字段名为
tempFilePath,正常stop()后返回的是已完成封装的最终文件路径。 - 文件先以
.tmp写入,正常收尾后才移除.tmp后缀;不要在录音未停止时把临时文件当成完整 WAV/M4A。
分片示例:
const result = await recorder.stop();
const files = result.segments.map((segment) => ({
path: segment.tempFilePath,
duration: segment.duration,
size: segment.fileSize,
}));
console.log(files);
stopReason
常见值:
| 值 | 含义 |
|---|---|
manual |
业务主动调用 stop() |
duration |
达到 duration 自动停止 |
storage-low |
剩余空间低于 minFreeBytes,插件安全停止 |
interruption |
系统中断且无法/不允许继续 |
error |
采集或底层处理发生错误 |
若 finalizeFailed:true,说明录音结束阶段的文件封装失败。此时不要直接上传或依赖文件可播放性,应提示用户并记录脱敏诊断信息。
后台录音与权限
Android
插件清单声明:
RECORD_AUDIOFOREGROUND_SERVICEFOREGROUND_SERVICE_MICROPHONEPOST_NOTIFICATIONS
background:true 时,插件会启动系统可见的麦克风前台服务:
- 必须在 App 有可见 Activity、且由用户操作触发时开始。
- Android 14+ 对后台启动麦克风前台服务限制更严格;从后台直接调用会返回
9011007。 - 前台服务通知不能隐藏。
- Android 13+ 是否弹出通知授权、通知如何展示,受宿主 App 授权流程和系统策略影响。
- 部分国产 ROM 的省电策略可能在锁屏后限制后台执行,需在目标品牌实机验证并给用户合理引导。
iOS
插件包含麦克风用途说明和 audio 后台模式:
- 第一次录音前需要用户授权麦克风。
- 电话、Siri、闹钟、其他不可混音音频会话、媒体服务重置都可能中断录音。
interruptionPolicy:'resume'只会在系统允许时尝试恢复,不保证所有中断都能恢复。- 用户强制结束 App 后不能继续录音。
- App Store 隐私说明、录音提示和业务用途仍需由宿主 App 根据实际功能完成。
后台录音正确示例
// 必须在可见页面内,由用户点击等明确动作触发。
async function handleUserStartBackgroundRecording() {
await recorder.requestPermission();
await recorder.start({
preset: "meeting",
background: true,
});
}
不要在定时任务、静默推送、后台回调或应用启动后无用户感知地自动调用录音。
实时 ASR 接入
核心录音插件不会连接任何 ASR 服务。examples/asr/ 只是可选适配层,只有业务代码主动创建适配器并调用 connect() 后才会联网。
安全架构
App 向业务服务端请求短期凭据/签名 URL
-> App 连接 ASR WebSocket
-> 插件输出 PCM16LE frameBuffer
-> 业务代码发送二进制帧
-> ASR 返回中间/最终文本
严禁把以下内容硬编码进 App、插件目录或公开仓库:
- 阿里云 AccessKey / AccessKeySecret
- 百度永久 API Key / Secret Key
- 腾讯云 SecretId / SecretKey
- 长期有效 Token
- 可重复使用的签名 WebSocket URL
示例适配器
| 服务 | 创建方法 | 客户端应获取的短期配置 |
|---|---|---|
| 本地 Mock | createMockAsrAdapter() |
无,不联网 |
| 阿里云 | createAliyunAsrAdapter() |
短期 Token URL、非秘密 appKey |
| 百度智能云 | createBaiduAsrAdapter() |
推荐业务代理 URL、会话级 START 参数 |
| 腾讯云 | createTencentAsrAdapter() |
业务后端生成的短期签名 WSS URL |
先用 Mock 验证
import { getRecorderManager } from "@/uni_modules/sy-asrrec/manager.js";
import { createMockAsrAdapter } from "@/uni_modules/sy-asrrec/examples/asr/index.js";
const recorder = getRecorderManager();
const asr = createMockAsrAdapter();
asr.onResult((result) => {
console.log(result.isFinal ? "最终结果" : "中间结果", result.text);
});
asr.onError((error) => {
console.error("ASR 错误", error);
});
await asr.connect();
const token = recorder.onFrameRecorded((frame) => {
asr.send(frame.frameBuffer);
});
await recorder.start({
preset: "asr16k",
enableFrameCallback: true,
});
// 用户结束时:
await recorder.stop();
recorder.offFrameRecorded(token);
await asr.finish();
Mock 不上传音频,只根据收到的帧生成模拟中间/最终文本。请先用它确认权限、录音、帧事件和 UI 流程,再接真实云厂商。
ASR 音频要求
大多数实时中文 ASR 可使用:
| 参数 | 推荐值 |
|---|---|
| 编码 | PCM signed 16-bit little-endian |
| 采样率 | 16000Hz |
| 声道 | 1 |
| 实时帧 | frame.frameBuffer |
| WAV 文件头 | 不发送 |
云厂商可能要求按 100–200ms 聚合后再发送。插件默认每 40ms 产出 1280 字节;示例适配器会按服务需要聚合。协议、限流、计费与字段含义以云厂商当前官方文档为准。
错误处理
Promise 方法失败时会抛出带 code 和 details 的 Error:
try {
await recorder.start({ preset: "asr16k" });
} catch (error) {
switch (error.code) {
case 9011003:
uni.showModal({
title: "需要麦克风权限",
content: "请在系统设置中允许本应用使用麦克风。",
});
break;
case 9011007:
uni.showToast({
title: "请回到应用页面后开始录音",
icon: "none",
});
break;
default:
console.error(error.code, error.message, error.details);
}
}
错误码
| 错误码 | 常量 | 含义 | 建议处理 |
|---|---|---|---|
| 9011001 | INVALID_OPTIONS |
参数错误 | 检查格式、采样率、声道、分片时长 |
| 9011002 | INVALID_STATE |
当前状态不允许操作 | 避免重复 start/stop,先读取 getState() |
| 9011003 | PERMISSION_DENIED |
麦克风未授权 | 请求权限或引导用户到系统设置 |
| 9011004 | MICROPHONE_UNAVAILABLE |
麦克风不可用/被占用 | 检查系统中断、其他录音 App 和输入设备 |
| 9011005 | INITIALIZATION_FAILED |
初始化失败 | 更换参数、设备并收集诊断 |
| 9011006 | CAPTURE_FAILED |
采集过程失败 | 停止会话,查看 error/interruption |
| 9011007 | BACKGROUND_START_NOT_ALLOWED |
不允许从后台启动 | 回到可见页面,由用户重新开始 |
| 9011008 | FILE_WRITE_FAILED |
文件创建/写入/封装失败 | 检查目录、空间、finalizeFailed |
| 9011009 | ENCODER_FAILED |
编码器失败 | 检查 M4A 参数和目标机型 |
| 9011010 | INTERRUPTION_NOT_RECOVERED |
中断后无法恢复 | 结束当前会话,提示用户重新录制 |
| 9011011 | STORAGE_LOW |
剩余存储不足 | 清理旧文件或调高可用空间 |
| 9011012 | UNSUPPORTED_CONFIGURATION |
格式/位深不支持 | 仅使用 PCM/WAV/M4A 与 16bit |
| 9011013 | ROUTE_UNAVAILABLE |
输入路由不可用 | 检查蓝牙/耳机/USB 麦克风 |
| 9011099 | UNKNOWN |
未分类内部错误 | 提供最小复现和脱敏诊断 |
常见问题
1. 为什么 H5 或小程序不能使用?
插件底层直接调用 Android/iOS 原生录音能力,只在 App 端编译和运行。H5 或小程序请使用各平台提供的录音 API。
2. 为什么点击开始后提示 9011003?
麦克风尚未授权,或用户已拒绝权限。先调用 requestPermission();如果系统不再弹框,需要在产品界面说明原因并引导用户前往系统设置。
3. 为什么后台开始录音报 9011007?
background:true 不是“允许在后台第一次启动”。它表示用户在 App 可见时开始后,录音可以按系统规则继续到后台。请让用户回到可见页面并主动点击开始。
4. 为什么页面波形是一条直线?
依次检查:
- 是否运行在 Android/iOS 真机,而不是 H5。
- 是否重新编译了包含原生插件的基座。
- 是否设置
enableFrameCallback:true。 - 是否注册了
onFrameRecorded。 dBFS是否始终接近-96,以及麦克风是否被其他应用占用。- 是否在收到帧后用阻塞代码卡住主线程。
5. 为什么录音文件没有路径?
常见原因:
- 设置了
saveToFile:false。 - 开启了分片,此时顶层
tempFilePath为空,应读取segments。 - 调用了
cancel(),本次文件已删除。 - 文件封装失败,检查
finalizeFailed、error事件和错误码。
6. 为什么 WAV/M4A 无法播放?
- 必须等
stop()完成后再使用文件。 - 录音中的
.tmp还不是最终文件。 - 检查文件大小是否为 0、剩余存储是否充足。
- 在目标 Android/iOS 设备上验证,不要只用某一个桌面播放器判断。
- 若
finalizeFailed:true,本次文件不能视为有效产物。
7. 为什么 ASR 没有识别结果?
- 实时识别应发送
frame.frameBuffer,不要把 WAV 文件头发给 ASR。 - 使用
asr16k,确认 16000Hz、单声道、16bit。 - 先用 Mock 验证本地帧流。
- 确认短期 URL/Token 尚未过期。
- 检查厂商要求的分片大小、开始/结束控制帧和服务端日志。
- 不要把包含 Token 的完整 URL打印到控制台或工单。
8. droppedFrames 大于 0 会影响文件吗?
droppedFrames 统计的是实时 JS 回调链路为保持低延迟而丢弃的帧。文件由原生链路直接写入,通常不受 JS 消费速度影响。若业务要求每个实时帧绝不丢失,应降低回调频率、避免同步重任务,并结合服务端容错;移动端实时链路不应假设任何设备上都能绝对零丢帧。
9. VAD 能不能自动删除静音?
不能。VAD 只报告 silence/speech,不裁剪文件、不停止录音。业务可以根据事件决定 UI、ASR 发送策略或是否主动停止,但应避免误判导致用户内容丢失。
10. asrTts 能完全消除扬声器回声吗?
不能。它只提供适合“录音同时播放”的音频会话预设,不承诺 AEC。强回声消除请使用专业 RTC/音频处理方案,并在目标设备验证。
11. 蓝牙耳机切换后为什么录音变化或中断?
蓝牙、有线耳机、USB 麦克风的接入会改变系统输入路由、实际采样率或可用声道。监听 routeChange 和 interruption;0.1.0 只报告路由变化,不提供手动强制选择输入设备。
12. 能否同时创建多个 recorder?
getRecorderManager() 返回单例,同一时间只支持一个活动录音会话。多页面共用时应在业务层集中管理,不要让不同页面同时调用 start()。
13. 修改 Kotlin/Swift 后为什么行为没变化?
原生代码不随普通热刷新更新。请停止运行、重新制作/运行自定义基座或重新云打包。
14. 为什么会议预设没有实时帧和 VAD?
meeting 默认面向长录音,关闭高频帧和 VAD 以减少 JS 压力。如确实需要,可显式设置 enableFrameCallback:true 和 vad.enabled:true,并进行长时间性能测试。
性能与稳定性建议
- 实时 ASR 优先使用
asr16k,不要为了“更清晰”盲目提高采样率;云 ASR 不一定接受 48kHz。 - Android 传统 uni-app 存在原生 Base64 桥接再还原
ArrayBuffer的成本,建议帧长使用 40ms 或更高。 frameDurationMs越小,实时性越高,但 JS 调用次数和调度压力越大。onFrameRecorded内只做轻量入队/发送;波形计算、编码或落盘应节流或移到独立链路。- 不需要实时帧时关闭
enableFrameCallback。 - 长录音优先选择 M4A,并使用不少于 60 秒的合理分片。
- 每秒观察
stats;如果pendingFrameCallbacks持续升高或出现丢帧,先排查 JS 回调耗时。 - 开始前保留足够存储空间,并给业务设置明确的文件清理周期。
- 后台/息屏、来电、Siri、蓝牙切换、低电量、省电模式必须在目标品牌真机测试。
- 上线前至少完成 30 分钟连续录音、暂停恢复、分片、空间不足和异常中断测试。
文件管理建议
- 插件默认把文件写入应用沙箱,不申请公共存储权限。
- 需要长期保留的录音,应由业务层在
stop()成功后登记、迁移或上传。 - 上传前校验
fileSize、duration和finalizeFailed。 - 用户取消、退出草稿或撤回同意时,业务应按产品规则删除对应文件。
- 不要无限期保留会议、访谈、客服等可能包含个人信息的音频。
cleanup()是兜底工具,不代替业务数据库与文件的一致性管理。
隐私、权限与合规说明
核心插件行为
- 不包含广告。
- 不读取 IMEI、MAC、广告 ID、通讯录、相册、定位等设备或个人信息。
- 不主动创建网络连接。
- 只在用户授权并由业务调用
start()后访问麦克风。 - 音频默认写入应用沙箱,或通过实时帧回调交给调用方内存。
- 不申请公共存储权限。
- 不支持通话录音、隐蔽录音或绕过系统提示。
使用云 ASR 时
云 ASR 网络连接由你的业务代码和所选服务商负责,不属于核心插件的自动行为。宿主 App 应在隐私政策和首次使用流程中如实说明:
- 采集的数据类型是语音/音频。
- 采集目的,例如实时转写、语音输入、会议纪要。
- 音频或识别文本发送给谁,包括业务服务器和云服务商。
- 保存位置、保存期限和删除方式。
- 用户如何停止录音、撤回授权和删除历史内容。
- 是否涉及跨境传输、未成年人或敏感个人信息。
请使用随机会话 ID 关联 ASR 请求,不要把 IMEI、MAC、手机号或可识别用户身份的值写入 cuid、URL 或日志。
本说明描述插件工程行为,不构成法律意见。上架和运营前请根据目标地区、应用市场、云服务商与实际业务完成隐私合规评估。
购买 / 接入前请确认
- 目标平台是 App-Android 或 App-iOS。
- 项目可以使用 HBuilderX 4.61+ 和 UTS 插件。
- 需求是 App 麦克风采集,不是通话录音、系统内录或静默后台录音。
- 可以接受输出只支持 16bit PCM、WAV 和 M4A,不需要 MP3。
- 如果接入实时 ASR,业务方自行准备云账号、计费、服务端签名与短期凭据接口。
- 如果需要后台录音,可以接受系统可见通知、麦克风指示和操作系统限制。
- 如果需要边播 TTS 边录音,可以接受 0.1.0 不承诺 AEC。
- 已计划在目标 Android 品牌、iOS 版本、蓝牙设备和正式云包上试用验证。
如果以上任一项与业务不符,建议先咨询或使用试用版验证,不要仅根据 API 名称判断可用性。
更新记录
0.1.0
- 首个公开版本:Android/iOS UTS 录音 API。
- 支持 PCM16LE、WAV、M4A(AAC-LC)。
- 支持实时 PCM 帧、dBFS、基础能量 VAD 和帧统计。
- 支持暂停、恢复、停止、取消、限时、分片和低存储保护。
- 支持后台录音、系统中断和输入路由变化事件。
- 提供阿里云、百度智能云、腾讯云实时 ASR 示例与本地 Mock。
- 核心插件不联网、不内置云厂商长期密钥。

收藏人数:
购买普通授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 26
赞赏 0
下载 12424854
赞赏 1934
赞赏
京公网安备:11010802035340号