This commit is contained in:
Home
2025-12-01 03:42:34 +08:00
parent 492a164bff
commit fde86ef902
1917 changed files with 21835 additions and 214147 deletions
@@ -0,0 +1,17 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style>
/*每个页面公共css */
</style>
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
@@ -0,0 +1,22 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif
@@ -0,0 +1,66 @@
{
"name": "RecorderManager-onFrameRecorded",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {
"Record": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "2"
}
@@ -0,0 +1,17 @@
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "录音实时帧回调"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
@@ -0,0 +1,101 @@
<template>
<view class="content">
<view class="start-record" @click="onStartRecord">开始录音</view>
<view class="stop-record" @click="onStopRecord">停止录音</view>
<view class="frame">
<view>实时回调是否是最后一帧{{isLastFrame}}</view>
<view class="frameBuffer">{{frameBuffer}}</view>
</view>
<yao-RecordFrame
ref="recordFrame"
@onFrameRecorded="frameRecorded"
@currentDecibels="onCurrentDecibels"
@onStop="stopIt"></yao-RecordFrame>
</view>
</template>
<script>
export default {
data() {
return {
frameBuffer:'',//实时帧的frameBuffer值
isLastFrame:false,//是否是最后一帧
vuLevel:0
}
},
onLoad() {
},
methods: {
onStartRecord(){
//开启录音(仅支持两种参数)
this.$refs.recordFrame.start({
sampleRate:16000,
frameSize:1024,
gain:1.0 //增益值,数字越高音频声音越大 1.0~20.0
});
this.isLastFrame='false';
},
onStopRecord(){
//停止录音
this.$refs.recordFrame.stop();
},
//停止录音
stopIt(base64){
//base64音频只能在浏览器播放
//也可以base64音频转成文件音频可app播放
console.log(base64);
},
//帧回调
frameRecorded({isLastFrame,frameBuffer}){
//console.log(isLastFrame,frameBuffer);
if(!isLastFrame){
this.frameBuffer=frameBuffer;
}
if(this.isLastFrame!='true'){
this.isLastFrame=isLastFrame?'true':'false';
}
},
onCurrentDecibels(decibels){
//当前分贝值 最小可测分贝(-80) 最大可测分贝(-30)
console.log("当前分贝:" + decibels)
}
}
}
</script>
<style>
.start-record{
background:#007aff;
width:90%;
margin:20rpx auto;
color:#fff;
padding:20rpx 0;
text-align:center;
border-radius: 20rpx;
}
.stop-record{
background:#e64340;
width:90%;
margin:20rpx auto;
color:#fff;
padding:20rpx 0;
text-align:center;
border-radius: 20rpx;
}
.frame{
width:90%;
margin:0 auto;
}
.frameBuffer{
width: 100%;
word-wrap: break-word;
word-break: break-all;
}
</style>
@@ -0,0 +1,72 @@
# yao-RecordFrame
##配置
需要将模块下uni_modules/yao-RecordFrame的dist复制到static目录下面
或者
将uni_modules/yao-RecordFrame/dist目录的配置文件放到static/dist目录下面
###说明
插件只适用于接实时语音识别的模型,不适合录音上传
### 示例代码
```javascript
<template>
<view class="content">
<view class="start-record" @click="onStartRecord">开始录音</view>
<view class="stop-record" @click="onStopRecord">停止录音</view>
<view class="frame">
<view>实时回调是否是最后一帧{{isLastFrame}}</view>
<view class="frameBuffer">{{frameBuffer}}</view>
</view>
<yao-RecordFrame
ref="recordFrame"
@onFrameRecorded="frameRecorded"
@onStop="stopIt"></yao-RecordFrame>
</view>
</template>
<script>
export default {
data() {
return {
frameBuffer:'',//实时帧的frameBuffer值
isLastFrame:false,//是否是最后一帧
}
},
onLoad() {
},
methods: {
onStartRecord(){
//开启录音(仅支持两种参数)
this.$refs.recordFrame.start({
sampleRate:16000,
frameSize:1024
})
},
onStopRecord(){
//停止录音
this.$refs.recordFrame.stop();
},
//停止录音
stopIt(base64){
//base64音频只能在浏览器播放
//也可以base64音频转成文件音频可app播放
console.log(base64);
},
frameRecorded({isLastFrame,frameBuffer}){
console.log(isLastFrame,frameBuffer);
if(!isLastFrame){
this.frameBuffer=frameBuffer;
}
this.isLastFrame=isLastFrame?'true':'false';
}
}
}
</script>
```
@@ -0,0 +1,300 @@
! function() {
"use strict";
function t(t, e) {
for (var r = 0; r < e.length; r++) {
var n = e[r];
n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object
.defineProperty(t, (i = n.key, o = void 0, "symbol" == typeof(o = function(t, e) {
if ("object" != typeof t || null === t) return t;
var r = t[Symbol.toPrimitive];
if (void 0 !== r) {
var n = r.call(t, e || "default");
if ("object" != typeof n) return n;
throw new TypeError("@@toPrimitive must return a primitive value.")
}
return ("string" === e ? String : Number)(t)
}(i, "string")) ? o : String(o)), n)
}
var i, o
}
function e(t) {
return e = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function(t) {
return t.__proto__ || Object.getPrototypeOf(t)
}, e(t)
}
function r(t, e) {
return r = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function(t, e) {
return t.__proto__ = e, t
}, r(t, e)
}
function n() {
if ("undefined" == typeof Reflect || !Reflect.construct) return !1;
if (Reflect.construct.sham) return !1;
if ("function" == typeof Proxy) return !0;
try {
return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function() {}))), !0
} catch (t) {
return !1
}
}
function i(t, e, o) {
return i = n() ? Reflect.construct.bind() : function(t, e, n) {
var i = [null];
i.push.apply(i, e);
var o = new(Function.bind.apply(t, i));
return n && r(o, n.prototype), o
}, i.apply(null, arguments)
}
function o(t) {
var n = "function" == typeof Map ? new Map : void 0;
return o = function(t) {
if (null === t || (o = t, -1 === Function.toString.call(o).indexOf("[native code]"))) return t;
var o;
if ("function" != typeof t) throw new TypeError("Super expression must either be null or a function");
if (void 0 !== n) {
if (n.has(t)) return n.get(t);
n.set(t, a)
}
function a() {
return i(t, arguments, e(this).constructor)
}
return a.prototype = Object.create(t.prototype, {
constructor: {
value: a,
enumerable: !1,
writable: !0,
configurable: !0
}
}), r(a, t)
}, o(t)
}
function a(t) {
if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return t
}
function s(t) {
var r = n();
return function() {
var n, i = e(t);
if (r) {
var o = e(this).constructor;
n = Reflect.construct(i, arguments, o)
} else n = i.apply(this, arguments);
return function(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError(
"Derived constructors may only return object or undefined");
return a(t)
}(this, n)
}
}
function f(t) {
return function(t) {
if (Array.isArray(t)) return u(t)
}(t) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array
.from(t)
}(t) || function(t, e) {
if (!t) return;
if ("string" == typeof t) return u(t, e);
var r = Object.prototype.toString.call(t).slice(8, -1);
"Object" === r && t.constructor && (r = t.constructor.name);
if ("Map" === r || "Set" === r) return Array.from(t);
if ("Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)) return u(t, e)
}(t) || function() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
)
}()
}
function u(t, e) {
(null == e || e > t.length) && (e = t.length);
for (var r = 0, n = new Array(e); r < e; r++) n[r] = t[r];
return n
}
function l(t, e, r, n) {
this.fromSampleRate = t, this.toSampleRate = e, this.channels = 0 | r, this.noReturn = !!n, this.initialize()
}
l.prototype.initialize = function() {
if (!(this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0)) throw new Error(
"Invalid settings specified for the resampler.");
this.fromSampleRate == this.toSampleRate ? (this.resampler = this.bypassResampler, this.ratioWeight = 1) : (
this.fromSampleRate < this.toSampleRate ? (this.lastWeight = 1, this.resampler = this
.compileLinearInterpolation) : (this.tailExists = !1, this.lastWeight = 0, this.resampler = this
.compileMultiTap), this.ratioWeight = this.fromSampleRate / this.toSampleRate)
}, l.prototype.compileLinearInterpolation = function(t) {
var e = t.length;
this.initializeBuffers(e);
var r, n, i = this.outputBufferSize,
o = this.ratioWeight,
a = this.lastWeight,
s = 0,
f = 0,
u = 0,
l = this.outputBuffer;
if (e % this.channels == 0) {
if (e > 0) {
for (; a < 1; a += o)
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = this.lastOutput[r] * s + t[
r] * f;
for (a--, e -= this.channels, n = Math.floor(a) * this.channels; u < i && n < e;) {
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = t[n + r] * s + t[n + this
.channels + r] * f;
a += o, n = Math.floor(a) * this.channels
}
for (r = 0; r < this.channels; ++r) this.lastOutput[r] = t[n++];
return this.lastWeight = a % 1, this.bufferSlice(u)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.compileMultiTap = function(t) {
var e = [],
r = t.length;
this.initializeBuffers(r);
var n = this.outputBufferSize;
if (r % this.channels == 0) {
if (r > 0) {
for (var i = this.ratioWeight, o = 0, a = 0; a < this.channels; ++a) e[a] = 0;
var s = 0,
f = 0,
u = !this.tailExists;
this.tailExists = !1;
var l = this.outputBuffer,
h = 0,
c = 0;
do {
if (u)
for (o = i, a = 0; a < this.channels; ++a) e[a] = 0;
else {
for (o = this.lastWeight, a = 0; a < this.channels; ++a) e[a] += this.lastOutput[a];
u = !0
}
for (; o > 0 && s < r;) {
if (!(o >= (f = 1 + s - c))) {
for (a = 0; a < this.channels; ++a) e[a] += t[s + a] * o;
c += o, o = 0;
break
}
for (a = 0; a < this.channels; ++a) e[a] += t[s++] * f;
c = s, o -= f
}
if (0 != o) {
for (this.lastWeight = o, a = 0; a < this.channels; ++a) this.lastOutput[a] = e[a];
this.tailExists = !0;
break
}
for (a = 0; a < this.channels; ++a) l[h++] = e[a] / i
} while (s < r && h < n);
return this.bufferSlice(h)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.bypassResampler = function(t) {
return this.noReturn ? (this.outputBuffer = t, t.length) : t
}, l.prototype.bufferSlice = function(t) {
if (this.noReturn) return t;
try {
return this.outputBuffer.subarray(0, t)
} catch (e) {
try {
return this.outputBuffer.length = t, this.outputBuffer
} catch (e) {
return this.outputBuffer.slice(0, t)
}
}
}, l.prototype.initializeBuffers = function(t) {
this.outputBufferSize = Math.ceil(t * this.toSampleRate / this.fromSampleRate);
try {
this.outputBuffer = new Float32Array(this.outputBufferSize), this.lastOutput = new Float32Array(this
.channels)
} catch (t) {
this.outputBuffer = [], this.lastOutput = []
}
};
var h = function(e) {
! function(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError(
"Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: !0,
configurable: !0
}
}), Object.defineProperty(t, "prototype", {
writable: !1
}), e && r(t, e)
}(h, e);
var n, i, o, u = s(h);
function h() {
var t;
! function(t, e) {
if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function")
}(this, h);
var e = a(t = u.call(this));
return t.port.onmessage = function(t) {
var r = t.data,
n = r.type,
i = r.data;
if (console.log("type", n), "init" === n) {
var o = i.frameSize,
a = i.toSampleRate,
s = i.arrayBufferType,
f = i.fromSampleRate;
return e.frameSize = o * Math.floor(f / a), e.resampler = new l(f, a, 1), e
.frameBuffer = [], void(e.arrayBufferType = s),e.gain = i.gain || 1.0;
}
"stop" === n && (e.port.postMessage({
frameBuffer: e.transData(e.frameBuffer),
isLastFrame: !0
}), e.frameBuffer = [])
}, t
}
return n = h, (i = [{
key: "process",
value: function(t) {
var e, r = t[0][0];
return this.frameSize ? ((e = this.frameBuffer).push.apply(e, f(r)), this
.frameBuffer.length >= this.frameSize && (this.port.postMessage({
frameBuffer: this.transData(this.frameBuffer),
isLastFrame: !1
}), this.frameBuffer = []), !0) : (r && this.port.postMessage({
frameBuffer: this.transData(r),
isLastFrame: !1
}), !0)
}
}, {
key: "transData",
value: function(t) {
const gain = this.gain; // 增益系数,可根据实际需求修改
t = t.map(sample => sample * gain); // 对每个样本应用增益
return "short16" === this.arrayBufferType && (t = function(t) {
for (var e = new ArrayBuffer(2 * t.length), r = new DataView(e), n = 0,
i = 0; i < t.length; i += 1, n += 2) {
var o = Math.max(-1, Math.min(1, t[i]));
r.setInt16(n, o < 0 ? 32768 * o : 32767 * o, !0)
}
return r.buffer
}(t = this.resampler.resampler(t))), t
}
}]) && t(n.prototype, i), o && t(n, o), Object.defineProperty(n, "prototype", {
writable: !1
}), h
}(o(AudioWorkletProcessor));
registerProcessor("processor-worklet", h)
}();
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});
@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
@@ -0,0 +1,11 @@
## 1.0.52025-09-03
新增分贝值
## 1.0.42025-08-30
加大增益值范围
## 1.0.32025-08-29
增益
## 1.0.22025-08-29
添加新属性:增益
## 1.0.02025-08-02
# yao-RecordFrame
web audio api录音
@@ -0,0 +1,332 @@
<template>
<view
:options="options"
:change:options="record.startRecord"
:status="status"
:change:status="record.onStop"
>
</view>
</template>
<script>
export default{
data(){
return{
options:null,
status:null
}
},
methods:{
start(option){
this.options=option;
this.status='start';
},
stop(){
this.status='stop';
this.options=null;
},
frameRecorded({isLastFrame,frameBuffer}){
this.$emit('onFrameRecorded',{isLastFrame,frameBuffer:this.base64ToUint8Array(frameBuffer)})
},
decibels(value){
this.$emit('currentDecibels',value);
},
base64ToUint8Array(base64) {
const binaryString = atob(base64.split(',')[1] || base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
},
toShowToast(){
uni.showToast({
title:'发生错误,请检查是否有麦克风权限',
icon:'none'
});
this.stop();
},
recordedChunks(base64){
this.$emit('onStop',base64)
},
}
}
</script>
<script module="record" lang="renderjs">
// 保存需要关闭的引用
let mediaStream;
let audioContext;
let processor;
// 全局变量存储录音数据
let recordedChunks = [];
let decibelHistory = []; // 存储分贝历史数据用于可视化
// 分贝计算相关配置
const DB_CONFIG = {
minDecibels: -80, // 最小可测分贝
maxDecibels: -30, // 最大可测分贝
smoothingTimeConstant: 0.8 // 平滑系数,使分贝变化更平缓
};
export default{
data(){
return{
}
},
methods:{
async startRecord(options){
if(options==null) return;
if (audioContext) return;
try{
// 配置参数
var a, i = options.sampleRate,
s = options.frameSize;
if(options.gain<1.0){
options.gain=1.0;
}
if(options.gain>20.0){
options.gain=20.0;
}
audioContext = new AudioContext();
// 加载并初始化AudioWorklet
await audioContext.audioWorklet.addModule('static/dist/processor.worklet.js');
//const mediaStream = new MediaStream();
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: options.sampleRate,
channelCount: 1
}
});
const source = audioContext.createMediaStreamSource(mediaStream);
processor = new AudioWorkletNode(audioContext, 'processor-worklet');
// 初始化处理器
processor.port.postMessage({
type: 'init',
data: {
frameSize: options.frameSize, // 样本数 (1280字节 / 2字节每样本)
fromSampleRate: 48000, // 输入采样率
toSampleRate: options.sampleRate, // 输出采样率 (1/3)
arrayBufferType: 'short16',
gain:options.gain
}
});
// 接收40ms间隔的音频数据
processor.port.onmessage = (t) => {
var r = t.data,
o = r.frameBuffer,
n = r.isLastFrame;
// 计算当前帧的分贝值
if (o && o.byteLength > 0) {
const decibels = this.calculateDecibels(o);
this.onDecibelsCalculated(decibels); // 触发分贝回调
// 存储历史数据,限制长度
decibelHistory.push(decibels);
if (decibelHistory.length > 100) {
decibelHistory.shift();
}
}
if (null == o ? void 0 : o.byteLength)
for (var a = 0; a < o.byteLength;) {
const frameData = {
isLastFrame: n && a + s >= o.byteLength,
frameBuffer: t.data.frameBuffer.slice(a, a + s)
};
this.onFrameRecorded(frameData);
// 存储录音数据(仅在非暂停状态)
recordedChunks.push(frameData.frameBuffer);
a += s;
}
else this.onFrameRecorded(t.data);
};
source.connect(processor);
processor.connect(audioContext.destination);
}catch(err){
this.$ownerInstance.callMethod('toShowToast');
}
},
onFrameRecorded({isLastFrame,frameBuffer}){
this.$ownerInstance.callMethod('frameRecorded', {isLastFrame,frameBuffer:this.toBase64(frameBuffer)});
},
calculateDecibels(frameBuffer) {
try {
// 将帧数据转换为16位整数数组
const samples = new Int16Array(frameBuffer);
// 计算均方根(RMS)
let sum = 0;
for (let i = 0; i < samples.length; i++) {
const value = samples[i] / 32768; // 归一化到[-1, 1]范围
sum += value * value; // 平方和
}
const rms = Math.sqrt(sum / samples.length);
// 防止log(0)错误
if (rms < 0.00001) {
return DB_CONFIG.minDecibels;
}
// 转换为分贝 (20 * log10(rms))
let db = 20 * Math.log10(rms);
// 应用平滑处理
if (decibelHistory.length > 0) {
const lastDb = decibelHistory[decibelHistory.length - 1];
db = lastDb * DB_CONFIG.smoothingTimeConstant + db * (1 - DB_CONFIG.smoothingTimeConstant);
}
// 限制分贝范围
return Math.max(DB_CONFIG.minDecibels, Math.min(DB_CONFIG.maxDecibels, db));
} catch (e) {
console.error('计算分贝时出错:', e);
return DB_CONFIG.minDecibels;
}
},
onDecibelsCalculated(decibels) {
//console.log(`当前分贝: ${decibels.toFixed(1)} dB`);
this.$ownerInstance.callMethod('decibels', decibels.toFixed(1));
// 可以在这里添加分贝可视化逻辑
// 例如更新UI显示当前音量
},
toBase64(buffer){
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
},
async onRecordedChunks(chunks){
var mergedBuffer=this.mergeAudioBuffers(chunks);
// 2. 将合并的二进制流转换为WAV格式(需要添加WAV文件头)
const wavBlob = this.createWavBlob(mergedBuffer, 1, 16000); // 单声道,44100Hz
// 3. 将WAV转为Base64(可选,若需传输)
const base64 = await this.blobToBase64(wavBlob);
this.$ownerInstance.callMethod('recordedChunks',base64)
},
onStop(value){
if(value!=='stop') return;
this.onFrameRecorded({isLastFrame:true,frameBuffer:''});
this.onRecordedChunks(recordedChunks);
recordedChunks = [];
if (mediaStream) {
// 停止所有媒体轨道
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
if (processor) {
// 断开音频节点连接
processor.disconnect();
processor = null;
}
if (audioContext) {
// 关闭音频上下文
audioContext.close().then(() => {
audioContext = null;
});
}
},
//合并所有buffer
mergeAudioBuffers(buffers) {
let totalLength = buffers.reduce((acc, buf) => acc + buf.byteLength, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
buffers.forEach(buffer => {
result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
});
// 验证合并后的长度是否正确
if (offset !== totalLength) console.error("合并后的长度不符!");
return result.buffer;
},
// 新增WAV封装函数(基于前序回答的createWavBlob
createWavBlob(pcmData, numChannels, sampleRate) {
const bytesPerSample = 2; // 16-bit PCM
const blockAlign = numChannels * bytesPerSample;
const byteRate = sampleRate * blockAlign;
const bufferLength = pcmData.byteLength;
const totalLength = 44 + bufferLength; // WAV头(44字节) + 音频数据
const buffer = new ArrayBuffer(totalLength);
const view = new DataView(buffer);
// 写入WAV文件头(RIFF、fmt、data区块)
this.writeString(view, 0, 'RIFF');
view.setUint32(4, totalLength - 8, true);
this.writeString(view, 8, 'WAVE');
this.writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true); // fmt区块大小
view.setUint16(20, 1, true); // PCM格式
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, 16, true); // 16位采样
this.writeString(view, 36, 'data');
view.setUint32(40, bufferLength, true);
// 写入音频数据(假设pcmData是Uint8Array,需转换为16位PCM
const pcm16 = new Int16Array(pcmData);
for (let i = 0; i < pcm16.length; i++) {
view.setInt16(44 + i * 2, pcm16[i], true);
}
return new Blob([buffer], { type: 'audio/wav' });
},
// 辅助函数:Blob转Base64
blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
},
// 辅助函数:字符串写入DataView
writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,300 @@
! function() {
"use strict";
function t(t, e) {
for (var r = 0; r < e.length; r++) {
var n = e[r];
n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object
.defineProperty(t, (i = n.key, o = void 0, "symbol" == typeof(o = function(t, e) {
if ("object" != typeof t || null === t) return t;
var r = t[Symbol.toPrimitive];
if (void 0 !== r) {
var n = r.call(t, e || "default");
if ("object" != typeof n) return n;
throw new TypeError("@@toPrimitive must return a primitive value.")
}
return ("string" === e ? String : Number)(t)
}(i, "string")) ? o : String(o)), n)
}
var i, o
}
function e(t) {
return e = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function(t) {
return t.__proto__ || Object.getPrototypeOf(t)
}, e(t)
}
function r(t, e) {
return r = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function(t, e) {
return t.__proto__ = e, t
}, r(t, e)
}
function n() {
if ("undefined" == typeof Reflect || !Reflect.construct) return !1;
if (Reflect.construct.sham) return !1;
if ("function" == typeof Proxy) return !0;
try {
return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function() {}))), !0
} catch (t) {
return !1
}
}
function i(t, e, o) {
return i = n() ? Reflect.construct.bind() : function(t, e, n) {
var i = [null];
i.push.apply(i, e);
var o = new(Function.bind.apply(t, i));
return n && r(o, n.prototype), o
}, i.apply(null, arguments)
}
function o(t) {
var n = "function" == typeof Map ? new Map : void 0;
return o = function(t) {
if (null === t || (o = t, -1 === Function.toString.call(o).indexOf("[native code]"))) return t;
var o;
if ("function" != typeof t) throw new TypeError("Super expression must either be null or a function");
if (void 0 !== n) {
if (n.has(t)) return n.get(t);
n.set(t, a)
}
function a() {
return i(t, arguments, e(this).constructor)
}
return a.prototype = Object.create(t.prototype, {
constructor: {
value: a,
enumerable: !1,
writable: !0,
configurable: !0
}
}), r(a, t)
}, o(t)
}
function a(t) {
if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return t
}
function s(t) {
var r = n();
return function() {
var n, i = e(t);
if (r) {
var o = e(this).constructor;
n = Reflect.construct(i, arguments, o)
} else n = i.apply(this, arguments);
return function(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError(
"Derived constructors may only return object or undefined");
return a(t)
}(this, n)
}
}
function f(t) {
return function(t) {
if (Array.isArray(t)) return u(t)
}(t) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array
.from(t)
}(t) || function(t, e) {
if (!t) return;
if ("string" == typeof t) return u(t, e);
var r = Object.prototype.toString.call(t).slice(8, -1);
"Object" === r && t.constructor && (r = t.constructor.name);
if ("Map" === r || "Set" === r) return Array.from(t);
if ("Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)) return u(t, e)
}(t) || function() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
)
}()
}
function u(t, e) {
(null == e || e > t.length) && (e = t.length);
for (var r = 0, n = new Array(e); r < e; r++) n[r] = t[r];
return n
}
function l(t, e, r, n) {
this.fromSampleRate = t, this.toSampleRate = e, this.channels = 0 | r, this.noReturn = !!n, this.initialize()
}
l.prototype.initialize = function() {
if (!(this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0)) throw new Error(
"Invalid settings specified for the resampler.");
this.fromSampleRate == this.toSampleRate ? (this.resampler = this.bypassResampler, this.ratioWeight = 1) : (
this.fromSampleRate < this.toSampleRate ? (this.lastWeight = 1, this.resampler = this
.compileLinearInterpolation) : (this.tailExists = !1, this.lastWeight = 0, this.resampler = this
.compileMultiTap), this.ratioWeight = this.fromSampleRate / this.toSampleRate)
}, l.prototype.compileLinearInterpolation = function(t) {
var e = t.length;
this.initializeBuffers(e);
var r, n, i = this.outputBufferSize,
o = this.ratioWeight,
a = this.lastWeight,
s = 0,
f = 0,
u = 0,
l = this.outputBuffer;
if (e % this.channels == 0) {
if (e > 0) {
for (; a < 1; a += o)
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = this.lastOutput[r] * s + t[
r] * f;
for (a--, e -= this.channels, n = Math.floor(a) * this.channels; u < i && n < e;) {
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = t[n + r] * s + t[n + this
.channels + r] * f;
a += o, n = Math.floor(a) * this.channels
}
for (r = 0; r < this.channels; ++r) this.lastOutput[r] = t[n++];
return this.lastWeight = a % 1, this.bufferSlice(u)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.compileMultiTap = function(t) {
var e = [],
r = t.length;
this.initializeBuffers(r);
var n = this.outputBufferSize;
if (r % this.channels == 0) {
if (r > 0) {
for (var i = this.ratioWeight, o = 0, a = 0; a < this.channels; ++a) e[a] = 0;
var s = 0,
f = 0,
u = !this.tailExists;
this.tailExists = !1;
var l = this.outputBuffer,
h = 0,
c = 0;
do {
if (u)
for (o = i, a = 0; a < this.channels; ++a) e[a] = 0;
else {
for (o = this.lastWeight, a = 0; a < this.channels; ++a) e[a] += this.lastOutput[a];
u = !0
}
for (; o > 0 && s < r;) {
if (!(o >= (f = 1 + s - c))) {
for (a = 0; a < this.channels; ++a) e[a] += t[s + a] * o;
c += o, o = 0;
break
}
for (a = 0; a < this.channels; ++a) e[a] += t[s++] * f;
c = s, o -= f
}
if (0 != o) {
for (this.lastWeight = o, a = 0; a < this.channels; ++a) this.lastOutput[a] = e[a];
this.tailExists = !0;
break
}
for (a = 0; a < this.channels; ++a) l[h++] = e[a] / i
} while (s < r && h < n);
return this.bufferSlice(h)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.bypassResampler = function(t) {
return this.noReturn ? (this.outputBuffer = t, t.length) : t
}, l.prototype.bufferSlice = function(t) {
if (this.noReturn) return t;
try {
return this.outputBuffer.subarray(0, t)
} catch (e) {
try {
return this.outputBuffer.length = t, this.outputBuffer
} catch (e) {
return this.outputBuffer.slice(0, t)
}
}
}, l.prototype.initializeBuffers = function(t) {
this.outputBufferSize = Math.ceil(t * this.toSampleRate / this.fromSampleRate);
try {
this.outputBuffer = new Float32Array(this.outputBufferSize), this.lastOutput = new Float32Array(this
.channels)
} catch (t) {
this.outputBuffer = [], this.lastOutput = []
}
};
var h = function(e) {
! function(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError(
"Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: !0,
configurable: !0
}
}), Object.defineProperty(t, "prototype", {
writable: !1
}), e && r(t, e)
}(h, e);
var n, i, o, u = s(h);
function h() {
var t;
! function(t, e) {
if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function")
}(this, h);
var e = a(t = u.call(this));
return t.port.onmessage = function(t) {
var r = t.data,
n = r.type,
i = r.data;
if (console.log("type", n), "init" === n) {
var o = i.frameSize,
a = i.toSampleRate,
s = i.arrayBufferType,
f = i.fromSampleRate;
return e.frameSize = o * Math.floor(f / a), e.resampler = new l(f, a, 1), e
.frameBuffer = [], void(e.arrayBufferType = s),e.gain = i.gain || 1.0;
}
"stop" === n && (e.port.postMessage({
frameBuffer: e.transData(e.frameBuffer),
isLastFrame: !0
}), e.frameBuffer = [])
}, t
}
return n = h, (i = [{
key: "process",
value: function(t) {
var e, r = t[0][0];
return this.frameSize ? ((e = this.frameBuffer).push.apply(e, f(r)), this
.frameBuffer.length >= this.frameSize && (this.port.postMessage({
frameBuffer: this.transData(this.frameBuffer),
isLastFrame: !1
}), this.frameBuffer = []), !0) : (r && this.port.postMessage({
frameBuffer: this.transData(r),
isLastFrame: !1
}), !0)
}
}, {
key: "transData",
value: function(t) {
const gain = this.gain; // 增益系数,可根据实际需求修改
t = t.map(sample => sample * gain); // 对每个样本应用增益
return "short16" === this.arrayBufferType && (t = function(t) {
for (var e = new ArrayBuffer(2 * t.length), r = new DataView(e), n = 0,
i = 0; i < t.length; i += 1, n += 2) {
var o = Math.max(-1, Math.min(1, t[i]));
r.setInt16(n, o < 0 ? 32768 * o : 32767 * o, !0)
}
return r.buffer
}(t = this.resampler.resampler(t))), t
}
}]) && t(n.prototype, i), o && t(n, o), Object.defineProperty(n, "prototype", {
writable: !1
}), h
}(o(AudioWorkletProcessor));
registerProcessor("processor-worklet", h)
}();
@@ -0,0 +1,100 @@
{
"id": "yao-RecordFrame",
"displayName": "web audio api录音+实时帧回调 支持 ios Android h5",
"version": "1.0.5",
"description": "web audio api录音、实时帧回调、分贝值",
"keywords": [
"ios",
"Android",
"h5",
"实时帧回调",
"分贝"
],
"repository": "",
"engines": {
"uni-app": "^4.07",
"uni-app-x": ""
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "3371387322"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "需要麦克风权限"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": "√",
"ios": "√",
"harmony": "x"
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
@@ -0,0 +1,78 @@
# yao-RecordFrame
##配置
需要将模块下uni_modules/yao-RecordFrame的dist复制到static目录下面
或者
将uni_modules/yao-RecordFrame/dist目录的配置文件放到static/dist目录下面
###说明
插件只适用于接实时语音识别的模型,不适合录音上传
### 示例代码
```javascript
<template>
<view class="content">
<view class="start-record" @click="onStartRecord">开始录音</view>
<view class="stop-record" @click="onStopRecord">停止录音</view>
<view class="frame">
<view>实时回调是否是最后一帧{{isLastFrame}}</view>
<view class="frameBuffer">{{frameBuffer}}</view>
</view>
<yao-RecordFrame
ref="recordFrame"
@onFrameRecorded="frameRecorded"
@currentDecibels="onCurrentDecibels"
@onStop="stopIt"></yao-RecordFrame>
</view>
</template>
<script>
export default {
data() {
return {
frameBuffer:'',//实时帧的frameBuffer值
isLastFrame:false,//是否是最后一帧
}
},
onLoad() {
},
methods: {
onStartRecord(){
//开启录音(仅支持两种参数)
this.$refs.recordFrame.start({
sampleRate:16000,
frameSize:1024,
gain:1.0 //增益值,数字越高音频声音越大 1.0~20.0
})
},
onStopRecord(){
//停止录音
this.$refs.recordFrame.stop();
},
//停止录音
stopIt(base64){
//base64音频只能在浏览器播放
//也可以base64音频转成文件音频可app播放
console.log(base64);
},
frameRecorded({isLastFrame,frameBuffer}){
console.log(isLastFrame,frameBuffer);
if(!isLastFrame){
this.frameBuffer=frameBuffer;
}
this.isLastFrame=isLastFrame?'true':'false';
},
onCurrentDecibels(decibels){
//当前分贝值 最小可测分贝(-80) 最大可测分贝(-30)
console.log("当前分贝:" + decibels)
}
}
}
</script>
```