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
MediaRecorderis 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 withMediaRecorder. getUserMedia()requires a secure context (HTTPS or localhost) and explicit microphone permission from the user.- Use
AudioWorkletfor custom DSP work;ScriptProcessorNodeis 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:
- Capture —
getUserMedia()requests microphone access and returns aMediaStream. - Process (optional) — The Web Audio API inspects or transforms the stream through audio nodes.
- Record —
MediaRecorderencodes 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,
AudioContextstarts in a suspended state until triggered by a direct user interaction. Create it (or callaudioContext.resume()) inside a button click handler, not on page load.
Discover how at OpenReplay.com.
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
| Need | Use |
|---|---|
| Simple microphone recording | getUserMedia + MediaRecorder |
| Real-time effects or filters | Web Audio API nodes |
| Custom DSP processing | AudioWorklet |
| Audio visualization | AnalyserNode |
| Encoding to a file | MediaRecorder |
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
AudioContexton 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.