Web 音频录制与处理
Web 音频的核心依赖两套 API:
MediaStream(麦克风采集) +MediaRecorder(音频录制) 用于基础的录音与导出;而AudioContext(Web Audio API) 则用于底层、复杂的音频流分析与处理(如实时波形图、变声、降噪)。
1. 核心 API 解析
1.1 navigator.mediaDevices.getUserMedia (获取麦克风流)
这是所有音视频操作的入口,用于向用户申请麦克风权限,并返回一个 MediaStream 流。
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
// 成功获取到音频流
})
.catch((err) => {
console.error("麦克风权限被拒绝或设备不可用", err);
});
面试安全踩坑点:
- HTTPS 限制:
getUserMedia必须在 HTTPS 环境下才能调用(本地localhost/127.0.0.1除外)。如果在 HTTP 环境下,navigator.mediaDevices将会是undefined。 - 授权机制:在 HTTPS 下,用户授权一次后,浏览器通常会记住授权状态,后续不需要每次都弹窗重新授权;而在不安全的 HTTP 协议下(某些老版本浏览器允许),每次调用都会强制弹窗授权。
1.2 MediaRecorder (音频录制)
拿到 MediaStream 之后,如何把声音“录下来”并保存为文件?这就必须用到 MediaRecorder
// 1. 传入麦克风流,实例化录制器
const mediaRecorder = new MediaRecorder(stream);
const audioChunks = [];
// 2. 监听数据可用事件,收集音频片段
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
// 3. 监听录制停止事件,合成最终的音频文件 (Blob)
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
const audioUrl = URL.createObjectURL(audioBlob);
// 接下来可以播放 (new Audio(audioUrl)) 或者上传给服务器
};
// 4. 控制录制
mediaRecorder.start(); // 开始录制
// mediaRecorder.stop(); // 停止录制
1.3 AudioContext (Web Audio API)
这是 Web 音频的终极武器,主要用于在音频流输出到扬声器之前,对其进行分析、合成、滤波、变声等底层处理。
核心工作流(节点连接 Node Routing): 音频源 (Source) -> 处理节点 (Analyser/Gain/BiquadFilter) -> 最终输出目的地 (Destination)。
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// 1. 创建音频源节点 (将麦克风流接入)
const sourceNode = audioCtx.createMediaStreamSource(stream);
// 2. 创建处理节点:比如分析节点 (用于画波形图)
const analyserNode = audioCtx.createAnalyser();
// 3. 连接节点
sourceNode.connect(analyserNode);
// analyserNode.connect(audioCtx.destination); // 如果需要自己听到自己的回音,连到 destination
2. 面试高频场景题
场景一:如何实现一个实时的“音频波形图 (可视化)”?
答题思路:
- 通过
getUserMedia获取麦克风流。 - 实例化
AudioContext,并将流作为输入源接入createMediaStreamSource。 - 连接一个
AnalyserNode(分析节点)。 - 使用
requestAnimationFrame在每一帧调用analyser.getByteFrequencyData(dataArray)获取当前音频的频域数据。 - 数据映射原理:
getByteFrequencyData返回的数组中,每个元素的值都在 0 ~ 255 之间(代表特定频率的音量强度)。我们需要把这个0-255的值,按比例映射到 Canvas 的高度上。
完整代码示例(绘制跳动的频谱条):
// 假设已经拿到了 analyser 和 canvas 上下文 ctx
analyser.fftSize = 256; // 决定频谱条的数量 (fftSize的一半)
const bufferLength = analyser.frequencyBinCount; // 128
const dataArray = new Uint8Array(bufferLength); // 长度128的数组,每个值在 0~255
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const barWidth = (canvasWidth / bufferLength) * 2.5; // 计算单根柱子的宽度
function draw() {
requestAnimationFrame(draw); // 循环调用
// 1. 将当前频域数据填充到数组中
analyser.getByteFrequencyData(dataArray);
// 2. 清空上一帧的画布
ctx.fillStyle = "rgb(0, 0, 0)";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// 3. 循环绘制每一根柱子
let x = 0;
for (let i = 0; i < bufferLength; i++) {
// 核心映射逻辑:dataArray[i] 最大是 255,按比例映射为 canvas 高度
const barHeight = (dataArray[i] / 255) * canvasHeight;
// 绘制柱子(颜色可以随高度变化)
ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
ctx.fillRect(x, canvasHeight - barHeight, barWidth, barHeight); // 从下往上画
x += barWidth + 1; // 加上间隙
}
}
draw(); // 启动绘制循环
场景二:录音结束后,如何将音频上传给后端?
答题思路:
- 在
MediaRecorder.onstop中,将收集到的audioChunks转化为Blob对象。 - 使用
FormData包装这个 Blob。 - 通过
fetch或axios发送POST请求(注意 Content-Type 会自动设为multipart/form-data)。
const formData = new FormData();
// 参数: 字段名, Blob对象, 文件名
formData.append("audioFile", audioBlob, "recording.webm");
fetch("/api/upload", { method: "POST", body: formData });
场景三:浏览器音频自动播放策略 (Autoplay Policy) 拦截了怎么办?
答题思路:
现代浏览器(尤其是 Chrome 和 Safari)严格限制了音频的自动播放:必须要有用户的实际交互(点击、触摸)后,才能播放音频或启动 AudioContext。
- 表现:如果在页面加载时直接
audio.play()或者初始化new AudioContext(),控制台会报Autoplay is only allowed...的警告,并且状态会被挂起。 - 解决方案:
- 页面初始化时给出明显的交互按钮(如“点击开始体验”)。
- 在按钮的
click事件回调中去执行audio.play()。 - 对于
AudioContext,如果是在交互前创建的,它的状态会是suspended。需要在用户点击事件里调用audioCtx.resume()唤醒它。
场景四:如果想实现微信语音那样的“长按录音,松开发送,上滑取消”,怎么做?
答题思路: 考察对移动端 Touch 事件和 MediaRecorder 状态机制的综合掌握:
- 监听
touchstart:调用mediaRecorder.start()开始录音,并记录初始触摸点 Y 坐标。 - 监听
touchmove:实时计算当前 Y 坐标与初始坐标的差值。如果向上滑动距离超过阈值(如 50px),UI 提示“松开手指,取消发送”。 - 监听
touchend:调用mediaRecorder.stop()。- 在
onstop的回调中判断:如果是正常松开,走上传逻辑;如果是上滑触发的松开,丢弃audioChunks数组,清空数据。
- 在