web端手机录音

devtools/2024/11/8 13:31:52/

可以将每个片段的音频,变成完整的mp3(或其他格式文件)

采样率使用16000(本代码中:其他采样率可能会导致噪音或者播放(具体采样率自行研究))

引入第三方依赖

<script src="https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.0/lame.min.js"></script> 

webRecorder的js 代码

export function to16BitPCM(input) {const dataLength = input.length * (16 / 8);const dataBuffer = new ArrayBuffer(dataLength);const dataView = new DataView(dataBuffer);let offset = 0;for (let i = 0; i < input.length; i++, offset += 2) {const s = Math.max(-1, Math.min(1, input[i]));dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);}return dataView;
}
export function to16kHz(audioData, sampleRate = 44100) {const data = new Float32Array(audioData);const fitCount = Math.round(data.length * (16000 / sampleRate));const newData = new Float32Array(fitCount);const springFactor = (data.length - 1) / (fitCount - 1);newData[0] = data[0];for (let i = 1; i < fitCount - 1; i++) {const tmp = i * springFactor;const before = Math.floor(tmp).toFixed();const after = Math.ceil(tmp).toFixed();const atPoint = tmp - before;newData[i] = data[before] + (data[after] - data[before]) * atPoint;}newData[fitCount - 1] = data[data.length - 1];return newData;
}const audioWorkletCode = `class MyProcessor extends AudioWorkletProcessor {constructor(options) {super(options);this.audioData = [];this.audioDataFloat32 = [];this.sampleCount = 0;this.bitCount = 0;this.preTime = 0;}process(inputs) {// 去处理音频数据// eslint-disable-next-line no-undefif (inputs[0][0]) {const output = ${to16kHz}(inputs[0][0], sampleRate);this.sampleCount += 1;const audioData = ${to16BitPCM}(output);this.bitCount += 1;const data = [...new Int16Array(audioData.buffer)];this.audioData = this.audioData.concat(data);const dataFloat32 = [...output];this.audioDataFloat32 = this.audioDataFloat32.concat(dataFloat32);if (new Date().getTime() - this.preTime > 100) {this.port.postMessage({audioData: new Int16Array(this.audioData),audioDataFloat32: new Float32Array(this.audioDataFloat32),sampleCount: this.sampleCount,bitCount: this.bitCount});this.preTime = new Date().getTime();this.audioData = [];this.audioDataFloat32 = [];}return true;}}}registerProcessor('my-processor', MyProcessor);`;
const TAG = 'WebRecorder';
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia|| navigator.mozGetUserMedia || navigator.msGetUserMedia;export default class WebRecorder {constructor(requestId, params, isLog) {this.audioData = [];this.audioDataFloat32 = [];this.allAudioData = [];this.stream = null;this.audioContext = null;this.requestId = requestId;this.frameTime = [];this.frameCount = 0;this.sampleCount = 0;this.bitCount = 0;this.mediaStreamSource = null;this.isLog = isLog;this.params = params;}static isSupportMediaDevicesMedia() {return !!(navigator.getUserMedia || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia));}static isSupportUserMediaMedia() {return !!navigator.getUserMedia;}static isSupportAudioContext() {return typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined';}static isSupportMediaStreamSource(requestId, audioContext) {return typeof audioContext.createMediaStreamSource === 'function';}static isSupportAudioWorklet(audioContext) {return audioContext.audioWorklet && typeof audioContext.audioWorklet.addModule === 'function'&& typeof AudioWorkletNode !== 'undefined';}static isSupportCreateScriptProcessor(requestId, audioContext) {return typeof audioContext.createScriptProcessor === 'function';}start() {this.frameTime = [];this.frameCount = 0;this.allAudioData = [];this.audioData = [];this.sampleCount = 0;this.bitCount = 0;this.getDataCount = 0;this.audioContext = null;this.mediaStreamSource = null;this.stream = null;this.preTime = 0;try {if (WebRecorder.isSupportAudioContext()) {this.audioContext = new (window.AudioContext || window.webkitAudioContext)();} else {this.isLog && console.log(this.requestId, '浏览器不支持AudioContext', TAG);this.OnError('浏览器不支持AudioContext');}} catch (e) {this.isLog && console.log(this.requestId, '浏览器不支持webAudioApi相关接口', e, TAG);this.OnError('浏览器不支持webAudioApi相关接口');}this.getUserMedia(this.requestId, this.getAudioSuccess, this.getAudioFail);}stop() {if (!(/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent))) {this.audioContext && this.audioContext.suspend();}this.audioContext && this.audioContext.suspend();this.isLog && console.log(this.requestId, `webRecorder stop ${this.sampleCount}/${this.bitCount}/${this.getDataCount}`, JSON.stringify(this.frameTime), TAG);this.OnStop(this.allAudioData);}destroyStream() {// 关闭通道if (this.stream) {this.stream.getTracks().map((val) => {val.stop();});this.stream = null;}}async getUserMedia(requestId, getStreamAudioSuccess, getStreamAudioFail) {let audioOption = {echoCancellation: true,};if (this.params && String(this.params.echoCancellation) === 'false') { // 关闭回声消除audioOption = {echoCancellation: false,};}const mediaOption = {audio: audioOption,video: false,};// 获取用户的麦克风if (WebRecorder.isSupportMediaDevicesMedia()) {navigator.mediaDevices.getUserMedia(mediaOption).then(stream => {this.stream = stream;getStreamAudioSuccess.call(this, requestId, stream);}).catch(e => {getStreamAudioFail.call(this, requestId, e);});} else if (WebRecorder.isSupportUserMediaMedia()) {navigator.getUserMedia(mediaOption,stream => {this.stream = stream;getStreamAudioSuccess.call(this, requestId, stream);},function (err) {getStreamAudioFail.call(this, requestId, err);});} else {if (navigator.userAgent.toLowerCase().match(/chrome/) && location.origin.indexOf('https://') < 0) {this.isLog && console.log(this.requestId, 'chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限', TAG);this.OnError('chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限');} else {this.isLog && console.log(this.requestId, '无法获取浏览器录音功能,请升级浏览器或使用chrome', TAG);this.OnError('无法获取浏览器录音功能,请升级浏览器或使用chrome');}this.audioContext && this.audioContext.close();}}async getAudioSuccess(requestId, stream) {if (!this.audioContext) {return false;}if (this.mediaStreamSource) {this.mediaStreamSource.disconnect();this.mediaStreamSource = null;}this.audioTrack = stream.getAudioTracks()[0];const mediaStream = new MediaStream();mediaStream.addTrack(this.audioTrack);this.mediaStreamSource = this.audioContext.createMediaStreamSource(mediaStream);if (WebRecorder.isSupportMediaStreamSource(requestId, this.audioContext)) {if (WebRecorder.isSupportAudioWorklet(this.audioContext)) { // 不支持 AudioWorklet 降级this.audioWorkletNodeDealAudioData(this.mediaStreamSource, requestId);} else {this.scriptNodeDealAudioData(this.mediaStreamSource, requestId);}} else { // 不支持 MediaStreamSourcethis.isLog && console.log(this.requestId, '不支持MediaStreamSource', TAG);this.OnError('不支持MediaStreamSource');}}getAudioFail(requestId, err) {if (err && err.err && err.err.name === 'NotAllowedError') {this.isLog && console.log(requestId, '授权失败', JSON.stringify(err.err), TAG);}this.isLog && console.log(this.requestId, 'getAudioFail', JSON.stringify(err), TAG);this.OnError(err);this.stop();}scriptNodeDealAudioData(mediaStreamSource, requestId) {if (WebRecorder.isSupportCreateScriptProcessor(requestId, this.audioContext)) {// 创建一个音频分析对象,采样的缓冲区大小为0(自动适配),输入和输出都是单声道const scriptProcessor = this.audioContext.createScriptProcessor(1024, 1, 1);// 连接this.mediaStreamSource && this.mediaStreamSource.connect(scriptProcessor);scriptProcessor && scriptProcessor.connect(this.audioContext.destination);scriptProcessor.onaudioprocess = (e) => {this.getDataCount += 1;// 去处理音频数据const inputData = e.inputBuffer.getChannelData(0);const output = to16kHz(inputData, this.audioContext.sampleRate);const audioData = to16BitPCM(output);this.audioDataFloat32.push(...output);this.audioData.push(...new Int16Array(audioData.buffer));this.allAudioData.push(...new Int16Array(audioData.buffer));if (new Date().getTime() - this.preTime > 100) {this.frameTime.push(`${Date.now()}-${this.frameCount}`);this.frameCount += 1;this.preTime = new Date().getTime();const audioDataArray = new Int16Array(this.audioData);this.OnReceivedData(audioDataArray);this.audioData = [];this.sampleCount += 1;this.bitCount += 1;}};} else { // 不支持this.isLog && console.log(this.requestId, '不支持createScriptProcessor', TAG);}}async audioWorkletNodeDealAudioData(mediaStreamSource, requestId) {try {const audioWorkletBlobURL = window.URL.createObjectURL(new Blob([audioWorkletCode], { type: 'text/javascript' }));await this.audioContext.audioWorklet.addModule(audioWorkletBlobURL);const myNode = new AudioWorkletNode(this.audioContext, 'my-processor', { numberOfInputs: 1, numberOfOutputs: 1, channelCount: 1 });myNode.onprocessorerror = (event) => {// 降级this.scriptNodeDealAudioData(mediaStreamSource, this.requestId);return false;}myNode.port.onmessage = (event) => {console.log(event)this.frameTime.push(`${Date.now()}-${this.frameCount}`);this.OnReceivedData(event.data.audioData);this.frameCount += 1;this.allAudioData.push(...event.data.audioData);this.sampleCount = event.data.sampleCount;this.bitCount = event.data.bitCount;};myNode.port.onmessageerror = (event) => {// 降级this.scriptNodeDealAudioData(mediaStreamSource, requestId);return false;}mediaStreamSource && mediaStreamSource.connect(myNode).connect(this.audioContext.destination);} catch (e) {this.isLog && console.log(this.requestId, 'audioWorkletNodeDealAudioData catch error', JSON.stringify(e), TAG);this.OnError(e);}}// 获取音频数据OnReceivedData(data) { }OnError(res) { }OnStop(res) { }
}
typeof window !== 'undefined' && (window.WebRecorder = WebRecorder);

代码,里面有一些测试demo(不一定能用),看主要代码即可

<template><div style="padding: 20px"><h3>录音上传</h3><div style="font-size: 14px"><el-button type="primary" @click="handleStart">开始录音</el-button><el-button type="info" @click="handlePause">暂停录音</el-button><el-button type="info" @click="handlePlay">播放录音</el-button><el-button type="info" @click="makemp3">生成MP3</el-button></div></div>
</template><script setup>
import lamejs from "lamejs";
import webRecorder from "./assets/js/index";import MPEGMode from "lamejs/src/js/MPEGMode";
import BitStream from "lamejs/src/js/BitStream";// window.MPEGMode = MPEGMode;
// window.Lame = Lame;
// window.BitStream = BitStream;const recorder = new WebRecorder();const audioData = [];function int8ArrayToMp3(int8ArrayData, sampleRate) {const numChannels = 1;const bufferSize = 4096;const encoder = new lamejs.Mp3Encoder(numChannels, sampleRate, 128);let remainingData = int8ArrayData;let mp3Data = [];while (remainingData.length > 0) {const chunkSize = Math.min(bufferSize, remainingData.length);const chunk = remainingData.subarray(0, chunkSize);const leftChannel = new Int16Array(chunk.length);for (let i = 0; i < chunk.length; i++) {leftChannel[i] = chunk[i];}const mp3buffer = encoder.encodeBuffer(leftChannel);if (mp3buffer.length > 0) {mp3Data.push(new Uint8Array(mp3buffer));}remainingData = remainingData.subarray(chunkSize);}const mp3buffer = encoder.flush();if (mp3buffer.length > 0) {mp3Data.push(new Uint8Array(mp3buffer));}return new Blob(mp3Data, { type: "audio/mp3" });
}function int8ArrayToWavURL(int8ArrayData) {const numChannels = 1; // 单声道const sampleRate = 44100; // 采样率const bytesPerSample = 2; // 16-bit audioconst byteRate = sampleRate * numChannels * bytesPerSample;const dataLength = int8ArrayData.length * bytesPerSample;const header = createWavHeader(numChannels, sampleRate, byteRate, dataLength);const wavBuffer = new Uint8Array(header.buffer.byteLength + int8ArrayData.length * bytesPerSample);wavBuffer.set(new Uint8Array(header.buffer), 0);for (let i = 0; i < int8ArrayData.length; i++) {const value = int8ArrayData[i];wavBuffer[i * 2 + 44] = value & 0xff;wavBuffer[i * 2 + 45] = (value >> 8) & 0xff;}const blob = new Blob([wavBuffer], { type: "audio/wav" });return URL.createObjectURL(blob);
}function createWavHeader(numChannels, sampleRate, byteRate, dataLength) {const buffer = new ArrayBuffer(44);const view = new DataView(buffer);// RIFF chunk descriptorwriteString(view, 0, "RIFF");view.setUint32(4, 36 + dataLength, true);writeString(view, 8, "WAVE");// fmt sub-chunkwriteString(view, 12, "fmt ");view.setUint32(16, 16, true);view.setUint16(20, 0x0001, true); // WAVE_FORMAT_PCMview.setUint16(22, numChannels, true);view.setUint32(24, sampleRate, true);view.setUint32(28, byteRate, true);view.setUint16(32, 2, true); // BLOCK_ALIGNview.setUint16(34, 16, true);// data sub-chunkwriteString(view, 36, "data");view.setUint32(40, dataLength, true);return view;
}function writeString(view, offset, string) {for (let i = 0; i < string.length; i++) {view.setUint8(offset + i, string.charCodeAt(i));}
}// 获取采集到的音频数据
// recorder.OnReceivedData = (data) => {
//   // console.log(data);
//   audioData.push(...data);
// };// 获取采集到的音频数据
recorder.OnReceivedData = (data) => {// console.log(data);handlePlay2(data);
};const handleStart = () => {recorder.start();
};const handlePause = () => {recorder.stop();
};function downloadMP3(url, filename) {const a = document.createElement("a");a.href = url;a.download = filename || "audio.mp3";document.body.appendChild(a);a.click();document.body.removeChild(a);
}let i = 0;
let tempAudioBuffer = []; // 用于存储累积的音频数据
let startTime = null; // 记录开始累积的时间
const handlePlay2 = (audioData) => {i += 1;// 将音频数据转换为 Int16Array//const int16ArrayAudioData = new Int16Array(audioData);// 如果这是第一次接收数据,记录开始时间if (startTime === null) {startTime = Date.now();}// 将新接收到的数据添加到缓冲区tempAudioBuffer.push(...audioData);// 检查是否已经累积了3秒的数据const currentTime = Date.now();if (currentTime - startTime >= 5000) { // 5000毫秒即5秒startTime = Date.now();let copiedArray = [...tempAudioBuffer];tempAudioBuffer=[];processAudioBuffer2(copiedArray);// // 重置变量以准备下一次累积// audioBuffer = [];}
};
const processAudioBuffer2 = (audioBuffer) => {// console.log(audioData);// 转 wavconst int16ArrayAudioData = new Int16Array(audioBuffer);//console.log("8位录音数据:",int16ArrayAudioData);var mp3Data = [];// var audioData; // 假设这里是你的 PCM 音频数据var sampleRate = 16000; // 通常的采样率const encoder = new lamejs.Mp3Encoder(1, sampleRate, 128);var mp3Tmp = encoder.encodeBuffer(int16ArrayAudioData);mp3Data.push(mp3Tmp);mp3Tmp = encoder.flush(); // Write last data to the output data, toomp3Data.push(mp3Tmp); // mp3Data contains now the complete mp3Datavar blob = new Blob(mp3Data, { type: "audio/mp3" });var url = URL.createObjectURL(blob);var a = document.createElement("a");a.href = url;a.download = "recording.mp3";document.body.appendChild(a);a.click();
};const handlePlay = () => {// console.log(audioData);// 转 wavconst int16ArrayAudioData = new Int16Array(audioData);console.log("8位录音数据:",int16ArrayAudioData);var mp3Data = [];// var audioData; // 假设这里是你的 PCM 音频数据var sampleRate = 16000; // 通常的采样率const encoder = new lamejs.Mp3Encoder(1, sampleRate, 128);var mp3Tmp = encoder.encodeBuffer(int16ArrayAudioData);mp3Data.push(mp3Tmp);mp3Tmp = encoder.flush(); // Write last data to the output data, toomp3Data.push(mp3Tmp); // mp3Data contains now the complete mp3Datavar blob = new Blob(mp3Data, { type: "audio/mp3" });var url = URL.createObjectURL(blob);var a = document.createElement("a");a.href = url;a.download = "recording.mp3";document.body.appendChild(a);a.click();
};const makemp3 = () => {var mp3Data = [];var mp3encoder = new lamejs.Mp3Encoder(1, 44100, 128); // mono 44.1kHz encode to 128kbps// 生成一秒钟的正弦波样本var sampleRate = 44100;var frequency = 440; // A4 音符var samples = new Int16Array(sampleRate);for (var i = 0; i < sampleRate; i++) {samples[i] = 32767 * Math.sin(2 * Math.PI * frequency * (i / sampleRate)); // 生成正弦波}console.log("16位正弦数据:",samples);var mp3Tmp = mp3encoder.encodeBuffer(samples); // encode mp3mp3Data.push(mp3Tmp); // Push encode buffer to mp3Data variablemp3Tmp = mp3encoder.flush(); // Write last data to the output data, toomp3Data.push(mp3Tmp); // mp3Data contains now the complete mp3Datavar blob = new Blob(mp3Data, { type: "audio/mp3" });var url = URL.createObjectURL(blob);var a = document.createElement("a");a.href = url;a.download = "recording.mp3";document.body.appendChild(a);a.click();
}
</script>


