Back

在浏览器中使用 Web Audio API 录制音频

在浏览器中使用 Web Audio API 录制音频

大多数开发者认为 Web Audio API 负责录音。其实并非如此——至少不是直接负责。Web Audio API 是一个处理与路由引擎,真正的录音工作由 MediaRecorder API 完成。理解这一区别,是避免走错方向的最快方式。

本文将介绍当前推荐的浏览器音频录制架构:使用 getUserMedia() 捕获麦克风输入,可选地将音频通过 Web Audio 图进行路由,并使用 MediaRecorder 进行编码。

核心要点

  • Web Audio API 用于处理和路由音频,但真正将音频录制成文件的是 MediaRecorder
  • 完整的录音流程包含三个阶段:使用 getUserMedia() 捕获、可选地通过 Web Audio 节点处理、使用 MediaRecorder 编码。
  • getUserMedia() 需要安全上下文(HTTPS 或 localhost),并需要用户明确授予麦克风权限。
  • 自定义 DSP 处理请使用 AudioWorkletScriptProcessorNode 已废弃,且在高负载下会出现卡顿。
  • 录制前务必使用 MediaRecorder.isTypeSupported() 检查 MIME 类型支持情况,因为 Safari 和 Chrome 支持的格式有所不同。

浏览器音频录制的实际工作原理

现代录音流程包含三个明确的阶段:

  1. 捕获getUserMedia() 请求麦克风访问权限,并返回一个 MediaStream
  2. 处理 (可选) — Web Audio API 通过音频节点检查或转换音频流。
  3. 录制MediaRecorder 将音频流编码为文件。

基本录音并不需要 Web Audio API。但如果你需要噪声过滤、增益控制、可视化,或通过 AudioWorklet 实现自定义效果,它可以无缝嵌入到这条链路的中间环节。

一个细节虽小但很重要:MediaRecorder 直接录制的是 MediaStream。如果你想录制 Web Audio 图的处理后输出,需要使用 audioContext.createMediaStreamDestination() 将音频图重新路由回一个流,并将该节点的 .stream 传递给 MediaRecorder

第 1 步:使用 getUserMedia 请求麦克风访问权限

getUserMedia() 需要一个安全上下文(HTTPS 或 localhost),并且必须经过用户明确授权后浏览器才会授予麦克风访问权限。

try {
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
      echoCancellation: true,
      noiseSuppression: true,
      sampleRate: 44100
    }
  });
} catch (err) {
  console.error('Microphone access failed:', err.name, err.message);
}

务必使用 try/catch 包裹该调用。用户可能拒绝授权,某些环境(如 HTTP 页面)会直接以 NotAllowedErrorSecurityError 拒绝该调用。需要注意的是,sampleRate 等约束只是建议——浏览器可能会根据底层硬件忽略这些设置。

第 2 步:通过 Web Audio API 进行路由(可选)

如果你需要在录制前分析或处理音频,可以创建一个 AudioContext 并将音频流接入其中:

const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);

// Example: connect to an AnalyserNode for visualization
const analyser = audioContext.createAnalyser();
source.connect(analyser);

// To record the processed output, route to a MediaStreamDestination
const destination = audioContext.createMediaStreamDestination();
analyser.connect(destination);
// destination.stream is what you pass to MediaRecorder

对于自定义处理——例如噪声门或实时电平表——请使用 AudioWorklet,而不是已废弃的 ScriptProcessorNode。AudioWorklet 运行在主线程之外,这意味着它不会阻塞 UI,也不会在高负载下丢失音频样本。

注意: 在 iOS Safari 上,AudioContext 会以挂起状态启动,直到由直接的用户交互触发。请在按钮点击处理函数中创建它(或调用 audioContext.resume()),而不是在页面加载时。

第 3 步:使用 MediaRecorder 进行录制和导出

MediaRecorder 接收一个 MediaStream 并对其进行编码。不要硬编码 MIME 类型——先检查支持情况:

function pickMimeType() {
  const candidates = [
    'audio/webm;codecs=opus',
    'audio/ogg;codecs=opus',
    'audio/mp4'
  ];
  return candidates.find(type => MediaRecorder.isTypeSupported(type)) || '';
}

const mimeType = pickMimeType();
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
const chunks = [];

recorder.ondataavailable = (e) => {
  if (e.data.size > 0) chunks.push(e.data);
};

recorder.onstop = () => {
  const blob = new Blob(chunks, {
    type: recorder.mimeType || mimeType || chunks[0]?.type || 'audio/webm'
  });

  const url = URL.createObjectURL(blob);
  document.querySelector('audio').src = url;
};

recorder.start();

WebM/Opus 通常是现代浏览器的最佳默认选项——文件体积小,质量出色。而 Safari 通常更倾向于 audio/mp4。最可靠的策略是:维护一个候选类型列表逐个回退,让浏览器选择它支持的第一个类型。你可以在 Can I Use 上查看当前浏览器对 MediaRecorder 及相关格式的支持情况。

MediaRecorder 与 Web Audio API:快速对比

需求使用
简单的麦克风录音getUserMedia + MediaRecorder
实时效果或滤波Web Audio API 节点
自定义 DSP 处理AudioWorklet
音频可视化AnalyserNode
编码为文件MediaRecorder

应避免的做法

  • ScriptProcessorNode — 已废弃,运行在主线程上,高负载下会引起音频卡顿。
  • 硬编码 MIME 类型 — 在 Safari 和较旧版本的 Firefox 上会静默失败。
  • 在页面加载时创建 AudioContext — 浏览器会将其挂起,直到用户进行手势操作,因此应在事件处理函数中恢复它。
  • 忘记停止音频流 — 完成后调用 stream.getTracks().forEach(t => t.stop()),否则麦克风指示器会一直亮着。

结语

浏览器音频录制是两个 API 协作完成的工作:getUserMedia()MediaRecorder 负责捕获与编码,而 Web Audio API 负责中间的所有处理。从满足需求的最简流程开始,仅在必要时加入 Web Audio 处理,并始终在配置 MediaRecorder 之前检查 MIME 类型支持情况。一句话即可概括整个架构。

常见问题

不必。对于基础录音,getUserMedia() 和 MediaRecorder 已经足够。只有当你需要在录制前对音频进行处理、分析或转换时——例如应用滤波器、构建可视化或通过 AudioWorklet 运行自定义 DSP——才有必要使用 Web Audio API。

Safari 和 Chrome 支持不同的录音格式。Safari 通常更倾向于 audio/mp4,而 Chrome 通常使用 WebM/Opus。如果你硬编码了不支持的 MIME 类型,MediaRecorder 可能会抛出错误或产生无法使用的输出。请始终使用 MediaRecorder.isTypeSupported() 在运行时检测兼容的格式。

使用 audioContext.createMediaStreamDestination() 创建一个 MediaStreamAudioDestinationNode,将音频图的最终节点连接到它,并将其 .stream 属性传递给 MediaRecorder。这样捕获的就是处理后的音频,而不是来自 getUserMedia() 的原始麦克风音频流。

iOS Safari 要求 AudioContext 必须在直接的用户手势(例如点击或触摸事件)内创建或恢复。如果你在页面加载时实例化它,它会保持挂起状态。请将 AudioContext 的创建移入按钮处理函数中,或在其中调用 audioContext.resume() 来解锁播放和处理。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay