Back

Recording Audio in the Browser with Web Audio API

Recording Audio in the Browser with Web Audio API

Most developers assume the Web Audio API handles recording. It doesn’t—not directly. The Web Audio API is a processing and routing engine. The actual recording job belongs to the MediaRecorder API. Understanding this distinction is the fastest way to stop building the wrong thing.

This article walks through the current recommended architecture for browser audio recording: capturing microphone input with getUserMedia(), optionally routing audio through a Web Audio graph, and encoding it with MediaRecorder.

Key Takeaways

  • The Web Audio API processes and routes audio, but MediaRecorder is what actually records it to a file.
  • A complete recording pipeline has three stages: capture with getUserMedia(), optionally process with Web Audio nodes, and encode with MediaRecorder.
  • getUserMedia() requires a secure context (HTTPS or localhost) and explicit microphone permission from the user.
  • Use AudioWorklet for custom DSP work; ScriptProcessorNode is deprecated and causes glitches under load.
  • Always check MIME type support with MediaRecorder.isTypeSupported() before recording, since Safari and Chrome differ on supported formats.

How Browser Audio Recording Actually Works

The modern recording pipeline has three distinct stages:

  1. CapturegetUserMedia() requests microphone access and returns a MediaStream.
  2. Process (optional) — The Web Audio API inspects or transforms the stream through audio nodes.
  3. RecordMediaRecorder encodes the stream into a file.

You don’t need the Web Audio API for basic recording. But if you want noise filtering, gain control, visualization, or custom effects via AudioWorklet, it slots cleanly into the middle of this chain.

A subtle but important detail: MediaRecorder records a MediaStream directly. If you want to record the processed output of a Web Audio graph, you’ll need to route the graph back into a stream using audioContext.createMediaStreamDestination() and pass that node’s .stream to MediaRecorder.

Step 1: Request Microphone Access with getUserMedia

getUserMedia() requires a secure context (HTTPS or localhost) and explicit user permission before the browser grants microphone access.

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);
}

Always wrap this in a try/catch. Users can deny permission, and some environments (like HTTP pages) will reject the call outright with a NotAllowedError or SecurityError. Note that constraints like sampleRate are hints—browsers may ignore them depending on the underlying hardware.

Step 2: Route Through the Web Audio API (Optional)

If you need to analyze or process audio before recording, create an AudioContext and pipe the stream through it:

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

For custom processing—like a noise gate or real-time level meter—use AudioWorklet instead of the deprecated ScriptProcessorNode. AudioWorklet runs off the main thread, which means it won’t block your UI or drop audio samples under load.

Note: On iOS Safari, AudioContext starts in a suspended state until triggered by a direct user interaction. Create it (or call audioContext.resume()) inside a button click handler, not on page load.

Step 3: Record and Export with MediaRecorder

MediaRecorder takes a MediaStream and encodes it. Don’t hardcode a MIME type—check support first:

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 is usually the best default for modern browsers—small file size, excellent quality. Safari often prefers audio/mp4 instead. Falling back through a list of candidate types and letting the browser pick the first one it supports is the most reliable strategy. You can verify current browser support for MediaRecorder and related formats on Can I Use.

MediaRecorder vs Web Audio API: Quick Reference

NeedUse
Simple microphone recordinggetUserMedia + MediaRecorder
Real-time effects or filtersWeb Audio API nodes
Custom DSP processingAudioWorklet
Audio visualizationAnalyserNode
Encoding to a fileMediaRecorder

What to Avoid

  • ScriptProcessorNode — deprecated, runs on the main thread, causes audio glitches under load.
  • Hardcoded MIME types — break silently on Safari and older Firefox.
  • Creating AudioContext on page load — browsers suspend it until a user gesture occurs, so resume it inside an event handler.
  • Forgetting to stop the stream — call stream.getTracks().forEach(t => t.stop()) when finished, or the microphone indicator will stay on.

Conclusion

Browser audio recording is a two-API job: getUserMedia() and MediaRecorder handle capture and encoding, while the Web Audio API handles everything in between. Start with the simplest pipeline that meets your requirements, add Web Audio processing only when you need it, and always check MIME type support before configuring MediaRecorder. That’s the entire architecture in one sentence.

FAQs

No. For basic recording, getUserMedia() and MediaRecorder are enough. The Web Audio API only becomes necessary when you need to process, analyze, or transform the audio before recording, such as applying filters, building visualizations, or running custom DSP through an AudioWorklet.

Safari and Chrome support different recording formats. Safari often prefers audio/mp4, while Chrome commonly uses WebM/Opus. If you hardcode an unsupported MIME type, MediaRecorder may throw an error or produce unusable output. Always use MediaRecorder.isTypeSupported() to detect a compatible format at runtime.

Create a MediaStreamAudioDestinationNode using audioContext.createMediaStreamDestination(), connect the final node of your audio graph to it, and pass its .stream property to MediaRecorder. This captures the post-processed audio rather than the raw microphone stream that came from getUserMedia().

iOS Safari requires AudioContext to be created or resumed inside a direct user gesture, such as a click or touch event. If you instantiate it during page load, it stays suspended. Move the AudioContext creation into a button handler, or call audioContext.resume() inside one to unlock playback and processing.

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