【javascript】录音可视化

devtools/2025/2/15 21:28:40/
webkit-tap-highlight-color: rgba(0, 0, 0, 0);">

文章目录

  • 前言
  • 一、音频数据格式
    • 1. 常见的数据格式
  • 二、获取音频方式
    • 1. 用FileReader对象读取音频文件
    • 2. 通过url请求获取音频数据
    • 3. 录音获取音频流
  • 三、音频格式转换
  • 四、音频可视化
  • 总结


前言

分享一些在开发语音识别功能遇到的问题和解决方法,包含音频数据格式转换,大文件上传,录音权限获取,音频可视化


一、音频数据格式

1. 常见的数据格式

从音质,文件大小,应用场景三方面对比如下,文件大小无损(pcm,wav,flac)大于有损(mp3,ogg,opus),实时语音需要低延迟opus,而高进度语音识别需要高质格式wav或pcm

数据格式音质文件大小应用场景
MP3 (MPEG-1 Audio Layer III)有损压缩,支持多种比特率(如128/192/320kbps)较小适合网络传输和移动设备,音质在较高比特率下接近无损,但不适合高精度语音识别
OGG (Ogg Vorbis)有损压缩,支持多种比特率,类似mps3,但通常提供更好的压缩效率和音质较小适合网络传输和移动设备,不适合高精度语音识别
Opus高效的有损压缩,音质在低比特率下表现良好,支持多种比特率和采样率较小适合实时语音通信和语音识别系统,尤其是需要低延迟的场景
WMA (Windows Media Audio)有损压缩,支持多种比特率,类似mps3,但通常提供更好的压缩效率和音质,专为Windows平台优化较小适合Windows平台的音频传输和存储,不适合高精度语音识别
AMR (Adaptive Multi-Rate)有损未压缩,专为移动通信设计,能够根据网络条件动态调整比特率较小广泛用于移动设备的语音通话和语音识别
WAV (Waveform Audio File Format)无损压缩,支持多种采样率(8/16/44.1khz)和位深(8/6位)较大广泛用于语音识别、音频编辑和存储
FLAC (Free Lossless Audio Codec)无损压缩,能够将音频压缩到大约原始大小的一半,同时保持音质不变比wav小适合需要高质量音频但又希望减小文件大小的场景
PCM (Pulse Code Modulation)无损未压缩,直接将模拟音频信号转换为数字信号,提供最高的音质,支持多种采样率和位深较大常用于语音识别系统中的原始音频数据处理

二、获取音频方式

1. 用FileReader对象读取音频文件

FileReader 只能访问用户明确选择的文件内容,可以从input标签选择文件,或者从拖放对象里获取,可以将文件读取以二进制和文本输出,reader.readAsArrayBuffer(data)//data是 Blob 或 File 对象,reader.readAsDataURL(data),reader.readAsText(data),以下是通过input方式:

<input ref="fileInput" type="file" @change="getFile" />
const getFile = async (event: any) => {const file = event.target.files[0];const reader = new FileReader();reader.onload = (e: any) => {const arrayBuffer = e.target.result;};reader.readAsArrayBuffer(file);
};

2. 通过url请求获取音频数据

以下是通过fetch获取本地音频资源:

import testurl from "@/assets/test.mp3"
async function fetchAndConvertToBlob(url: string) {const response = await fetch(url)const blob = await response.blob()return blob
}

3. 录音获取音频流

(1)设置触发事件
Pointer Events 提供了一种统一的方式来处理不同类型的输入设备,包括鼠标、触摸屏和手写笔。可能会存在一些旧版本兼容问题,(推荐polyfill和shim处理兼容)

触摸事件防抖处理

