更新记录

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 语音场景:内置 asr16kasrTtsmeetingcustom 四种预设。
  • 实时可观测:提供 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 保留字段

类型声明中包含 mixWithOthersnotification 配置,为后续版本预留。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 granteddenied 或运行时返回的权限状态
granted 是否已授权
requested 本次调用链是否发起过请求
canAskAgain 当前是否具备再次请求的条件;最终以系统行为为准

start(options)

开始录音。成功后返回实际会话参数并触发 start 事件。

const result = await recorder.start({ preset: "asr16k" });

返回字段:

字段 说明
sessionId 本次录音的随机会话 ID
state recording
actualInputSampleRate 设备原始输入采样率
sampleRate 插件标准化后的输出采样率
numberOfChannels 输出声道数
bitsPerSample 固定 16
format 保存文件格式

actualInputSampleRatesampleRate 不同不代表错误。插件会在原生层把设备实际输入转换为目标采样率。

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 silencespeech 切换
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 是业务首选字段,为 PCM16LE ArrayBuffer
  • 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_AUDIO
  • FOREGROUND_SERVICE
  • FOREGROUND_SERVICE_MICROPHONE
  • POST_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 方法失败时会抛出带 codedetails 的 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. 为什么页面波形是一条直线?

依次检查:

  1. 是否运行在 Android/iOS 真机,而不是 H5。
  2. 是否重新编译了包含原生插件的基座。
  3. 是否设置 enableFrameCallback:true
  4. 是否注册了 onFrameRecorded
  5. dBFS 是否始终接近 -96,以及麦克风是否被其他应用占用。
  6. 是否在收到帧后用阻塞代码卡住主线程。

5. 为什么录音文件没有路径?

常见原因:

  • 设置了 saveToFile:false
  • 开启了分片,此时顶层 tempFilePath 为空,应读取 segments
  • 调用了 cancel(),本次文件已删除。
  • 文件封装失败,检查 finalizeFailederror 事件和错误码。

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 麦克风的接入会改变系统输入路由、实际采样率或可用声道。监听 routeChangeinterruption;0.1.0 只报告路由变化,不提供手动强制选择输入设备。

12. 能否同时创建多个 recorder?

getRecorderManager() 返回单例,同一时间只支持一个活动录音会话。多页面共用时应在业务层集中管理,不要让不同页面同时调用 start()

13. 修改 Kotlin/Swift 后为什么行为没变化?

原生代码不随普通热刷新更新。请停止运行、重新制作/运行自定义基座或重新云打包。

14. 为什么会议预设没有实时帧和 VAD?

meeting 默认面向长录音,关闭高频帧和 VAD 以减少 JS 压力。如确实需要,可显式设置 enableFrameCallback:truevad.enabled:true,并进行长时间性能测试。


性能与稳定性建议

  • 实时 ASR 优先使用 asr16k,不要为了“更清晰”盲目提高采样率;云 ASR 不一定接受 48kHz。
  • Android 传统 uni-app 存在原生 Base64 桥接再还原 ArrayBuffer 的成本,建议帧长使用 40ms 或更高。
  • frameDurationMs 越小,实时性越高,但 JS 调用次数和调度压力越大。
  • onFrameRecorded 内只做轻量入队/发送;波形计算、编码或落盘应节流或移到独立链路。
  • 不需要实时帧时关闭 enableFrameCallback
  • 长录音优先选择 M4A,并使用不少于 60 秒的合理分片。
  • 每秒观察 stats;如果 pendingFrameCallbacks 持续升高或出现丢帧,先排查 JS 回调耗时。
  • 开始前保留足够存储空间,并给业务设置明确的文件清理周期。
  • 后台/息屏、来电、Siri、蓝牙切换、低电量、省电模式必须在目标品牌真机测试。
  • 上线前至少完成 30 分钟连续录音、暂停恢复、分片、空间不足和异常中断测试。

文件管理建议

  • 插件默认把文件写入应用沙箱,不申请公共存储权限。
  • 需要长期保留的录音,应由业务层在 stop() 成功后登记、迁移或上传。
  • 上传前校验 fileSizedurationfinalizeFailed
  • 用户取消、退出草稿或撤回同意时,业务应按产品规则删除对应文件。
  • 不要无限期保留会议、访谈、客服等可能包含个人信息的音频。
  • 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。
  • 核心插件不联网、不内置云厂商长期密钥。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android:RECORD_AUDIO、FOREGROUND_SERVICE、FOREGROUND_SERVICE_MICROPHONE、POST_NOTIFICATIONS(仅后台录音通知);iOS:麦克风权限与 audio 后台模式。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件核心不联网、不采集、不上传任何数据;音频仅在用户授权并调用录音后写入应用沙箱,或由调用方自行配置网络连接发送。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。