http://www.ppmy.cn/devtools/132322.html

相关文章

SpringMVC快速上手

便利之处 springMVC在web项目中主要的作用就是对请求和响应的处理&#xff1b; 处理请求 原先我们需要获取前端发送的简单参数需要通过httpServletRequest.getParameter来获取 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletExce…

HTTPSOK:SSL/TLS证书自动续期工具

HTTPSOK 是一个支持 SSL/TLS 证书自动续期 的工具&#xff0c;旨在简化 SSL 证书的管理&#xff0c;尤其是自动化处理证书续期的工作。对于大多数网站而言&#xff0c;SSL 证书的续期是一项必要但容易被忽视的工作&#xff0c;因为 SSL 证书的有效期通常为 90 天。使用 HTTPSOK…

Dubbo负载均衡

负载均衡策略与配置细节 Dubbo 内置了 client-based 负载均衡机制&#xff0c;如下是当前支持的负载均衡算法&#xff0c;结合上文提到的自动服务发现机制&#xff0c;消费端会自动使用 Weighted Random LoadBalance 加权随机负载均衡策略 选址调用。 如果要调整负载均衡算法…

leetcode 2043.简易银行系统

1.题目要求: 示例: 输入&#xff1a; ["Bank", "withdraw", "transfer", "deposit", "transfer", "withdraw"] [[[10, 100, 20, 50, 30]], [3, 10], [5, 1, 20], [5, 20], [3, 4, 15], [10, 50]] 输出&#xff…

