<template>
<div :class="longPress == '1' ? 'record-layer' : 'record-layer1'">
<div :class="longPress == '1' ? 'record-box' : 'record-box1'">
<div class="record-btn-layer">
<button
class="record-btn"
:class="longPress == '1' ? 'record-btn-1' : 'record-btn-2'"
:style="
VoiceTitle != '松开手指,取消发送' && longPress != '1'
? 'background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);'
: 'background-color: rgba(0, 0, 0, .5);color:white'
"
@longtap="longpressBtn"
@touchend="touchendBtn()"
@touchstart="longpressBtn"
>
<image
src="https://penghuahai.obs.cn-south-1.myhuaweicloud.com:443/penghuahai/blind/1659237233452.png"
/>
<text style="user-select: none">{{ VoiceText }}</text>
</button>
</div>
<!-- 语音音阶动画 -->
<div
:class="
VoiceTitle != '松开手指,取消发送'
? 'prompt-layer prompt-layer-1'
: 'prompt-layer1 prompt-layer-1'
"
v-if="longPress == '2'"
>
<div class="prompt-loader">
<canvas
id="recordCanvas"
ref="record"
style="width: 130px; height: 50px"
></canvas>
</div>
<text class="span">{{ VoiceTitle }}</text>
</div>
</div>
</div>
</template>
<script>
var init; // 录制时长计时器
var timer; // 播放 录制倒计时
import Recorder from "js-audio-recorder";
import axios from "axios";
const baseApi = ref(import.meta.env.VITE_APP_API_BASEURL);
export default {
data() {
return {
longPress: "1", // 1显示 按住 说话 2显示 说话中
delShow: false, // 删除提示框显示隐藏
time: 0, //录音时长
duration: 60000, //录音最大值ms 60000/1分钟
tempFilePath: "", //音频路径
startPoint: {}, //记录长按录音开始点信息,用于后面计算滑动距离。
sendLock: true, //发送锁,当为true时上锁,false时解锁发送
VoiceTitle: "松手结束录音",
// recorderManager: uni.getRecorderManager(),
VoiceText: "按住 说话",
types: "",
recorder: null,
playTime: 0,
timer: null,
src: null,
voiceShowFlag: false,
drawRecordId: null,
drawPlayId: null,
isVoiceOpen: false,
};
},
props: [],
created: function () {
let that = this;
// 检测并请求访问麦克风
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(function (stream) {
that.isVoiceOpen = true;
})
.catch(function (error) {
that.isVoiceOpen = false;
});
},
methods: {
// 长按录音事件
longpressBtn(e) {
console.log(this.isVoiceOpen);
if (this.isVoiceOpen) {
this.startPoint = e.touches[0]; //记录长按时开始点信息,后面用于计算上划取消时手指滑动的距离。
this.longPress = "2";
this.VoiceText = "语音输入中... ...";
// 监听音频开始事件
this.sendLock = false; //长按时是不上锁的。
this.handleStart();
} else {
this.$message({
message: "请先允许该网页使用麦克风",
type: "info",
});
}
},
// 长按松开录音事件
touchendBtn() {
this.longPress = "1";
this.VoiceText = "按住 说话";
this.VoiceTitle = "松手结束录音";
if (this.isVoiceOpen) {
this.handleStop();
this.uploadRecord();
}
},
// // 删除录音
// handleTouchMove(e) {
// //touchmove时触发
// var moveLenght =
// e.touches[e.touches.length - 1].clientY -
// this.startPoint.clientY; //移动距离
// if (Math.abs(moveLenght) > 70) {
// this.VoiceTitle = "松开手指,取消发送";
// this.VoiceText = "松开手指,取消发送";
// this.delBtn();
// this.handleDestroy();
// this.sendLock = true; //触发了上滑取消发送,上锁
// } else {
// this.VoiceTitle = "松手结束录音";
// this.VoiceText = "松手结束录音";
// this.handleStop();
// this.uploadRecord();
// this.sendLock = false; //上划距离不足,依然可以发送,不上锁
// }
// },
delBtn() {
this.delShow = false;
this.time = 0;
this.tempFilePath = "";
// this.VoiceTitle = '松手结束录音'
},
// 开始录音
handleStart() {
this.recorder = new Recorder();
Recorder.getPermission().then(
() => {
console.log("开始录音");
this.recorder.start(); // 开始录音
this.drawRecord();
},
(error) => {
this.$message({
message: "请先允许该网页使用麦克风",
type: "info",
});
console.log(`${error.name} : ${error.message}`);
}
);
},
handlePause() {
console.log("暂停录音");
this.recorder.pause(); // 暂停录音
},
handleResume() {
console.log("恢复录音");
this.recorder.resume(); // 恢复录音
},
handleStop() {
console.log("停止录音");
this.recorder.stop(); // 停止录音
this.drawRecordId && cancelAnimationFrame(this.drawRecordId);
this.drawRecordId = null;
},
handlePlay() {
console.log("播放录音");
console.log(this.recorder);
this.recorder.play(); // 播放录音
// 播放时长
this.timer = setInterval(() => {
try {
this.playTime = this.recorder.getPlayTime();
} catch (error) {
this.timer = null;
}
}, 100);
},
handlePausePlay() {
console.log("暂停播放");
this.recorder.pausePlay(); // 暂停播放
// 播放时长
this.playTime = this.recorder.getPlayTime();
this.time = null;
},
handleResumePlay() {
console.log("恢复播放");
this.recorder.resumePlay(); // 恢复播放
// 播放时长
this.timer = setInterval(() => {
try {
this.playTime = this.recorder.getPlayTime();
} catch (error) {
this.timer = null;
}
}, 100);
},
handleStopPlay() {
console.log("停止播放");
this.recorder.stopPlay(); // 停止播放
// 播放时长
this.playTime = this.recorder.getPlayTime();
this.timer = null;
},
handleDestroy() {
console.log("销毁实例");
this.recorder.destroy(); // 毁实例
this.timer = null;
},
uploadRecord() {
if (this.recorder == null || this.recorder.duration === 0) {
this.$message({
message: "请先录音",
type: "error",
});
return false;
}
this.recorder.pause(); // 暂停录音
this.timer = null;
console.log("上传录音"); // 上传录音
const formData = new FormData();
const blob = this.recorder.getWAVBlob(); // 获取wav格式音频数据
// 此处获取到blob对象后需要设置fileName满足当前项目上传需求,其它项目可直接传把blob作为file塞入formData
const newbolb = new Blob([blob], { type: "audio/wav" });
const fileOfBlob = new File(
[newbolb],
new Date().getTime() + ".wav"
);
console.log(fileOfBlob, "-------------");
formData.append("file", fileOfBlob);
const url = window.URL.createObjectURL(fileOfBlob);
this.src = url;
this.$emit("post-ready");
axios({
method: "post",
url: `${baseApi.value}/qa/asr`,
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((res) => {
this.$emit("post-success", res.data.data);
})
.catch((err) => {
data.value.Loading = false;
proxy.$message.warning(err.msg);
});
},
// 录音波浪图
drawRecord() {
// 用requestAnimationFrame稳定60fps绘制
this.drawRecordId = requestAnimationFrame(this.drawRecord);
this.drawWave({
canvas: this.$refs.record,
dataArray: this.recorder.getRecordAnalyseData(),
});
},
// 播放波浪图
drawPlay() {
// 用requestAnimationFrame稳定60fps绘制
this.drawPlayId = requestAnimationFrame(this.drawPlay);
this.drawWave({
canvas: this.$refs.play,
dataArray: this.recorder.getPlayAnalyseData(),
});
},
// 绘制波形图
drawWave({ canvas, dataArray }) {
const ctx = canvas.getContext("2d");
const bufferLength = dataArray.length;
// 填充背景色
ctx.fillStyle = "#95eb6c";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 设定波形绘制颜色
ctx.lineWidth = 4;
ctx.strokeStyle = "#000";
ctx.beginPath();
var sliceWidth = (canvas.width * 1.0) / bufferLength, // 一个点占多少位置,共有bufferLength个点要绘制
x = 0; // 绘制点的x轴位置
for (var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var 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">
/* 语音录制开始--------------------------------------------------------------------- */
.record-layer {
width: 65%;
// padding: 300px 0;
box-sizing: border-box;
z-index: 10;
}
.record-layer1 {
width: 100vw;
// padding: 300px 0;
box-sizing: border-box;
height: 100vh;
position: fixed;
background-color: rgb(0 0 0 / 60%);
// padding: 0 4vw;
z-index: 10;
bottom: 0;
transform: translateX(-11px);
}
.record-box {
width: 100%;
position: relative;
}
.record-box1 {
width: 100%;
position: relative;
bottom: -83vh;
height: 17vh;
}
.record-btn-layer {
width: 100%;
}
.record-btn-layer button::after {
border: none;
transition: all 0.1s;
}
.record-btn-layer button {
font-size: 14px;
line-height: 37px;
width: 100%;
height: 37px;
border-radius: 8px;
text-align: center;
background: #ffd300;
transition: all 0.1s;
border: none;
outline: none;
}
.record-btn-layer button image {
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: middle;
transition: all 0.3s;
}
.record-btn-layer .record-btn-1 {
background-image: linear-gradient(to right, #43e97b 0%, #38f9d7 100%);
color: #000 !important;
}
.record-btn-layer .record-btn-2 {
border-radius: 168rpx 168rpx 0 0;
height: 17vh;
line-height: 17vh;
transition: all 0.3s;
}
/* 提示小弹窗 */
.prompt-layer {
border-radius: 15px;
background: #95eb6c;
padding: 8px 16px;
box-sizing: border-box;
position: absolute;
left: 50%;
height: 11vh;
transform: translateX(-50%);
transition: all 0.3s;
}
// .prompt-layer::after {
// content: "";
// display: block;
// border: 12px solid rgb(0 0 0 / 0%);
// border-radius: 10rpx;
// border-top-color: #95eb6c;
// position: absolute;
// bottom: -46rpx;
// left: 50%;
// transform: translateX(-50%);
// transition: all 0.3s;
// }
//取消动画
.prompt-layer1 {
border-radius: 15px;
background: #95eb6c;
padding: 8px 16px;
box-sizing: border-box;
position: absolute;
left: 50%;
height: 11vh;
transform: translateX(-50%);
transition: all 0.3s;
}
.prompt-layer-1 {
font-size: 12px;
width: 200px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
top: -300px;
}
.prompt-layer-1 .p {
color: #000;
}
.prompt-layer-1 .span {
color: rgb(0 0 0 / 60%);
}
.prompt-loader .em {
}
/* 语音音阶------------- */
.prompt-loader {
width: 125px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.prompt-loader .em {
display: block;
background: #333;
width: 1px;
height: 10%;
margin-right: 2.5px;
float: left;
}
.prompt-loader .em:last-child {
margin-right: 0;
}
.prompt-loader .em:nth-child(1) {
animation: load 2.5s 1.4s infinite linear;
}
.prompt-loader .em:nth-child(2) {
animation: load 2.5s 1.2s infinite linear;
}
.prompt-loader .em:nth-child(3) {
animation: load 2.5s 1s infinite linear;
}
.prompt-loader .em:nth-child(4) {
animation: load 2.5s 0.8s infinite linear;
}
.prompt-loader .em:nth-child(5) {
animation: load 2.5s 0.6s infinite linear;
}
.prompt-loader .em:nth-child(6) {
animation: load 2.5s 0.4s infinite linear;
}
.prompt-loader .em:nth-child(7) {
animation: load 2.5s 0.2s infinite linear;
}
.prompt-loader .em:nth-child(8) {
animation: load 2.5s 0s infinite linear;
}
.prompt-loader .em:nth-child(9) {
animation: load 2.5s 0.2s infinite linear;
}
.prompt-loader .em:nth-child(10) {
animation: load 2.5s 0.4s infinite linear;
}
.prompt-loader .em:nth-child(11) {
animation: load 2.5s 0.6s infinite linear;
}
.prompt-loader .em:nth-child(12) {
animation: load 2.5s 0.8s infinite linear;
}
.prompt-loader .em:nth-child(13) {
animation: load 2.5s 1s infinite linear;
}
.prompt-loader .em:nth-child(14) {
animation: load 2.5s 1.2s infinite linear;
}
.prompt-loader .em:nth-child(15) {
animation: load 2.5s 1.4s infinite linear;
}
@keyframes load {
0% {
height: 10%;
}
50% {
height: 100%;
}
100% {
height: 10%;
}
}
/* 语音音阶-------------------- */
.prompt-layer-2 {
top: -40px;
}
.prompt-layer-2 .text {
color: rgb(0 0 0 / 100%);
font-size: 12px;
}
/* 语音录制结束---------------------------------------------------------------- */
</style>