Skip to main content

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. 面试高频场景题

场景一:如何实现一个实时的“音频波形图 (可视化)”?

答题思路:

  1. 通过 getUserMedia 获取麦克风流。
  2. 实例化 AudioContext,并将流作为输入源接入 createMediaStreamSource
  3. 连接一个 AnalyserNode (分析节点)
  4. 使用 requestAnimationFrame 在每一帧调用 analyser.getByteFrequencyData(dataArray) 获取当前音频的频域数据。
  5. 数据映射原理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(); // 启动绘制循环

场景二:录音结束后,如何将音频上传给后端?

答题思路:

  1. MediaRecorder.onstop 中,将收集到的 audioChunks 转化为 Blob 对象。
  2. 使用 FormData 包装这个 Blob。
  3. 通过 fetchaxios 发送 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... 的警告,并且状态会被挂起。
  • 解决方案
    1. 页面初始化时给出明显的交互按钮(如“点击开始体验”)。
    2. 在按钮的 click 事件回调中去执行 audio.play()
    3. 对于 AudioContext,如果是在交互前创建的,它的状态会是 suspended。需要在用户点击事件里调用 audioCtx.resume() 唤醒它。

场景四:如果想实现微信语音那样的“长按录音,松开发送,上滑取消”,怎么做?

答题思路: 考察对移动端 Touch 事件和 MediaRecorder 状态机制的综合掌握:

  1. 监听 touchstart:调用 mediaRecorder.start() 开始录音,并记录初始触摸点 Y 坐标。
  2. 监听 touchmove:实时计算当前 Y 坐标与初始坐标的差值。如果向上滑动距离超过阈值(如 50px),UI 提示“松开手指,取消发送”。
  3. 监听 touchend:调用 mediaRecorder.stop()
    • onstop 的回调中判断:如果是正常松开,走上传逻辑;如果是上滑触发的松开,丢弃 audioChunks 数组,清空数据。