录音插件 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,默认是16
sampleBits: 16,
// 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值
sampleRate: 48000,
// 声道,支持 1 或 2, 默认是1
numChannels: 1,
// 是否边录边转换,默认是false
compiling: 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;
});
},
/**
* 下载录音文件
* */
// 下载pcm
downPCM() {
console.log("pcm: ", this.recorder.getPCMBlob());
// 这里传参进去的时文件名
this.recorder.downloadPCM("新文件");
},
// 下载wav
downWAV() {
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">
<div
class="h100 w65 flex1 flex-column"
style="margin: 0 auto; min-width: 800px"
>
<div class="flex-1 scroll w100 pt-30">
<div
v-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-avatar
class="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-avatar
class="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-input
type="textarea"
placeholder="请输入内容"
v-model="textarea"
maxlength="500"
show-word-limit
:autosize="{ minRows: 4 }"
>
</el-input>
<div class="w10 text-center">
<el-button
type="success"
icon="el-icon-microphone"
circle
class="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,默认是16
sampleBits: 16,
// 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值
sampleRate: 48000,
// 声道,支持 1 或 2, 默认是1
numChannels: 1,
// 是否边录边转换,默认是false
compiling: 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;
});
},
/**
* 下载录音文件
* */
// 下载pcm
downPCM() {
console.log("pcm: ", this.recorder.getPCMBlob());
// 这里传参进去的时文件名
this.recorder.downloadPCM("新文件");
},
// 下载wav
downWAV() {
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);