const debounce = (fn: any, delay: any) => {let timer: anyreturn (...args: any[]) => {if (timer) {clearTimeout(timer) // 清除前一个定时器}timer = setTimeout(() => {fn(...args) // 调用函数,传递参数}, delay)}
}const debounceStartRecord = debounce(startRecord,500)
const debounceEndRecord = debounce(endRecord,500)
// startRecord和endRecord实现见下,音频格式转换处理

(2)判断应用设备麦克风授权,支持录音API

const handlePermissionDenied = async (fn: () => void) => {const checkPermissionSupport = 'permissions' in navigatorconst checkMediaDevicesSupport = async () => {if ('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) {try {// 使用getUserMedia方法获取麦克风权限const stream = await navigator.mediaDevices.getUserMedia({ audio: true });// 录音权限已授权,去释放资源stream.getTracks().forEach((track) => track.stop());return true} catch (error) {console.error("navigator getUserMedia failed:", error);return false}} else {return false}}const permissionDeniedMessage = "您已拒绝麦克风权限。请在设置中授权麦克风权限,以便使用录音功能。";const permissionPromptMessage = "浏览器将提示您授权麦克风权限。请在设置中授权麦克风权限,以便使用录音功能。";const permissionQueryFailedMessage = "无法查询麦克风权限状态。请在设置中授权麦克风权限,以便使用录音功能。";const deviceNotSupportedMessage = "您的设备不支持录音功能。建议您使用支持该功能的浏览器或更新系统版本";if (checkPermissionSupport) {console.log("navigator.permissions is supported");try {const permissionStatus = await navigator.permissions.query({ name: "microphone" as PermissionName });console.log('permissionStatus', permissionStatus);if (permissionStatus.state === "granted") {// 录音权限已授权fn();} else if (permissionStatus.state === "prompt") {// 浏览器将提示用户进行授权,部分浏览器没有提示,需直接请求媒体设备const mediaDevicesPermission = await checkMediaDevicesSupport();if (mediaDevicesPermission) {// 录音权限已授权fn();} else {alert(permissionPromptMessage);}} else if (permissionStatus.state === "denied") {// 录音权限被拒绝alert(permissionDeniedMessage);}} catch (error) {console.error("Permission query failed:", error);alert(permissionQueryFailedMessage);}} else if (await checkMediaDevicesSupport()) {console.log("navigator.permissions is not supported, but navigator.mediaDevices.getUserMedia is supported");fn();} else {alert(deviceNotSupportedMessage);}
};

(3)使用audio-recorder-polyfill库处理音频文件转换

满足兼容不同系统和设备的WebRTC录音api支持,并提供释放麦克风方法,可以转换wav文件,自定义采样位数 (sampleBits)16,采样率(sampleRate)16000Hz,声道(numChannels)1

开始录音

const startRecord = (event?: any) => {event?.stopPropagation()handlePermissionDenied(() => {console.log("开始录音", isRecording.value)isPermissioning = false // 避免重复点击if (!isRecording.value) {try {navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {pcmRecorder.value = new AudioRecorder(stream, {sampleBits: 16, // 采样位数,支持 8 或 16,默认是16sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000numChannels: 1 // 声道,支持 1 或 2, 默认是1})pcmRecorder.value.start()changeRecordState("start")})} catch (error) {console.error("开始录音失败", error)}}})
}

结束录音

const endRecord = (e?: any) => {e?.preventDefault()console.log("结束录音", isRecording.value, isPermissioning)if (isPermissioning) {isRecording.value = false}if (isFouceEnd || !isRecording.value) {return}try {if (pcmRecorder.value) {// console.log(pcmRecorder.value)pcmRecorder.value.stop()pcmRecorder.value.addEventListener("dataavailable", async (e: any) => {pcmData.value = e.data// console.log("pcmData.value", pcmData.value)let base64String: any = await blobToBase64(pcmData.value)// console.log("pcmstr", base64String)base64String = base64String.split(",")[1]audioToText({ voice: base64String }).then((res: any) => {console.log("res", res)if (res.text) {curText.value = res.textsendMsg({ type: "model" })} else {// 判断当前识别成功的文字,如果第一次对话res.text为空,仍然展示关键字跳转if (msgList.value.length == 1) {isShowLink.value = true}}})})if (pcmRecorder.value && pcmRecorder.value.stream) {pcmRecorder.value.stream.getTracks().forEach((track: any) => {track.stop()track.applyConstraints({ echoCancellation: false }).catch((error: any) => {console.error("更新轨道状态失败2:", error)})})pcmRecorder.value.stream = null // 释放 MediaStream 对象}changeRecordState("end")}} catch (error) {changeRecordState("end")console.error("停止录音失败:", error)}
}

@vitejs/plugin-legacy 插件的主要作用是为旧版浏览器提供兼容性支持,通过 Babel 转译代码,使其可以在旧版浏览器中运行。它主要处理 JavaScript 语法的兼容性问题,而不是特定的 Web API(如 navigator.mediaDevices.getUserMedia 或 navigator.permissions)。这里推荐webrtc-adapter 和 permissions-api ,webrtc-adapter 是一个 Polyfill,用于解决 WebRTC API 的兼容性问题,特别是 navigator.mediaDevices.getUserMedia 方法的兼容性问题;permissions-api 是一个 Polyfill,用于解决 navigator.permissions API 的兼容性问题。navigator.permissions 是一个较新的 API,用于查询和监听权限状态。

Recorder.js 提供了更底层的控制,适合需要自定义音频处理的场景。
AudioRecorder 是一个更简单的封装,适合快速实现录音功能。
https://blog.csdn.net/gitblog_00046/article/details/137811883
两者都能满足你的需求(16 位采样位数、16000Hz 采样率、单声道),并在录音结束后释放麦克风资源。

三、音频格式转换

二进制数据转换
在这里插入图片描述

数据格式概念场景
Data URL是一种将文件内容直接嵌入到页面中的方式,通常以 data: 协议开头。它将文件内容编码为 Base64 字符串适合嵌入到 HTML 或 CSS 中,便于显示和传输,适合小文件,会增加页面大小,可能受浏览器存储限制
Blob URL是一种指向 Blob 对象的 URL,通常以 blob: 协议开头临时的url,适用于大文件(如音频,视频),便于在 或 标签中直接播放,不增加页面大小
Blob(Binary Large Object)表示了一个不可变、原始数据的类文件对象。Blob 可以包含任意类型的数据,如文本、二进制数据、JSON 等常用于处理文件上传、下载等操作。Blob 的大小可以从 0 到浏览器所允许的最大值不等
ArrayBuffer表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 的内容不能直接操作,只能通过 DataView 对象或 TypedArrray 对象来访问适合底层处理,如音频分析或使用 Web Audio AP

备注: Blob.arrayBuffer() 方法是一种较新的基于 Promise 的 API,用于将文件读取为数组缓冲区。
const encodedData = window.btoa(“Hello, world”); // 编码字符串
const decodedData = window.atob(encodedData); // 解码字符串

  1. buffer和blob互相转换

const fileToBlob = (file: File) => {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = function (e: any) {const blob = new Blob([e.target.result], { type: file.type });// buffer转blobconst blobUrl = URL.createObjectURL(blob);//创建blob urlresolve(blobUrl)};reader.onerror = (error) => {reject(error);};reader.readAsArrayBuffer(file); // 读取为 ArrayBuffer})
}const bufferToAudioUrl = (buffer: any) => {/*MIME 类型:1、 mp3:audio/mpeg 或 audio/mp3(所有主流浏览器均支持)2、 wav:/wav、audio/wave 或 audio/x-wav(所有主流浏览器均支持)3、 mp4:audio/mp4 或 audio/x-m4a(所有主流浏览器均支持)4、 ogg:audio/ogg(Chrome、Firefox、Edge 支持,但 Safari 不支持)5、 webm:audio/webm(Chrome、Firefox、Edge 支持,但 Safari 支持有限)*/const blob = new Blob([buffer], { type: "audio/mpeg" });return URL.createObjectURL(blob); //创建blob url
}export const blobToBuffer = (blob: Blob) => {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = () => {resolve(reader.result); // reader.result 是 ArrayBuffer};reader.onerror = (error) => {reject(error);};reader.readAsArrayBuffer(blob);});
}
  1. blob转base64
const blobToBase64 = (blob: Blob) => {return new Promise((resolve, reject) => {const reader = new FileReader()reader.onload = () => {resolve(reader.result)}reader.onerror = (error) => {reject(error)}reader.readAsDataURL(blob) // 将 Blob 转换为 Base64 字符串})
}
  1. base64转Uint8Array
export function base64ToUint8Array(base64: string) {// 解码Base64字符串const byteCharacters = atob(base64.trim());// 将解码后的二进制字符串转换为Uint8Arrayconst byteNumbers = new Uint8Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}return byteNumbers;//byteNumbers.buffer是arraybuffer类型
};

四、音频可视化

主要是通过arraybuffer进行音频可视化将音频文件读取为 ArrayBuffer 是一种更底层的方法,适用于需要对音频数据进行进一步处理的场景,例如使用 Web Audio API 进行音频分析或处理。以下是实现思路和部分代码:

  1. 使用AudioContext创建音频上下文,获取音频节点source
    创建音频节点source可以3种方式:audiodom获取audCtx.createMediaElementSource(data),MediaStream媒体流audCtx.createMediaStreamSource(data),音频转成AudioBufferaudCtx.createBufferSource()
  2. 创建音频分析对象audCtx.createAnalyser(),并将音频源(source)连接到音频分析器source.connect(analyser)
  3. 启动音频源播放source.start(0)
  4. 绘制平滑动画使用canvasrequestAnimationFrame
  5. 从分析对象获取当前音频的频率数据,并将其填充到指定的 Uint8Array 数组analyser.getByteFrequencyData(dataArray)
  6. 使用dataArray分析数据进行音频条纹绘制
// 音频分析器
const initAudioAnalyser = async (data: any,type: "audiodom" | "mediastream" | "arraybuffer"
) => {// console.log(data, type);if (data) {// 创建音频节点source可以3种方式:audiodom获取,MediaStream媒体流,音频转成AudioBufferif (type == "audiodom") {isRecording.value = true;animationLoop();source = audCtx.createMediaElementSource(data);} else if (type == "mediastream") {source = audCtx.createMediaStreamSource(data);} else if (type == "arraybuffer") {isRecording.value = true;animationLoop();source = audCtx.createBufferSource();const uint8Array = base64ToUint8Array(data);dataArray = uint8Array;const arrayBuffer = uint8Array.buffer;/*返回audiodom实现思路const audioUrl = bufferToAudioUrl(arrayBuffer);const audioPlayer: any = document.getElementById("audioPlayer");audioPlayer.src = audioUrl;audioPlayer.play();source = audCtx.createMediaElementSource(audioPlayer);*/if (arrayBuffer instanceof ArrayBuffer) {try {const audioBuffer = await audCtx.decodeAudioData(arrayBuffer);console.log("audioBuffer 解码成功:", audioBuffer);source.buffer = audioBuffer;} catch (error: any) {isRecording.value = false;console.error("解码音频数据时出错:", error);// 报解码音频数据时出错: DOMException: Failed to execute 'decodeAudioData' on 'BaseAudioContext': Unable to decode audio data// 这是因为 ArrayBuffer 中的数据不是有效的音频文件格式}}}analyser = audCtx.createAnalyser();analyser.fftSize = 256; // 设置 FFT 大小,影响频率分析的精度,常见的值有 128, 256, 512, 1024, 2048console.log("Audio stream connected to AnalyserNode:",source.numberOfOutputs > 0); //为true表明已经连接到一个源//创建数组,用于接收分析器节点的分析数据dataArray = new Uint8Array(analyser.frequencyBinCount);//analyser.frequencyBinCount表示当前频率的数组长度source.connect(analyser);// analyser.connect(audCtx.destination); //输出设备 播放声音(如果需要将音频输出到扬声器,可以再连接到 audCtx.destination:)if (audCtx.state === "suspended") {// 如果你的代码在没有用户交互的情况下执行,AudioContext 可能不会从 suspended 状态变为 runningsource.start(0);source.onended = function () {console.log("音频播放结束");isRecording.value = false;};}console.log("initAudioAnalyser",audCtx.state,audCtx.sampleRate,dataArray);}
};
// 绘制音频波动的函数
const draw = () => {if (!analyser) {return;}analyser.getByteFrequencyData(dataArray); //让分析器节点分析出数据到数组中const volumeThreshold = 40; // 设置音量阈值,只有当音量高于这个值时才更新可视化// 计算平均振幅let sum = 0;for (let i = 0; i < dataArray.length; i++) {sum += dataArray[i];}const averageAmplitude = sum / dataArray.length;// 计算平均振幅console.log(audCtx.state, dataArray, averageAmplitude, volumeThreshold);// 根据平均振幅决定是否更新可视化if (averageAmplitude > volumeThreshold) {// 更新可视化效果// console.log("音量足够,更新可视化")/*dataArray一直为0,可能导致的原因(1)音频未正确播放,audCtx.state不是running(2)source是否正确连接analyser,source.connect(analyser)(3)getByteFrequencyData调用时机不正确,是否音频播放后调用,需要持续调用,volumeThreshold设置音量(4)采样率不匹配(audCtx.sampleRate和audiobuffer.sampleRate)(5)调试时的时间延迟问题:AnalyserNode必须在音频播放一定时间后才能提供有效的数据如果音频源被静音或音量为0*/// console.log("音乐节点", dataArray)const bufferLength = dataArray.length / 2.5; //一般两半波幅const barWidth = width / bufferLength;// 清空画布ctx.clearRect(0, 0, width, height);ctx.fillStyle = "#000000";for (let i = 0; i < bufferLength; i++) {const data = dataArray[i]; //<256const barHeight = (data / 255) * height; // 乘以height放大波幅// console.log(barHeight)// const x = i * barWidthconst x1 = i * barWidth + width / 2;const x2 = width / 2 - (i + 1) * barWidth;// const y = height - barHeight //底部对齐const y = (height - barHeight) / 2; //中心对其// ctx?.fillRect(x, y, barWidth - 3, barHeight)ctx?.fillRect(x1, y, barWidth - 4, barHeight);ctx?.fillRect(x2, y, barWidth - 4, barHeight);}} else {// 音量较低,不更新可视化// console.log("音量较低,跳过可视化更新")ctx.clearRect(0, 0, width, height);}
};const animationLoop = () => {if (!isRecording.value) {return;}draw();requestAnimationFrame(animationLoop);
};

总结

缝缝补补整理,有错误遗漏希望指出,后续还会补充完善~


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

相关文章

Linux入侵检查流程

1. 初步信息收集 1.1 系统信息 • 目的&#xff1a;了解当前系统的基本情况&#xff0c;包括操作系统版本、内核版本等。 • 命令&#xff1a; # 查看操作系统发行版信息 cat /etc/os-release # 查看内核版本 uname -r 1.2 网络信息 • 目的&#xff1a;查看网络连接状态、…

【LeetCode: 1552. 两球之间的磁力 + 二分】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

QT笔记——QRadioButton

文章目录 1、概要2、实际的应用2.1、创建多个QRadioButton,只可同时选中其中一个&#xff0c;点击后实现对应的槽函数 1、概要 实现QRadioButton相关的应用&#xff1b;2、实际的应用 2.1、创建多个QRadioButton,只可同时选中其中一个&#xff0c;点击后实现对应的槽函数 创建…

Android:播放Rtsp视频流的两种方式

一.SurfaceView Mediaplayer XML中添加SurfaceView: <SurfaceViewandroid:id"id/surface_view"android:layout_width"match_parent"android:layout_height"match_parent"/> Activity代码&#xff1a; package com.android.rtsp;impor…

【第4章:循环神经网络(RNN)与长短时记忆网络(LSTM)— 4.5 序列标注与命名实体识别】

一、引言 嘿,各位技术小伙伴们!今天咱们要来深入聊聊循环神经网络(RNN)和长短时记忆网络(LSTM),这俩在序列标注和命名实体识别领域那可是相当厉害的角色。咱就从最基础的概念开始,一步步揭开它们神秘的面纱,看看它们到底是怎么在实际应用中发挥巨大作用的。 二、序列…

ES常用查询

根据编号查询 GET custom/_search { "query": { "term": { "no": "abc" } } } 查询指定的列 GET custom/_search { "_source": ["id", "no"], "size": 10000, …

深度学习框架探秘|TensorFlow vs PyTorch:AI 框架的巅峰对决

在深度学习框架中&#xff0c;TensorFlow 和 PyTorch 无疑是两大明星框架。前面两篇文章我们分别介绍了 TensorFlow&#xff08;点击查看&#xff09; 和 PyTorch&#xff08;点击查看&#xff09;。它们引领着 AI 开发的潮流&#xff0c;吸引着无数开发者投身其中。但这两大框…

【php】php json_encode($arr) 和 json_encode($arr, 320) 有什么区别?

在 PHP 中&#xff0c;json_encode() 函数用于将 PHP 变量&#xff08;通常是数组或对象&#xff09;编码为 JSON 格式的字符串。json_encode($arr) 和 json_encode($arr, 320) 的区别主要在于第二个参数&#xff0c;该参数是一个由多个 JSON_* 常量按位或&#xff08;|&#x…