Delta Lake

什么是 Delta Lake&#xff1f; Delta Lake 是经过优化的存储层&#xff0c;为 Databricks 上湖屋中的表提供了基础。 Delta Lake 是开源软件&#xff0c;它使用基于文件的事务日志扩展了 Parquet 数据文件&#xff0c;可以处理 ACID 事务和可缩放的元数据。 Delta Lake 与 Ap…

一些 uniapp相关bug

1.当input聚焦时布局未上移 <scroll-view style"height: calc(100vh - 100rpx - 38rpx)" :scroll-y"true"><wd-form ref"formRef" :model"fbObj">....<wd-inputlabel"联系方式"prop"contact"clear…

CentOS 7 更换软件仓库

CentOS 7 于2024年6月30日停止维护&#xff0c;官方仓库已经没有软件了&#xff0c;想要继续使用 &#xff0c;需要更换软件仓库&#xff0c;这里更换到阿里云的软件仓库 https://developer.aliyun.com/mirror/ 查看目前可用的软件数量 yum repolist 更换软件仓库&#xff1a…

成都睿明智科技有限公司共赴抖音电商蓝海

在这个短视频风起云涌的时代&#xff0c;抖音作为现象级的社交媒体平台&#xff0c;不仅改变了人们的娱乐方式&#xff0c;更悄然间重塑了电商行业的格局。在这片充满机遇与挑战的蓝海中&#xff0c;成都睿明智科技有限公司凭借其敏锐的市场洞察力和专业的服务能力&#xff0c;…