实现思路
前端把 音频文件以流的形式给后端接口,接收接口解析完成之后返回的内容,需要以下步骤:
-
获取手机录音权限
-
开启录音
-
结束录音,将 录音 Blob 转为 音频文件
-
将音频文件给到后端
封装音频类
// mediaDevices.js
// import FFmpeg, { createFFmpeg } from "@ffmpeg/ffmpeg";
// import ffmpegCore from "@ffmpeg/core/dist/ffmpeg-core";
const ERROR_CODE_MESSAGE = {
PERMISSION_DENIED: "用户拒绝提供信息。",
PermissionDeniedError: "用户拒绝提供信息。",
NOT_SUPPORTED_ERROR: "浏览器不支持硬件设备。",
NotSupportedError: "浏览器不支持硬件设备。",
MANDATORY_UNSATISFIED_ERROR: "无法发现指定的硬件设备。",
MandatoryUnsatisfiedError: "无法发现指定的硬件设备。"
};
// 获取录音权限
export function getUserMedia() {
// 获取录音权限异常
function throwError(err) {
const errCode = err.code;
const errName = err.name;
const errorMessage = ERROR_CODE_MESSAGE[errCode] || ERROR_CODE_MESSAGE[errName];
return errorMessage || `无法打开麦克风。异常信息: ${errCode || errName}`;
}
return new Promise((resolve, reject) => {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// 授权音频
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => {
/* 使用音频流 */
console.log("授权成功", stream);
resolve(stream);
})
.catch(err => {
console.error("发生错误: " + err);
reject(throwError(err));
});
} else {
reject("抱歉,你的浏览器暂不支持录音功能");
}
});
}
export default class MediaDevices {
constructor(config = {}, stream) {
// 配置
config = config || {};
config.sampleBits = 16; //采样数位 8, 16
config.sampleRate = 16000; //采样率(1/6 44100)
// 创建音频上下文
var context = new (window.webkitAudioContext || window.AudioContext)();
// 创建音频源
var mediaStreamSource = context.createMediaStreamSource(stream);
var createScript = context.createScriptProcessor || context.createJavaScriptNode;
var recorder = createScript.apply(context, [4096, 1, 1]);
this.context = context;
this.mediaStreamSource = mediaStreamSource;
this.recorder = recorder;
// 创建录音对象
var recognition = this.createRecognition(config);
// 音频采集
recorder.onaudioprocess = function(e) {
recognition.input(e.inputBuffer.getChannelData(0));
};
this.recognition = recognition;
}
// 创建语音识别对象
createRecognition(config) {
return {
size: 0, //录音文件长度
buffer: [], //录音缓存
inputSampleRate: this.context.sampleRate, //输入采样率
inputSampleBits: 16, //输入采样数位 8, 16
outputSampleRate: config.sampleRate, //输出采样率
oututSampleBits: config.sampleBits, //输出采样数位 8, 16
init: function() {
this.size = 0;
this.buffer = [];
},
input: function(data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
},
compress: function() {
//合并压缩
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//压缩
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0,
j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
},
encodeWAV: function() {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1; //单声道
var offset = 0;
var writeString = function(str) {
for (var 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 (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7fff;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
}
return new Blob([data], { type: config.audioType || "audio/wav" });
}
};
}
// 开始录音
start() {
// 初始化
this.recognition.init();
this.mediaStreamSource.connect(this.recorder);
this.recorder.connect(this.context.destination);
}
// 停止录音
stop() {
this.recorder.disconnect();
}
// 清除数据
clear() {
this.recognition.init();
}
// 上传录音
upload(url, callback) {
var fd = new FormData();
fd.append("audioData", this.getBlob());
var xhr = new XMLHttpRequest();
if (callback) {
xhr.upload.addEventListener(
"progress",
function(e) {
callback("uploading", e);
},
false
);
xhr.addEventListener(
"load",
function(e) {
callback("ok", e);
},
false
);
xhr.addEventListener(
"error",
function(e) {
callback("error", e);
},
false
);
xhr.addEventListener(
"abort",
function(e) {
callback("cancel", e);
},
false
);
}
xhr.open("POST", url);
xhr.send(fd);
}
// 获取音频文件
getBlob() {
this.stop();
return this.recognition.encodeWAV();
}
// 转为pcm格式音频文件
async toPcmFile(blob, config = {}) {
if (!blob) {
blob = this.getBlob();
}
// FFmpeg.load().then(() => {
// console.log("加载完成");
// });
// console.log("....", FFmpeg);
// const ffmpeg = createFFmpeg({log: true, corePath: ffmpegCore, ...config});
// 异步加载 ffmpeg
// await ffmpeg.load();
// return new Promise((resolve, reject) => {
// ffmpeg
// .run("-i", blob, "-f", "s16le", "-acodec", "pcm_s16le", "output.pcm")
// .then(res => {
// resolve(res);
// })
// .catch(err => {
// reject(err);
// });
// });
// FFmpeg({
// arguments: ["-i", audioBlob, "-f", "s16le", "-acodec", "pcm_s16le", "output.pcm"],
// files: [audioBlob]
// }).then(() => {
// // PCM数据现在可用
// });
}
}
离线音频示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>H5 录音功能</title>
</head>
<body>
<button id="startRecord">开始录音</button>
<button id="stopRecord" disabled>停止录音</button>
<button id="playAudio" disabled>播放录音</button>
<script>
import MediaDevices, { getUserMedia } from "./mediaDevices.js";
let mediaRecorder;
let recordedChunks = [];
// 获取音频权限
getUserMedia().then(stream => {
// 创建音频对象
mediaRecorder = new MediaDevices({}, stream);
})
document.getElementById('startRecord').addEventListener('click', () => {
if (mediaRecorder) {
mediaRecorder.start();
document.getElementById('startRecord').disabled = true;
document.getElementById('stopRecord').disabled = false;
recordedChunks = []; // 清空之前的录音数据
}
});
document.getElementById('stopRecord').addEventListener('click', () => {
if (mediaRecorder) {
mediaRecorder.stop();
document.getElementById('startRecord').disabled = false;
document.getElementById('stopRecord').disabled = true;
document.getElementById('playAudio').disabled = false;
}
});
document.getElementById('playAudio').addEventListener('click', () => {
const audioBlob = new Blob(recordedChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
});
</script>
</body>
</html>