uniapp中使用网页录音并上传声音文件(发语音)——js-audio-recorder的使用【伸手党福利】

article/2025/7/22 6:15:22

uniapp中上传音频只能在app或小程序当中实现,如何使用网页完成语音的录制和上传则成为了困扰前端童鞋的重点。
本文着重解决:
js-audio-recorder报 error:浏览器不支持getUserMedia ! 的问题。
js-audio-recorder报 NotFoundError : Requested device not found 的问题
js-audio-recorder的使用、demo以及文件上传的问题。

本篇文章主要讲如何使用uniapp(vue)在网页中(web模式下)录制声音并上传
js-audio-recorder插件支持在微信公众号(网页)、UC浏览器、Chrome及其内核的浏览器内运行。

文章前半部分先讲操作和demo,后半部分再讲原理和插件来源。

demo编写——前期准备:

  1. 安装插件
yarn add js-audio-recorder

如果需要转mp3加一句:

yarn add lamejs
  1. 建立vue文件

由于代码是从网上上复制下来的,个人做了一些修改,所以会看到一些注释。
此代码上传按钮会直接上传文件到后端,请修改为自己的后端地址


<template><div class="home" style="margin:1vw;"><Button type="success" @click="getPermission()" style="margin:1vw;">获取麦克风权限</Button><br/><Button type="info" @click="startRecorder()"  style="margin:1vw;">开始录音</Button><Button type="info" @click="resumeRecorder()" style="margin:1vw;">继续录音</Button><Button type="info" @click="pauseRecorder()" style="margin:1vw;">暂停录音</Button><Button type="info" @click="stopRecorder()" style="margin:1vw;">结束录音</Button><br/><Button type="success" @click="playRecorder()" style="margin:1vw;">录音播放</Button><Button type="success" @click="pausePlayRecorder()" style="margin:1vw;">暂停录音播放</Button><Button type="success" @click="resumePlayRecorder()" style="margin:1vw;">恢复录音播放</Button><Button type="success" @click="stopPlayRecorder()" style="margin:1vw;">停止录音播放</Button><br/><Button type="info" @click="getRecorder()" style="margin:1vw;">获取录音信息</Button><Button type="info" @click="downPCM()" style="margin:1vw;">下载PCM</Button><Button type="info" @click="downWAV()" style="margin:1vw;">下载WAV</Button><Button type="info" @click="getMp3Data()" style="margin:1vw;">下载MP3</Button><br/><Button type="error" @click="destroyRecorder()" style="margin:1vw;">销毁录音</Button><br/><Button type="error" @click="uploadWav()" style="margin:1vw;">上传录音</Button><br/><div style="width:100%;height:200px;border:1px solid red;"><canvas id="canvas"></canvas><span style="padding: 0 10%;"></span><canvas id="playChart"></canvas></div></div>
</template><script>import Recorder from 'js-audio-recorder'const lamejs = require('lamejs')const recorder = new Recorder({sampleBits: 16,                 // 采样位数,支持 8 或 16,默认是16sampleRate: 48000,              // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000numChannels: 1,                 // 声道,支持 1 或 2, 默认是1// compiling: false,(0.x版本中生效,1.x增加中)  // 是否边录边转换,默认是false})// 绑定事件-打印的是当前录音数据recorder.onprogress = function(params) {// console.log('--------------START---------------')// console.log('录音时长(秒)', params.duration);// console.log('录音大小(字节)', params.fileSize);// console.log('录音音量百分比(%)', params.vol);// console.log('当前录音的总数据([DataView, DataView...])', params.data);// console.log('--------------END---------------')}export default {name: 'home',data () {return {//波浪图-录音drawRecordId:null,oCanvas : null,ctx : null,//波浪图-播放drawPlayId:null,pCanvas : null,pCtx : null,}},mounted(){this.startCanvas();},methods: {/*** 波浪图配置* */startCanvas(){//录音波浪// this.oCanvas = document.getElementById('canvas');// this.ctx = this.oCanvas.getContext("2d");// //播放波浪// this.pCanvas = document.getElementById('playChart');// this.pCtx = this.pCanvas.getContext("2d");},/***  录音的具体操作功能* */// 开始录音startRecorder () {recorder.start().then(() => {// this.drawRecord();//开始绘制图片uni.showToast({title: '开始录音',})}, (error) => {// 出错了uni.showToast({title: `${error.name} : ${error.message}`,})console.log(`${error.name} : ${error.message}`);});},// 继续录音resumeRecorder () {recorder.resume()},// 暂停录音pauseRecorder () {recorder.pause();this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 结束录音stopRecorder () {recorder.stop()uni.showToast({title: '结束录音',})this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 录音播放playRecorder () {recorder.play();uni.showToast({title: '录音播放',})// this.drawPlay();//绘制波浪图},// 暂停录音播放pausePlayRecorder () {recorder.pausePlay()},// 恢复录音播放resumePlayRecorder () {recorder.resumePlay();this.drawPlay();//绘制波浪图},// 停止录音播放stopPlayRecorder () {recorder.stopPlay();},// 销毁录音destroyRecorder () {recorder.destroy().then(function() {recorder = null;this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;});},/***  获取录音文件* */getRecorder(){let toltime = recorder.duration;//录音总时长let fileSize = recorder.fileSize;//录音总大小//录音结束,获取取录音数据let PCMBlob = recorder.getPCMBlob();//获取 PCM 数据let wav = recorder.getWAVBlob();//获取 WAV 数据let channel = recorder.getChannelData();//获取左声道和右声道音频数据console.log(toltime);console.log(fileSize);// console.log(PCMBlob);console.log(wav);console.log(channel);console.log(recorder);},/***  下载录音文件* *///下载pcmdownPCM(){//这里传参进去的时文件名recorder.downloadPCM('新文件');},//下载wavdownWAV(){//这里传参进去的时文件名recorder.downloadWAV('新文件');},/***  获取麦克风权限* */getPermission(){Recorder.getPermission().then(() => {this.$Message.success('获取权限成功')uni.showToast({title: '没有找到您要查询的内容!'})}, (error) => {uni.showToast({title: `${error.name} : ${error.message}`,})console.log(`${error.name} : ${error.message}`);});},/*** 文件格式转换 wav-map3* */getMp3Data(){const mp3Blob = this.convertToMp3(recorder.getWAV());recorder.download(mp3Blob, 'recorder', 'mp3');},blobToFile(theBlob, fileName) {  //将blob转换为file  let file = new File([theBlob], fileName, {type: theBlob.type.split('/')[1], lastModified: Date.now()});  return file;  },convertToMp3(wavDataView) {// 获取wav头信息const wav = lamejs.WavHeader.readHeader(wavDataView); // 此处其实可以不用去读wav头信息,毕竟有对应的config配置const { channels, sampleRate } = wav;const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128);// 获取左右通道数据const result = recorder.getChannelData()const buffer = [];const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2);const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2);const remaining = leftData.length + (rightData ? rightData.length : 0);const maxSamples = 1152;for (let i = 0; i < remaining; i += maxSamples) {const left = leftData.subarray(i, i + maxSamples);let right = null;let mp3buf = null;if (channels === 2) {right = rightData.subarray(i, i + maxSamples);mp3buf = mp3enc.encodeBuffer(left, right);} else {mp3buf = mp3enc.encodeBuffer(left);}if (mp3buf.length > 0) {buffer.push(mp3buf);}}const enc = mp3enc.flush();if (enc.length > 0) {buffer.push(enc);}return new Blob(buffer, { type: 'audio/mp3' });},/*** 绘制波浪图-录音* */drawRecord () {//        // 用requestAnimationFrame稳定60fps绘制//        this.drawRecordId = requestAnimationFrame(this.drawRecord);//        // 实时获取音频大小数据//        let dataArray = recorder.getRecordAnalyseData(),//            bufferLength = dataArray.length;//        // 填充背景色//        this.ctx.fillStyle = 'rgb(200, 200, 200)';//        this.ctx.fillRect(0, 0, this.oCanvas.width, this.oCanvas.height);//        // 设定波形绘制颜色//        this.ctx.lineWidth = 2;//        this.ctx.strokeStyle = 'rgb(0, 0, 0)';//        this.ctx.beginPath();//        var sliceWidth = this.oCanvas.width * 1.0 / bufferLength, // 一个点占多少位置,共有bufferLength个点要绘制//                x = 0;          // 绘制点的x轴位置//        for (var i = 0; i < bufferLength; i++) {//          var v = dataArray[i] / 128.0;//          var y = v * this.oCanvas.height / 2;//          if (i === 0) {//            // 第一个点//            this.ctx.moveTo(x, y);//          } else {//            // 剩余的点//            this.ctx.lineTo(x, y);//          }//          // 依次平移,绘制所有点//          x += sliceWidth;//        }//        this.ctx.lineTo(this.oCanvas.width, this.oCanvas.height / 2);//        this.ctx.stroke();},/*** 绘制波浪图-播放* */drawPlay () {//        // 用requestAnimationFrame稳定60fps绘制//        this.drawPlayId = requestAnimationFrame(this.drawPlay);//        // 实时获取音频大小数据//        let dataArray = recorder.getPlayAnalyseData(),//                bufferLength = dataArray.length;//        // 填充背景色//        this.pCtx.fillStyle = 'rgb(200, 200, 200)';//        this.pCtx.fillRect(0, 0, this.pCanvas.width, this.pCanvas.height);//        // 设定波形绘制颜色//        this.pCtx.lineWidth = 2;//        this.pCtx.strokeStyle = 'rgb(0, 0, 0)';//        this.pCtx.beginPath();//        var sliceWidth = this.pCanvas.width * 1.0 / bufferLength, // 一个点占多少位置,共有bufferLength个点要绘制//                x = 0;          // 绘制点的x轴位置//        for (var i = 0; i < bufferLength; i++) {//          var v = dataArray[i] / 128.0;//          var y = v * this.pCanvas.height / 2;//          if (i === 0) {//            // 第一个点//            this.pCtx.moveTo(x, y);//          } else {//            // 剩余的点//            this.pCtx.lineTo(x, y);//          }//          // 依次平移,绘制所有点//          x += sliceWidth;//        }//        this.pCtx.lineTo(this.pCanvas.width, this.pCanvas.height / 2);//        this.pCtx.stroke();},uploadWav(){let file = this.blobToFile(recorder.getWAVBlob(),"1.wav")console.log("开始上传");console.log(file);uni.uploadFile({header: {},url: uni.getStorageSync('BaseUrl') +'/file/upload',// filePath: file,file:file,name: 'file',formData: {'user': 'test'},success: (uploadFileRes) => {console.log('/sys/common/static/scott/pic/' +JSON.parse(uploadFileRes.data).result);},complete: () => {},fail: (res) => {console.log(res)}})}},}
</script><style lang='less' scoped></style>
  1. 运行
    在这里插入图片描述

  2. 注意,调试环境这里会报错,所以开始解决报错问题:
    报错:error:浏览器不支持getUserMedia !

在这里插入图片描述
这里使用的chrome浏览器测试的,所以我们在chrome浏览器当中输入:

chrome://flags/#unsafely-treat-insecure-origin-as-secure

输入你的本地网址,改为enabled,选择重启浏览器按钮【生产环境当中由于是使用域名进行访问,所以就不会报错。】
在这里插入图片描述
原因及原理:使用js-audio-recorder报浏览器不支持getUserMedia

  1. 调试正常:(大前提你的电脑要求插入耳机/喇叭和麦克风【耳麦一体的需要转换为双插头】否则会报找不到设备)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

错误指引:

1. NotFoundError : Requested device not found

在这里插入图片描述
原因:没插麦克风
解决:插入麦克风并调整为音频主输入
在这里插入图片描述

2. 手机扫码显示不出来

在这里插入图片描述
原因:使用了localhost:8080作为url
解决:使用ip地址或者域名访问

3. 手机上访问不到或者无法测试(微信扫码也不行)

原因:只有谷歌内核的浏览器可以如此设置,想在手机上测试或使用请下载安卓版的谷歌浏览器或者使用域名访问。


插件和原理解说

插件官网(git)
https://github.com/2fps/recorder

文档
http://recorder.api.zhuyuntao.cn/Recorder/start.html

demo
https://recorder.zhuyuntao.cn/

uniapp文件上传函数解说
https://uniapp.dcloud.net.cn/api/request/network-file.html


http://chatgpt.dhexx.cn/article/4XVAyjmO.shtml

相关文章

前端实现在浏览器网页中录音

一、整体实现的思想 页面中实现录音需要使用浏览器提供的MediaRecorder API,所以要实现页面录音就需要浏览器支持MediaStream Recording相关的功能&#xff0c;即浏览器能够获取浏览器的录音权限。 页面内容&#xff0c;需要一个记录录音开始和结束的按钮&#xff0c;以及一个用…

基于神经网络的自适应最优控制

《 N e u r a l n e t w o r k a p p r o a c h t o c o n t i n u o u s − t i m e d i r e c t a d a p t i v e o p t i m a l c o n t r o l f o r p a r t i a l l y u n k n o w n n o n l i n e a r s y s t e m s 》 D r a g u n a V r a b i e ∗ , F r a n k L e w …

堆排序(排升序为啥建大堆,排降序为啥建小堆)

简介&#xff1a; 之前对堆排序认识的不是很透彻&#xff0c;今天回过头来再把堆排序的知识整理整理&#xff01;以及排升序为什么要建大堆&#xff0c;排降序要建小堆。 正文&#xff1a; 首先我们要知道&#xff1a;   ①堆的逻辑是一颗完全二叉树&#xff1b;   ②它使…

残差网络Residual Networks-残差网络的创建、训练、测试、调参

残差网络的创建、训练、测试、调参加粗样式 在Keras中实现残差网络模型的创建&#xff0c;并通过模型来实现对图片的分类。 残差网络的预备知识 网络越深越好&#xff1f; 随着网络层级的不断增加&#xff0c;模型精度不断得到提升&#xff0c;而当网络层级增加到一定的数目…

堆排序,为什么升序排列要建大堆,降序排列要建小堆

堆排序中用到了建立大小堆和向下调整的内容&#xff0c;对这些内容有些不了解的同学可以去补一补专门写堆的博客&#xff0c;方便更好的理解堆排序数据结构之堆&#xff08;Heap&#xff09;&#xff0c;堆的相关操作&#xff0c;用堆模拟优先级队列。 如果把待排序序列分为未排…

操作系统——动态分配算法(首次适应算法,最佳适应算法,最坏适应算法及回收)

操作系统——动态分配 写的时间早了&#xff0c;有些许漏洞和不足&#xff0c;请大家不要介意 分配方式可分为四类:单一连续分配、固定分区分配、动态分区分配以及动态可重定位分区分配算法四种方式&#xff0c;其中动态分区分配算法就是此实验的实验对象。动态分区分配又称为…

pandas中对列进行排序(单列/多列)/(升序/降序)/(多列升序,降序控制)

前言 我想把数据分析刻进DNA里 如下面的数据,对price,要进行最简单的升序操作: 这个好整: import pandas as pdshop pd.read_csv("data/shop.csv", index_colid)shop.sort_values(byprice, inplaceTrue)结果: 如果你想整活(被迫)要把数据进行降序排列: 按照降序,传…

最先适应法、最佳适应法、下次适配法、最差适配法

题&#xff1a; 分析&#xff1a; 1. 首先分析是不是最差适配法&#xff0c;最差适配法意思是首先找到最大的内存空间进行分配&#xff0c; 对于请求的5K存储空间&#xff0c;首先找到地址200K容量为56K的地方进行分配&#xff0c;然后剩余51K。 再对请求的15K进行分配&…

自适应滤波器更新算法-EP1

自适应滤波器更新算法-EP1 自适应滤波器是回声消除系统中非常重要的一个功能模块&#xff0c;而对于自适应滤波器来说&#xff0c;如果更新滤波器系数则是关键所在。本文将介绍几种现有的滤波器更新算法&#xff0c;并附上Matlab测试代码。 1、LMS算法 1.1算法原理 LMS算法即…

自适应神经网络算法原理,单神经元自适应控制

关于神经网络自适应控制求助 这句话你可以直接用&#xff0c;不用加引用。因为这句话是很容易验证的。在网络层数、隐含层节点数逐渐增加&#xff0c;训练次数增加之后&#xff0c;他的拟合能力也是不断增加的&#xff0c;所以说&#xff0c;他可以以任意精度逼近任何非线性连…

【转载】梯度下降算法的参数更新公式

NN这块的公式&#xff0c;前馈网络是矩阵乘法。损失函数的定义也是一定的。 但是如何更新参数看了不少描述&#xff0c;下面的叙述比较易懂的&#xff1a; 1、在吴恩达的CS229的讲义的第四页直接给出参数迭代公式 在UFLDL中反向传导算法一节也是直接给出的公式 2、例子&#x…

Java中Comparator的个人简单理解(升序降序)与使用

目录 Java自定义排序返回值简单记忆理解实践LInkedList升序&#xff08;默认情况&#xff09;降序 PriorityQueue升序下的小顶堆&#xff08;默认情况&#xff09;降序下的大顶堆 总结补充数组类型自定义排序降序排序 数组 Java自定义排序返回值简单记忆理解 默认情况下&#…

深度残差收缩网络(从信号降噪的角度进行理解)

本文探讨了深度残差收缩网络的另一种理解方式。 传统信号降噪算法的常见步骤是&#xff1a; ① 采用某种信号变换方法&#xff08;例如小波、经验模态分解&#xff09;&#xff0c;将含噪信号变换到另外一种形态&#xff08;例如小波系数、本征模态分量等&#xff09;。在这些…

NIPS 2016 深度学习 迁移学习 ---残差转移网络用于无监督领域自适应

深度学习的成功得益于大量的标注数据&#xff0c;而数据标注是非常消耗资源的。当一个问题中缺少标注数据时&#xff0c;可以从另一个源中所学知识迁移过来&#xff0c;并且用于新问题中。 清华大学的学者提出了一种新的方法&#xff08;https://arxiv.org/pdf/1602.04433.pdf&…

深度残差网络+自适应参数化ReLU激活函数(调参记录21)Cifar10~95.12%

本文在调参记录20的基础上,将残差模块的个数,从27个增加到60个,继续测试深度残差网络ResNet+自适应参数化ReLU激活函数在Cifar10数据集上的表现。 自适应参数化ReLU函数被放在了残差模块的第二个卷积层之后,这与Squeeze-and-Excitation Networks或者深度残差收缩网络是相似…

已知两个长度分别为m 和 n 的升序链表,合并降序链表,求时间复杂度

王道数据结构上一道题&#xff1a; 之前我看到一个电子版的书&#xff0c;上边答案解析写的有点错误&#xff0c; 听说有些同学买的实体书上&#xff0c;答案解析也是这样写的 这个是刊印错误&#xff0c;很显然2Max( m , n )大于等于 m n 而且这个解析也不够清晰。 解析&a…

波束形成 常见自适应波束形成算法信(干)噪比增益影响因素

0、其他补充 均匀线阵波束形成器的信噪比增益上确界可由下式表示&#xff1a; 其中为阵元数&#xff0c;所以为了方便起见&#xff0c;一般的稳健自适应波束形成算法在仿真过程中的阵元数量均设置为10。 阵列的导向矢量可由下式表示&#xff1a; 以首个阵元为参考阵元&#xff…

两个升序链表合并成一个降序链表的时间复杂度

王道考研P7 第六题 【2013年统考真题】已知两个长度分别为m和n的升序链表&#xff0c;若将它们合并为长度为mn的一个降序链表&#xff0c;则最坏情况下的时间复杂度是&#xff08;&#xff09; A. O(n) B. O(mn) C. O(min(m,n)) D. O(max(m,n)) 答案是D 注意&#xff0c;此题…

无线传感器网络路由优化中的能量均衡LEACH改进算法

文章目录 一、理论基础1、LEACH算法概述2、改进的LEACH算法 二、算法流程图三、仿真实验与分析四、参考文献 一、理论基础 1、LEACH算法概述 请参考这里。 2、改进的LEACH算法 改进的LEACH算法&#xff08;LEACH-N&#xff09;主要针对LEACH算法分簇阶段的缺陷而改进的&…

机器学习之自适应增强(Adaboost)

1.Adaboost简介 **Adaptive boosting(自适应增强)是一种迭代算法&#xff0c;其核心思想是针对同一个训练集训练不同的弱分类器&#xff0c;然后把这些弱分类器集合起来&#xff0c;构成一个强分类器&#xff0c;Adaboost可处理分类和回归问题。了解Adaboost算法之前&#xff…