Record plugin

The Record plugin lets you capture audio from the user’s microphone and display a live waveform preview while recording. See the live demo to try it in your browser.


Setup

Install wavesurfer.js (if you haven’t already — see Getting started), then import and register the plugin:

import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.js'

const ws = WaveSurfer.create({
  container: '#waveform',
})

const record = ws.registerPlugin(RecordPlugin.create({
  renderRecordedAudio: true,
}))

CDN / UMD:

<script src="https://unpkg.com/wavesurfer.js@7"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/record.js"></script>
<script>
  const ws = WaveSurfer.create({ container: '#waveform' })
  const record = ws.registerPlugin(WaveSurfer.RecordPlugin.create())
</script>

Options

Option Type Default Description
renderRecordedAudio boolean true Load and display the recorded audio into the waveform when recording ends.
scrollingWaveform boolean false Show a scrolling live waveform during recording (fixed-window, new data enters from the right).
scrollingWaveformWindow number 5 Width of the scrolling window in seconds.
continuousWaveform boolean false Accumulate and grow the waveform from left to right as audio is recorded.
continuousWaveformDuration number Pre-allocate the waveform for this many seconds. Omit to size it to the container width.
mimeType string auto-detected MIME type passed to MediaRecorder (e.g. 'audio/webm'). Falls back to the first browser-supported type.
audioBitsPerSecond number 128000 Encoding bitrate. The default avoids variable-bitrate encoding.
mediaRecorderTimeslice number Interval in milliseconds at which MediaRecorder delivers data chunks. Drives record-data-available.

Recording

Starting and stopping

// Start recording (requests mic permission if needed)
await record.startRecording()

// Stop recording; fires 'record-end' with a Blob
record.stopRecording()

startRecording() is async — it requests microphone access if a stream isn’t already open. Once called, recording begins immediately and the live waveform preview (if configured) activates.

stopRecording() finalises the recording and emits the record-end event with the complete audio as a Blob.

Pausing and resuming

record.pauseRecording()   // pauses the MediaRecorder and the live waveform
record.resumeRecording()  // resumes both

State checks

record.isRecording()  // true while actively capturing (not paused)
record.isPaused()     // true while paused

Handling the recorded audio

record.on('record-end', (blob) => {
  // blob is a Blob — create an object URL to download or play it
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'recording.' + blob.type.split('/')[1]
  a.click()
})

If renderRecordedAudio is true (the default), the plugin automatically loads the blob into the waveform when recording ends, so users can play it back straight away.

Choosing a microphone

Call the static helper to enumerate audio input devices, then pass deviceId to startRecording():

// Note: device labels are only populated after the user has granted
// microphone permission (e.g. after the first startRecording() call).
const devices = await RecordPlugin.getAvailableAudioDevices()

// devices is an array of MediaDeviceInfo objects
const select = document.querySelector('#mic-select')
devices.forEach((device) => {
  const option = document.createElement('option')
  option.value = device.deviceId
  option.text = device.label || device.deviceId
  select.appendChild(option)
})

// Pass the chosen deviceId when starting
select.addEventListener('change', async () => {
  await record.startRecording({ deviceId: select.value })
})

startRecording() accepts any MediaTrackConstraintsdeviceId is the most common one, but you can also pass echoCancellation, noiseSuppression, sampleRate, etc.


Live waveform

The plugin offers two modes for displaying audio while recording. Both are disabled by default; enable exactly one.

Scrolling waveform

RecordPlugin.create({
  scrollingWaveform: true,
  scrollingWaveformWindow: 5,  // show the last 5 seconds
})

The waveform acts like a moving ticker tape — the oldest data scrolls off the left edge and new data arrives on the right. The window is fixed; the waveform does not grow. This is the simplest live-preview mode.

Continuous waveform

RecordPlugin.create({
  continuousWaveform: true,
  continuousWaveformDuration: 120,  // pre-allocate 2 minutes
})

The waveform grows from left to right as audio is recorded, similar to how a voice memo app displays input. A cursor follows the recording head. If continuousWaveformDuration is omitted the plugin sizes the buffer to match the container’s pixel width.

Waveform flicker during recording. Without scrollingWaveform or continuousWaveform enabled, the plugin re-renders raw time-domain data on every frame (100 fps), which causes noticeable flicker. Enable one of the two live-waveform modes to eliminate this: scrollingWaveform normalises amplitude and uses peak values for stable rendering; continuousWaveform accumulates data so only the rightmost column changes per frame.

Only enable one live mode at a time. Enabling both scrollingWaveform and continuousWaveform together is not a documented configuration and produces undefined rendering behaviour.


Output format

The recording is delivered as a Blob whose MIME type is determined by mimeType. If you don’t set mimeType, the plugin tries audio/webm, audio/wav, audio/mpeg, audio/mp4, and audio/mp3 in order, picking the first one the browser supports.

RecordPlugin.create({
  mimeType: 'audio/webm',
  audioBitsPerSecond: 128000,
})

Converting to WAV. Most browsers produce audio/webm (with Opus codec) rather than WAV, even if you request audio/wav. True PCM WAV output requires post-processing: decode the blob with the Web Audio API (AudioContext.decodeAudioData), then re-encode the raw AudioBuffer into a WAV container using a library such as audiobuffer-to-wav or lamejs. There is no built-in WAV encoder in the plugin.


Events

Subscribe on the record plugin instance:

Event Payload When
record-start [] Recording begins (after startRecording() resolves).
record-pause blob: Blob Recording is paused. The blob contains audio captured so far.
record-resume [] Recording resumes after a pause.
record-end blob: Blob Recording stops. The blob is the complete recording.
record-progress duration: number Fires continuously (~100 fps) with elapsed time in milliseconds.
record-data-available blob: Blob Fires each time MediaRecorder delivers a chunk (controlled by mediaRecorderTimeslice).
record.on('record-progress', (durationMs) => {
  const seconds = Math.floor(durationMs / 1000)
  const minutes = Math.floor(seconds / 60)
  timer.textContent = `${minutes}:${String(seconds % 60).padStart(2, '0')}`
})

record.on('record-end', (blob) => {
  console.log('Recorded', blob.size, 'bytes as', blob.type)
})

Common pitfalls

Microphone stays “in use” after stopping. Calling stopRecording() stops the MediaRecorder but does not automatically release the microphone stream. The browser’s recording indicator stays on. To fully release the mic, call record.stopMic() after stopping:

record.stopRecording()
record.stopMic()

Recording quality with simultaneous playback. Playing audio through the same page while recording a microphone will cause the output to bleed into the capture on devices without hardware echo cancellation. Pass { echoCancellation: true, noiseSuppression: true } to startRecording() and avoid playing audio during capture on mobile.

Mobile / iOS device capture. Safari on iOS requires a user gesture (button tap) to both open the AudioContext and start getUserMedia. Call startRecording() directly inside a click handler — do not await anything before it in a chain that started outside a gesture. Some iOS versions also restrict which MIME types are accepted; if recording fails silently, leave mimeType unset so the plugin auto-detects the best available format.

Recording stops after ~1 second in React. This is typically caused by a component re-render that destroys and re-creates the wavesurfer / plugin instance mid-recording. Create the wavesurfer instance inside a useRef or useEffect with an empty dependency array so it is created once and persists across renders. See Frameworks for integration patterns.