需求:浏览器录音,发送到服务器触发语音搜索,代码中是把语音信息转成file,表单提交到后端
index.vue
<template>
<div class="box">
<div
class="talk_search_btn_item"
style="cursor: pointer"
@click="handleVoice()"
title="语音"
>
<img src="./icon2.png" v-show="!isOpenFlag" alt="" />
<img src="./icon21.png" v-show="isOpenFlag" alt="" />
</div>
<div class="box_list">
<div
v-for="(item, index) in audioData"
:key="index"
class="box_list_item"
@click="playAudio(item)"
>
<img
src="./audio0.png"
style="height: 100%; width: auto"
alt=""
v-show="!item.playAudioLoading"
/>
<img
src="./audio1.gif"
style="height: 100%; width: auto"
alt=""
v-show="item.playAudioLoading"
/>
</div>
</div>
</div>
</template>
<script>
// base64 转 ArrayBuffer
function _base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64); //解码使用base64编码的字符串
var len = binary_string.length; //获取长度
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
// console.log(bytes); //打印解析出来的byte
// return bytes;
return bytes.buffer;
}
// 下载
function download(buff) {
let url = window.URL.createObjectURL(
new Blob([buff], { type: "arraybuffer" })
);
const link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.setAttribute("download", "out");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 拼接 ArrayBuffer
function mergeArrayBuffers(arrayBuffers) {
// 计算新的ArrayBuffer的总长度
let totalLength = 0;
for (const buffer of arrayBuffers) {
totalLength += buffer.byteLength;
}
// 创建一个新的ArrayBuffer
const mergedBuffer = new ArrayBuffer(totalLength);
// 创建一个Uint8Array以便操作新的ArrayBuffer
const uint8Array = new Uint8Array(mergedBuffer);
let offset = 0;
// 逐个复制ArrayBuffer到新的ArrayBuffer中
for (const buffer of arrayBuffers) {
const sourceArray = new Uint8Array(buffer);
uint8Array.set(sourceArray, offset);
offset += sourceArray.length;
}
return mergedBuffer;
}
// ArrayBuffer 转 Float32Array
function convertArrayBufferToFloat32Array(arrayBuffer) {
const dataView = new DataView(arrayBuffer);
// const float32Array = new Float32Array(arrayBuffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
const float32Array = new Float32Array(arrayBuffer.byteLength / 2);
for (let i = 0; i < float32Array.length; i++) {
const pcmValue = dataView.getInt16(i * 2, true);
float32Array[i] = pcmValue / 32768.0;
}
return float32Array;
}
function blobToFile(blob, filename, type) {
return new File([blob], filename, { type });
}
import AudioRecorder from "./AudioRecorder.js";
export default {
data() {
return {
recorder: null, //录音对象
isOpenFlag: false, //正在录音标识
audioMessage: "正在进行语音对讲",
loadingMessage: null,
audioData: [],
};
},
methods: {
playAudio(data) {
if (data.playAudioLoading) return;
var arr = data.file_audio;
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
// 将 ArrayBuffer 格式的 PCM 数据转换为 Float32Array 格式
const float32Array = convertArrayBufferToFloat32Array(arr);
// 创建 AudioBufferSourceNode
const audioBufferSource = audioContext.createBufferSource();
// 创建 AudioBuffer
const audioBuffer = audioContext.createBuffer(
1,
float32Array.length,
8000
);
// 获取 AudioBuffer 的数据通道
const channelData = audioBuffer.getChannelData(0);
// 将 Float32Array 格式的 PCM 数据填充到 AudioBuffer 的数据通道
channelData.set(float32Array);
// 将 AudioBuffer 设置为 AudioBufferSourceNode 的音频数据
audioBufferSource.buffer = audioBuffer;
// 连接 AudioBufferSourceNode 到音频输出
audioBufferSource.connect(audioContext.destination);
// 播放音频
audioBufferSource.start();
data.playAudioLoading = true;
audioBufferSource.onended = function () {
// 播放结束后执行的操作
console.log("音频播放结束");
data.playAudioLoading = false;
};
},
handleVoice() {
var isOpenFlag = !this.isOpenFlag;
if (isOpenFlag) {
if (this.listLoading == true) {
this.$message.success("正在查询中,请稍等");
return;
}
this.onstart();
} else {
this.isOpenFlag = false;
this.loadingMessage && this.loadingMessage.close(); // 关闭通知加载
}
},
onstart() {
let _this = this;
if (_this.recorder) {
_this.recorder.stop();
}
this.isOpenFlag = true;
var all_ArrayBuffer = [];
var num = -1;
let config = {
onAudioProcess: function (audioData) {
let data = audioData.encodePCM();
let reader = new FileReader();
let base64data;
reader.onloadend = function () {
// console.log("发送语音", reader);
base64data = reader.result;
base64data = base64data.split(",")[1];
// console.log(base64data);
// base64data = JSON.stringify({
// type: 1,
// content: base64data.split(",")[1],
// });
var data2 = _base64ToArrayBuffer(base64data); // Uint8Array ArrayBuffer
num++;
// console.log(num);
if (_this.isOpenFlag) {
// num < 100
// console.log(data2);
all_ArrayBuffer.push(data2);
} else if (_this.isOpenFlag == false) {
// num == 100
var arr = mergeArrayBuffers(all_ArrayBuffer);
// console.log("arr", arr);
_this.audioData.push({
file_audio: arr,
playAudioLoading: false,
});
// 播放------------------------------------------------------------------------
// const audioContext = new (window.AudioContext ||
// window.webkitAudioContext)();
// // 将 ArrayBuffer 格式的 PCM 数据转换为 Float32Array 格式
// const float32Array = convertArrayBufferToFloat32Array(arr);
// // 创建 AudioBufferSourceNode
// const audioBufferSource = audioContext.createBufferSource();
// // 创建 AudioBuffer
// const audioBuffer = audioContext.createBuffer(
// 1,
// float32Array.length,
// 8000
// );
// // 获取 AudioBuffer 的数据通道
// const channelData = audioBuffer.getChannelData(0);
// // 将 Float32Array 格式的 PCM 数据填充到 AudioBuffer 的数据通道
// channelData.set(float32Array);
// // 将 AudioBuffer 设置为 AudioBufferSourceNode 的音频数据
// audioBufferSource.buffer = audioBuffer;
// // 连接 AudioBufferSourceNode 到音频输出
// audioBufferSource.connect(audioContext.destination);
// // 播放音频
// audioBufferSource.start();
// 播放------------------------------------------------------------------------
// download(arr);//下载到本地 可用GoldWave播放
var blob = new Blob([arr]); // ArrayBuffer 转 Blob 对象
// var file = _this.blobToFile(blob, 'test', 'text/plain' )
var file = blobToFile(
blob,
"audio" + _this.$moment().format("_YYYYMMDD_HHmmss"),
"audio/mpeg"
);
// _this.onSearch(file); //上传到服务端
if (_this.recorder) {
_this.recorder.stop();
}
_this.loadingMessage && _this.loadingMessage.close(); // 关闭通知加载
}
audioData.clearInput();
};
reader.readAsDataURL(new Blob([data]));
},
};
try {
AudioRecorder.get(function (rec) {
_this.recorder = rec;
_this.recorder.start();
_this.isOpenFlag = true;
setTimeout(() => {
_this.audioMessage = "正在录音";
_this.loadingMessage && _this.loadingMessage.close(); // 关闭通知加载
_this.loadingMessage = _this.$message({
showClose: false,
customClass: "evpMsgCls",
dangerouslyUseHTMLString: true,
message:
'<div class="el-icon-loading"></div><a>' +
// _this.videoName +
// ": " +
_this.audioMessage +
" </a>",
duration: 0,
});
}, 200);
}, config);
} catch (error) {
_this.isOpenFlag = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.box {
display: flex;
flex-direction: column;
img {
width: 36px;
height: 36px;
}
.box_list {
height: 0;
flex-grow: 1;
.box_list_item {
width: 120px;
height: 36px;
background-color: #3d6bf8;
border-radius: 15px;
margin-bottom: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
AudioRecorder.js
// 兼容
/**
* 注意:浏览器默认采样率为48000,国标采样率为8000,因此需要压缩音频文件到1/6
* input()、encodePCM() 录入任意时长的音频,最后调用encodePCM方法转成PCM格式
* inputCompress(),encodeCompressPCM两个方法:每触发一次inputCompress方法,压缩一次,
* 最后将所有压缩文件合并生成一个PCM文件
* 两种方式只能存在一种,根据要求修改代码
*
*/
let lastTime = 0;
const AudioRecorder = function(stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 16; // 采样数位 8, 16
config.sampleRate = config.sampleRate || 8000; // 采样率
// console.log('采样数位',config.sampleBits, '采样率',config.sampleRate);
// let context = new (window.AudioContext || window.webkitAudioContext)();
// let audioInput = context.createMediaStreamSource(stream);
// let recorder = context.createJavaScriptNode(4096, 1, 1);
let context = new AudioContext();
let audioInput = context.createMediaStreamSource(stream);
// let recorder = context.createScriptProcessor(4096, 1, 1);// 缓冲区大小、指定输入node的声道的数量,默认值是2、指定输出node的声道的数量,默认2
let recorder = context.createScriptProcessor(2048, 1, 1);// 缓冲区大小、指定输入node的声道的数量,默认值是2、指定输出node的声道的数量,默认2
let audioData = {
stream: stream,
size: 0, // 录音文件长度
buffer: [], // 录音缓存
compressBuffer: [], // 录音缓存,压缩后
compressBufferSize: 0,
inputSampleRate: context.sampleRate, // 输入采样率
inputSampleBits: 16, // 输入采样数位 8, 16
outputSampleRate: config.sampleRate, // 输出采样率
oututSampleBits: config.sampleBits, // 输出采样数位 8, 16
input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
// this.inputCompress();
if (lastTime) {
// console.log("耗时:" + (new Date().getTime() - lastTime));
}
lastTime = new Date().getTime();
// console.log("语音数据Size:" + this.size + "byte");
},
inputCompress: function (data) {
this.compressBuffer.push(this.compress());
this.compressBufferSize += this.compress().length;
this.clearInput();
},
clearInput: function () {
this.buffer = [];
this.size = 0;
},
compress: function () { // 合并处理
let data = new Float32Array(this.size);
let offset = 0;
for (let i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
let compression = parseInt(this.inputSampleRate / this.outputSampleRate);
let length = data.length / compression;
let result = new Float32Array(length);
let index = 0;
let j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
},
// 分次压缩后合并
encodeCompressPCM() {
let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
let bytesArray = this.compressBuffer;
let bytes = new Float32Array(this.compressBufferSize);
// 合并分片
let index = 0;
for (let b = 0; b < bytesArray.length; b++) {
for (let c = 0; c < bytesArray[b].length; c++) {
bytes[index] = bytesArray[b][c];
index++;
}
}
let dataLength = this.compressBufferSize * (sampleBits / 8);
let buffer = new ArrayBuffer(dataLength);
let data = new DataView(buffer);
let a = 0;
if (sampleBits === 8) {
for (let o = 0; o < bytes.length; o++, a++) {
let s = Math.max(-1, Math.min(1, bytes[o]));
let u = s < 0 ? 32768 * s : 32767 * s;
u = parseInt(255 / (65535 / (u + 32768)));
data.setInt8(a, u, !0);
}
} else {
for (let o = 0; o < bytes.length; o++, a += 2) {
let s = Math.max(-1, Math.min(1, bytes[o]));
data.setInt16(a, s < 0 ? 32768 * s : 32767 * s, !0)
}
}
return data;
},
// 压缩一次
encodePCM: function() {
let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
let bytes = this.compress();
let dataLength = bytes.length * (sampleBits / 8);
let buffer = new ArrayBuffer(dataLength);
let data = new DataView(buffer);
let a = 0;
if (sampleBits === 8) {
for (let o = 0; o < bytes.length; o++, a++) {
let s = Math.max(-1, Math.min(1, bytes[o]));
let u = s < 0 ? 32768 * s : 32767 * s;
u = parseInt(255 / (65535 / (u + 32768)));
data.setInt8(a, u, !0);
}
} else {
for (let o = 0; o < bytes.length; o++, a += 2) {
let s = Math.max(-1, Math.min(1, bytes[o]));
data.setInt16(a, s < 0 ? 32768 * s : 32767 * s, !0)
}
}
return data;
},
encodeWAV: function () {
let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
let bytes = this.compress();
let dataLength = bytes.length * (sampleBits / 8);
let buffer = new ArrayBuffer(44 + dataLength);
let data = new DataView(buffer);
let channelCount = 1;// 单声道
let offset = 0;
let writeString = function (str) {
for (let i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
}
// 资源交换文件标识符
writeString('RIFF'); offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + dataLength, true); offset += 4;
// WAV文件标志
writeString('WAVE'); offset += 4;
// 波形格式标志
writeString('fmt '); offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true); offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true); offset += 2;
// 通道数
data.setUint16(offset, channelCount, true); offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true); offset += 4;
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
// 每样本数据位数
data.setUint16(offset, sampleBits, true); offset += 2;
// 数据标识符
writeString('data'); offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, dataLength, true); offset += 4;
// 写入采样数据
if (sampleBits === 8) {
for (let i = 0; i < bytes.length; i++, offset++) {
let s = Math.max(-1, Math.min(1, bytes[i]));
let val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (let i = 0; i < bytes.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return data;
},
playSound: function () {
context.decodeAudioData(this.encodeWAV().buffer, function(buffer) {
let source = context.createBufferSource();
source.buffer = buffer; // 设置数据
source.connect(context.destination); // connect到扬声器
source.start();
}, function() {
// console.log('error');
});
}
};
// 开始录音
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
}
// 停止
this.stop = function () {
audioData.stream.getTracks()[0].stop();
audioInput.disconnect();
recorder.disconnect();
}
// 清空语音数据
this.clearAudio = function () {
audioData.clearInput();
}
// 获取音频文件
this.getBlob_WAV = function () {
this.stop();
let data = audioData.encodeWAV();
return new Blob([data], { type: 'audio/wav' });
}
// 获取音频文件
this.getBlob_PCM = function () {
this.stop();
let data = audioData.encodePCM();
return new Blob([data], { type: 'audio/pcm' });
}
// 获取音频文件
this.getBlob_CompressPCM = function () {
this.stop();
let data = audioData.encodeCompressPCM();
return new Blob([data], { type: 'audio/pcm' });
}
// 回放(WAV格式-使用audio标签)
this.playWav = function (audio) {
audio.src = window.URL.createObjectURL(this.getBlob_WAV());
}
// 播放语音(使用context)
this.playSound = function () {
audioData.playSound();
}
this.combineDateView = function(resultConstructor, ...arrays) {
}
// 音频采集
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
// record(e.inputBuffer.getChannelData(0));
// 回调函数
if (config.onAudioProcess) {
// console.log("--执行回调函数--");
config.onAudioProcess(audioData);
}
}
}
// 获取
AudioRecorder.get = function (callback, config) {
if (callback) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
(navigator.mediaDevices && (navigator.mediaDevices.getUserMedia || navigator.mediaDevices.webkitGetUserMedia || navigator.mediaDevices.mozGetUserMedia));
// window.console.log("navigator.getUserMedia:" + navigator.getUserMedia);
if (navigator.getUserMedia) {
navigator.getUserMedia(
{ audio: true }, // 只启用音频
function (stream) {
var rec = new AudioRecorder(stream, config);
callback(rec);
},
function (error) {
switch (error.code || error.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
alert('用户拒绝提供信息。');
break;
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
alert('浏览器不支持硬件设备。');
break;
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
alert('无法发现指定的硬件设备。');
break;
default:
alert('无法打开麦克风。异常信息:' + (error.code || error.name));
break;
}
});
} else {
alert('当前浏览器不支持录音功能。');
}
}
}
export default AudioRecorder;
保存到本地的文件,可用GoldWave进行播放,参数如下