录音插件 js-audio-recorder
bug:本地调试调取不起来麦克风
- 浏览器配置安全域名 chrome://flags/
- Insecure origins treated as secure
- 输入域名即可
- 电脑需要连接上耳机
<template><div class="BaseRecorder"><div class="BaseRecorder-record"><el-button @click="startRecorder()">开始录音</el-button><el-button @click="pauseRecorder()">暂停录音</el-button><el-button @click="resumeRecorder()">继续录音</el-button><el-button @click="stopRecorder()">结束录音</el-button></div><div class="BaseRecorder-play"><el-button @click="playRecorder()">录音播放</el-button><el-button @click="pausePlayRecorder()">暂停录音播放</el-button><el-button @click="resumePlayRecorder()">恢复录音播放</el-button><el-button @click="stopPlayRecorder()">停止录音播放</el-button></div><div class="BaseRecorder-download"><el-button @click="downPCM()">下载PCM</el-button><el-button @click="downWAV()">下载WAV</el-button></div><div class="BaseRecorder-destroy"><el-button type="error" @click="destroyRecorder()">销毁录音</el-button></div><div class="BaseRecorder-wave"><canvas ref="record"></canvas><canvas ref="play"></canvas></div></div>
</template><script>
import Recorder from "js-audio-recorder";export default {name: "home",data() {return {recorder: null,// 波浪图-录音drawRecordId: null,// 波浪图-播放drawPlayId: null,};},mounted() {this.init();},methods: {// 初始化init() {this.recorder = new Recorder({// 采样位数,支持 8 或 16,默认是16sampleBits: 16,// 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值sampleRate: 48000,// 声道,支持 1 或 2, 默认是1numChannels: 1,// 是否边录边转换,默认是falsecompiling: false,});},// 开始录音startRecorder() {this.recorder.start().then(() => {console.log("开始录音", this.recorder);this.drawRecord();this.recorder.onprogress = (params) => {console.log(params);// 此处控制数据的收集频率if (this.recorder.config.compiling) {console.log("音频总数据:", params.data);}};// 定时获取录音的数据并播放this.recorder.config.compiling &&(playTimer = setInterval(() => {let newData = this.recorder.getNextData();if (!newData.length) {return;}let byteLength = newData[0].byteLength;let buffer = new ArrayBuffer(newData.length * byteLength);let dataView = new DataView(buffer);// 数据合并for (let i = 0, iLen = newData.length; i < iLen; ++i) {for (let j = 0, jLen = newData[i].byteLength; j < jLen; ++j) {dataView.setInt8(i * byteLength + j, newData[i].getInt8(j));}}// 将录音数据转成WAV格式,并播放let a = encodeWAV(dataView,config.sampleRate,config.sampleRate,config.numChannels,config.sampleBits);let blob = new Blob([a], { type: "audio/wav" });blob.arrayBuffer().then((arraybuffer) => {console.log(arraybuffer);// Player.play(arraybuffer);});}, 3000));},(error) => {// 出错了console.log(`${error.name} : ${error.message}`);});},// 继续录音resumeRecorder() {this.recorder.resume();},// 暂停录音pauseRecorder() {this.recorder.pause();this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 结束录音stopRecorder() {this.recorder.stop();this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 录音播放playRecorder() {this.recorder.play();this.drawPlay(); // 绘制波浪图},// 暂停录音播放pausePlayRecorder() {this.recorder.pausePlay();},// 恢复录音播放resumePlayRecorder() {this.recorder.resumePlay();this.drawPlay(); // 绘制波浪图},// 停止录音播放stopPlayRecorder() {this.recorder.stopPlay();},// 销毁录音destroyRecorder() {this.recorder.destroy().then(() => {this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;this.drawPlayId && cancelAnimationFrame(this.drawPlayId);this.drawPlayId = null;this.recorder = null;});},/*** 下载录音文件* */// 下载pcmdownPCM() {console.log("pcm: ", this.recorder.getPCMBlob());// 这里传参进去的时文件名this.recorder.downloadPCM("新文件");},// 下载wavdownWAV() {console.log("wav: ", this.recorder.getWAVBlob());// 这里传参进去的时文件名this.recorder.downloadWAV("新文件");},/*** 绘制波浪图-录音* */drawRecord() {this.drawRecordId = requestAnimationFrame(this.drawRecord);this.drawWave({canvas: this.$refs.record,dataArray: this.recorder.getRecordAnalyseData(),bgcolor: "rgb(255, 128, 200)",lineWidth: 1,lineColor: "rgb(0, 128, 255)",});},/*** 绘制波浪图-播放* */drawPlay() {this.drawPlayId = requestAnimationFrame(this.drawPlay);this.drawWave({canvas: this.$refs.play,dataArray: this.recorder.getPlayAnalyseData(),});},drawWave({canvas,dataArray,bgcolor = "rgb(200, 200, 200)",lineWidth = 2,lineColor = "rgb(0, 0, 0)",}) {if (!canvas) return;const ctx = canvas.getContext("2d");const bufferLength = dataArray.length;// 一个点占多少位置,共有bufferLength个点要绘制const sliceWidth = canvas.width / bufferLength;// 绘制点的x轴位置let x = 0;// 填充背景色ctx.fillStyle = bgcolor;ctx.fillRect(0, 0, canvas.width, canvas.height);// 设定波形绘制颜色ctx.lineWidth = lineWidth;ctx.strokeStyle = lineColor;ctx.beginPath();for (let i = 0; i < bufferLength; i++) {const v = dataArray[i] / 128;const y = (v * canvas.height) / 2;if (i === 0) {// 第一个点ctx.moveTo(x, y);} else {// 剩余的点ctx.lineTo(x, y);}// 依次平移,绘制所有点x += sliceWidth;}// 最后一个点ctx.lineTo(canvas.width, canvas.height / 2);ctx.stroke();},},
};
</script>
<style lang="scss" scoped>
.BaseRecorder {& > div {margin: 20px 0;}&-wave {canvas {width: 100%;border: 1px solid #ccc;}}
}
</style>
智能面试页面
<template><div class="flex1 w100 h100 bg"><div style="width: 300px" class="bg-white h100 flex-column center"><div class="blue size-30 mb-60">Java面试专场</div><div class="size-26">张三</div><div class="gray-2 mt-20 mb-100">18378562388</div><el-button type="success" round @click="start()">开始面试</el-button></div><div class="flex-1 h100 over-hidden"><divclass="h100 w65 flex1 flex-column"style="margin: 0 auto; min-width: 800px"><div class="flex-1 scroll w100 pt-30"><divv-for="(item, index) in list.filter((item) => item.show)":key="index"class="mb-30"><div class="flex_l mb-30"><i class="el-icon-question size-28 blue mr-10"></i><div class="">{{ item.topic }}</div></div><div class="flex_l" v-if="item.file"><el-avatarclass="mr-10"size="small"src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"></el-avatar><el-card class="flex-1"> 语音已发送 </el-card></div><div class="flex_l" v-if="item.answer"><el-avatarclass="mr-10"size="small"src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"></el-avatar><el-card class="flex-1">{{ item.answer }}</el-card></div></div></div><div class="w100 flex1 pb-30"><el-inputtype="textarea"placeholder="请输入内容"v-model="textarea"maxlength="500"show-word-limit:autosize="{ minRows: 4 }"></el-input><div class="w10 text-center"><el-buttontype="success"icon="el-icon-microphone"circleclass="size-16 mb-10"@click="startRecorder":disabled="disabled"></el-button><el-button:disabled="disabled"type="primary"round@click="submit(1)">提交</el-button></div></div></div></div><!-- 结果 --><el-dialog:close-on-click-modal="false":close-on-press-escape="false"title="面试结果":visible.sync="centerDialogVisible"width="600px"center:show-close="false"style="top: 16vh"><el-result:icon="score >= 80 ? 'success' : score >= 60 ? 'warning' : 'error'":title="score >= 80 ? '优秀' : score >= 60 ? '良好' : '不合格'"subTitle="面试结果"><template slot="extra"><el-button type="primary" size="medium" @click="back">返回</el-button></template></el-result></el-dialog><!-- 录音 --><el-dialog:close-on-click-modal="false":close-on-press-escape="false"title="正在录音...":visible.sync="audioVisible"width="600px"center:show-close="false"style="top: 16vh"><div class="mb-20 size-18" v-if="list[index]">{{ list[index].topic }}</div><div class="BaseRecorder-wave"><canvas ref="record"></canvas><!-- <canvas ref="play"></canvas> --></div><div class="center mt-20"><el-button type="primary" size="medium" @click="submit(2)">提交</el-button></div></el-dialog></div><!-- <div class="BaseRecorder"><div class="BaseRecorder-record"><el-button @click="startRecorder()">开始录音</el-button><el-button @click="pauseRecorder()">暂停录音</el-button><el-button @click="resumeRecorder()">继续录音</el-button><el-button @click="stopRecorder()">结束录音</el-button></div><div class="BaseRecorder-play"><el-button @click="playRecorder()">录音播放</el-button><el-button @click="pausePlayRecorder()">暂停录音播放</el-button><el-button @click="resumePlayRecorder()">恢复录音播放</el-button><el-button @click="stopPlayRecorder()">停止录音播放</el-button></div><div class="BaseRecorder-download"><el-button @click="downPCM()">下载PCM</el-button><el-button @click="downWAV()">下载WAV</el-button></div><div class="BaseRecorder-destroy"><el-button type="error" @click="destroyRecorder()">销毁录音</el-button></div><div class="BaseRecorder-wave"><canvas ref="record"></canvas><canvas ref="play"></canvas></div></div> -->
</template><script>
import Recorder from "js-audio-recorder";
// import { subText, subAudio } from "../api/test.js";
import axios from "axios";
export default {name: "home",data() {return {index: null,disabled: true,list: [{topic: "题目1:1+2等于几?",show: false,answer: "",result: "",file: "",},{topic: "题目2:2+2等于几?",show: false,answer: "",result: "",file: "",},{topic: "题目3:白日依山尽的下一句是什么?",show: false,answer: "",result: "",file: "",},],textarea: "",config: {headers: {"Content-Type": "multipart/form-data",},},centerDialogVisible: false, //结果弹窗score: "", //得分audioVisible: false, //录音弹窗recorder: null,// 波浪图-录音drawRecordId: null,// 波浪图-播放drawPlayId: null,};},mounted() {this.init();},beforeDestroy() {this.destroyRecorder();},methods: {start(i = 0) {this.index = i;this.list[this.index].show = true;this.disabled = false;},// type 1 文字 2 语音async submit(type) {if (type == 1) {if (!this.textarea.trim()) {this.$message({message: "请输入答案",type: "warning",});return;}this.list[this.index].answer = this.textarea;this.disabled = true;this.textarea = "";const formData = new FormData();formData.append("topic", this.list[this.index].topic);formData.append("answer", this.list[this.index].answer);const { data } = await axios.post("/ququ/recognize-text",formData,this.config);console.log(data.result, 99);this.list[this.index].result = data.result;this.index += 1;if (this.index == this.list.length) {this.centerDialogVisible = true;this.index = null;console.log(this.list, 88);this.score =(this.list.filter((item) => item.result == "对").length * 100) /this.list.length;} else {this.start(this.index);}} else {this.stopRecorder();this.audioVisible = false;this.list[this.index].file = this.recorder.getWAVBlob();this.disabled = true;const formData = new FormData();formData.append("topic", this.list[this.index].topic);formData.append("file", this.list[this.index].file);const { data } = await axios.post("/ququ/recognize-video",formData,this.config);console.log(data.result, 99);this.list[this.index].result = data.result;this.index += 1;if (this.index == this.list.length) {this.centerDialogVisible = true;this.index = null;console.log(this.list, 88);this.score =(this.list.filter((item) => item.result == "对").length * 100) /this.list.length;} else {this.start(this.index);}}},back() {this.centerDialogVisible = false;this.list = [{topic: "题目1:1+2等于几?",show: false,answer: "",result: "",file: "",},{topic: "题目2:2+2等于几?",show: false,answer: "",result: "",file: "",},{topic: "题目3:白日依山尽的下一句是什么?",show: false,answer: "",result: "",file: "",},];},// 初始化init() {this.recorder = new Recorder({// 采样位数,支持 8 或 16,默认是16sampleBits: 16,// 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值sampleRate: 48000,// 声道,支持 1 或 2, 默认是1numChannels: 1,// 是否边录边转换,默认是falsecompiling: false,});},// 开始录音startRecorder() {this.recorder.start().then(() => {console.log("开始录音", this.recorder);this.audioVisible = true;this.drawRecord();this.recorder.onprogress = (params) => {console.log(params);// 此处控制数据的收集频率if (this.recorder.config.compiling) {console.log("音频总数据:", params.data);}};// 定时获取录音的数据并播放this.recorder.config.compiling &&(playTimer = setInterval(() => {let newData = this.recorder.getNextData();if (!newData.length) {return;}let byteLength = newData[0].byteLength;let buffer = new ArrayBuffer(newData.length * byteLength);let dataView = new DataView(buffer);// 数据合并for (let i = 0, iLen = newData.length; i < iLen; ++i) {for (let j = 0, jLen = newData[i].byteLength; j < jLen; ++j) {dataView.setInt8(i * byteLength + j, newData[i].getInt8(j));}}// 将录音数据转成WAV格式,并播放let a = encodeWAV(dataView,config.sampleRate,config.sampleRate,config.numChannels,config.sampleBits);let blob = new Blob([a], { type: "audio/wav" });blob.arrayBuffer().then((arraybuffer) => {console.log(arraybuffer);// Player.play(arraybuffer);});}, 3000));},(error) => {// 出错了console.log(`${error.name} : ${error.message}`);});},// 继续录音resumeRecorder() {this.recorder.resume();},// 暂停录音pauseRecorder() {this.recorder.pause();this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 结束录音stopRecorder() {this.recorder.stop();this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;},// 录音播放playRecorder() {this.recorder.play();this.drawPlay(); // 绘制波浪图},// 暂停录音播放pausePlayRecorder() {this.recorder.pausePlay();},// 恢复录音播放resumePlayRecorder() {this.recorder.resumePlay();this.drawPlay(); // 绘制波浪图},// 停止录音播放stopPlayRecorder() {this.recorder.stopPlay();},// 销毁录音destroyRecorder() {this.recorder.destroy().then(() => {this.drawRecordId && cancelAnimationFrame(this.drawRecordId);this.drawRecordId = null;this.drawPlayId && cancelAnimationFrame(this.drawPlayId);this.drawPlayId = null;this.recorder = null;});},/*** 下载录音文件* */// 下载pcmdownPCM() {console.log("pcm: ", this.recorder.getPCMBlob());// 这里传参进去的时文件名this.recorder.downloadPCM("新文件");},// 下载wavdownWAV() {console.log("wav: ", this.recorder.getWAVBlob());// 这里传参进去的时文件名this.recorder.downloadWAV("新文件");},/*** 绘制波浪图-录音* */drawRecord() {this.drawRecordId = requestAnimationFrame(this.drawRecord);this.drawWave({canvas: this.$refs.record,dataArray: this.recorder.getRecordAnalyseData(),bgcolor: "#a8e1fc",lineWidth: 1,lineColor: "rgb(255, 128, 200)",});},/*** 绘制波浪图-播放* */drawPlay() {this.drawPlayId = requestAnimationFrame(this.drawPlay);this.drawWave({canvas: this.$refs.play,dataArray: this.recorder.getPlayAnalyseData(),});},drawWave({canvas,dataArray,bgcolor = "rgb(200, 200, 200)",lineWidth = 2,lineColor = "rgb(0, 0, 0)",}) {if (!canvas) return;const ctx = canvas.getContext("2d");const bufferLength = dataArray.length;// 一个点占多少位置,共有bufferLength个点要绘制const sliceWidth = canvas.width / bufferLength;// 绘制点的x轴位置let x = 0;// 填充背景色ctx.fillStyle = bgcolor;ctx.fillRect(0, 0, canvas.width, canvas.height);// 设定波形绘制颜色ctx.lineWidth = lineWidth;ctx.strokeStyle = lineColor;ctx.beginPath();for (let i = 0; i < bufferLength; i++) {const v = dataArray[i] / 128;const y = (v * canvas.height) / 2;if (i === 0) {// 第一个点ctx.moveTo(x, y);} else {// 剩余的点ctx.lineTo(x, y);}// 依次平移,绘制所有点x += sliceWidth;}// 最后一个点ctx.lineTo(canvas.width, canvas.height / 2);ctx.stroke();},},
};
</script>
<style lang="scss" scoped>
.BaseRecorder {& > div {margin: 20px 0;}&-wave {canvas {width: 100%;border: 1px solid #ccc;}}
}
</style>
post请求,formdata传参
const formData = new FormData();
formData.append("topic", this.list[this.index].topic);
formData.append("answer", this.list[this.index].answer);
const { data } = await axios.post("/ququ/recognize-text",formData,{headers: {"Content-Type": "multipart/form-data",},});
console.log(data.result, 99);