Troubleshooting
Common problems and how to fix them. For conceptual background see Core concepts.
The waveform doesn’t show but audio plays
Cause: The container has no height, or height: 'auto' is set but the parent element has no explicit height. Another common cause is initialising wavesurfer inside a display: none element or reading the DOM before the container is in the page.
Fix: Give the container a concrete height either in CSS or via the height option:
WaveSurfer.create({
container: '#waveform',
height: 128, // pixels
})
If you use height: 'auto', the parent element must have a fixed height:
#waveform { height: 128px; }
Wait for the ready event before calling any methods that depend on the waveform being drawn. Accessing dimensions immediately after create() is too early.
ws.on('ready', () => {
// safe to interact with the waveform here
})
If the container starts hidden (display: none), initialise wavesurfer after making it visible, or call ws.setOptions({}) (a no-op that triggers a re-render) after revealing it.
EncodingError: Unable to decode audio data
Cause: The browser’s built-in audio decoder does not support the codec or container you are loading. The default WebAudio backend decodes the entire file in-browser — if the format is not natively supported (e.g. certain .ogg variants on Safari, some .flac on older browsers), decoding fails.
Fix — option 1: Switch to the MediaElement backend, which delegates decoding to the <audio> element and supports whatever codecs the browser/OS has installed:
WaveSurfer.create({
container: '#waveform',
backend: 'MediaElement',
url: '/audio/track.ogg',
})
Fix — option 2: Re-encode the file to a universally supported format. MP3 and AAC work in every major browser:
ffmpeg -i input.ogg -c:a libmp3lame -q:a 2 output.mp3
RangeError: Invalid array length
Cause: Wavesurfer received malformed or empty peaks data, or the peaks array length is inconsistent with the supplied duration. An empty array or an array with NaN/Infinity values triggers this when the renderer tries to allocate typed arrays from the data.
Fix: Validate your peaks before passing them in. Each channel array must be non-empty and contain only finite numbers between -1 and 1. You must also pass a matching duration (in seconds):
ws.load('/audio/track.mp3', peaks, duration)
// or via options:
WaveSurfer.create({
url: '/audio/track.mp3',
peaks: [[0.1, 0.4, 0.7, ...]],
duration: 183.5,
})
See Pre-decoded peaks for the full data format and how to generate valid peaks on the server.
Safari / iOS quirks
No programmatic autoplay. Safari and all iOS browsers block audio from playing without a user gesture. The autoplay option and calling ws.play() on ready will both be silently ignored or throw an AbortError. You must call ws.play() inside a click/tap handler.
Playback stops on screen lock. This is a known iOS power-saving behaviour. There is no workaround from JavaScript; consider informing your users or using a native app wrapper for uninterrupted background audio.
.flac / .wav seeking issues. Safari’s handling of byte-range requests for FLAC and uncompressed WAV can be unreliable. If seeking produces silence or jumps to the wrong position, re-encode to AAC (.m4a) or use pre-decoded peaks with the MediaElement backend.
Cursor glitches. On some Safari versions the playback cursor can flicker or jump. This is a rendering timing issue. Setting cursorWidth: 0 removes the cursor entirely as a workaround; otherwise upgrading to a newer Safari usually helps.
Because all third-party browsers on iOS must use WebKit under the hood, these restrictions apply equally to Chrome, Firefox, and Edge on iPhone and iPad.
See Playback for more on handling play/pause failures.
Firefox rendering quirks
Cursor oscillation with autoCenter. On even-pixel-width waveform containers, Firefox can cause the playback cursor to oscillate by 1 px per frame when autoCenter: true is enabled. This is a sub-pixel rounding difference between Firefox and Chromium. Workaround: give the container an odd pixel width, or set autoCenter: false if you do not need centred playback:
WaveSurfer.create({
container: '#waveform',
autoCenter: false,
})
Canvas rendering quirks. Firefox may render waveform bars slightly differently from Chrome at certain devicePixelRatio values. If bars look blurry, try setting pixelRatio explicitly via the renderFunction option or reduce barGap to 0.
CORS errors loading remote audio
Cause: When wavesurfer fetches audio from a different origin (CDN, S3, external server), the browser blocks the request unless the server includes the correct CORS headers.
Fix: Configure the server or bucket to respond with Access-Control-Allow-Origin:
# Nginx example
add_header Access-Control-Allow-Origin "*";
# AWS S3 bucket CORS rule (JSON)
[{ "AllowedOrigins": ["*"], "AllowedMethods": ["GET"] }]
If you need to send credentials (cookies, Authorization headers) with the request, use fetchParams:
WaveSurfer.create({
url: 'https://cdn.example.com/audio.mp3',
fetchParams: { credentials: 'include' },
})
The MediaElement backend also requires CORS headers when using WebAudio analysis. If you only need playback (no waveform decoding), CORS headers on the stream are still required to avoid a tainted canvas error.
See Loading audio for fetchParams and other fetch options.
React: two waveforms appear / plugins break on init
Cause: React 18 Strict Mode intentionally mounts components twice in development to surface side effects. If you call WaveSurfer.create() inside a useEffect without a proper cleanup, two wavesurfer instances are created — the first one is orphaned but still active, causing double rendering and plugin failures.
Fix: Return a cleanup function from useEffect that destroys the instance:
useEffect(() => {
const ws = WaveSurfer.create({
container: containerRef.current,
url: '/audio/track.mp3',
})
return () => ws.destroy()
}, [])
See Frameworks for a full React integration example and the useWavesurfer hook.
Autoplay blocked / AbortError: play() request was interrupted
Cause 1 — browser autoplay policy. Browsers block audio from auto-playing before the user has interacted with the page. Setting autoplay: true or calling ws.play() on the ready event will throw or silently fail.
Fix: Always call ws.play() from within a user-gesture event handler (click, keydown, etc.):
document.querySelector('#play-btn').addEventListener('click', () => {
ws.play()
})
If you want to attempt autoplay and handle the failure gracefully:
ws.on('ready', async () => {
try {
await ws.play()
} catch (err) {
// AbortError or NotAllowedError — show a play button instead
showPlayButton()
}
})
Cause 2 — calling play() then pause() back-to-back. play() is asynchronous. Calling pause() before the play promise resolves interrupts it and produces an AbortError.
Fix: Always await the play call before pausing:
await ws.play()
ws.pause()
// or simply:
ws.playPause()
See Playback for a full discussion of async playback methods.
Generating waveform data on the server
Drawing a waveform requires peak amplitude data. By default wavesurfer decodes the audio in the browser (slow for long files). Pre-generating peaks on the server lets the waveform render instantly.
Using audiowaveform
The audiowaveform CLI from the BBC is the most widely used tool. Install it via your package manager or download a binary from the releases page.
Generate JSON peak data from an MP3 file at 20 points per second with 8-bit precision:
audiowaveform -i long_clip.mp3 -o long_clip.json --pixels-per-second 20 --bits 8
To generate separate waveforms for each audio channel, add --split-channels:
audiowaveform -i long_clip.mp3 -o long_clip.json --pixels-per-second 20 --bits 8 --split-channels
Normalizing peaks
audiowaveform outputs raw amplitude integers. Wavesurfer expects values between -1 and 1.
Option A — client-side normalization (easiest): enable the normalize option and wavesurfer will scale the data on every load:
WaveSurfer.create({
container: '#waveform',
normalize: true,
})
Option B — server-side normalization (better for large files): pre-normalize the JSON once at generation time using this Python script:
python scale-json.py long_clip.json
import sys
import json
def scale_json(filename):
with open(filename, "r") as f:
file_content = f.read()
json_content = json.loads(file_content)
data = json_content["data"]
channels = json_content["channels"]
# number of decimals to use when rounding the peak value
digits = 2
max_val = float(max(data))
new_data = []
for x in data:
new_data.append(round(x / max_val, digits))
# audiowaveform generates interleaved peak data with --split-channels, so deinterleave it
if channels > 1:
deinterleaved_data = deinterleave(new_data, channels)
json_content["data"] = deinterleaved_data
else:
json_content["data"] = new_data
file_content = json.dumps(json_content, separators=(',', ':'))
with open(filename, "w") as f:
f.write(file_content)
def deinterleave(data, channelCount):
# separate the values for each channel and min/max value pair
deinterleaved = [data[idx::channelCount * 2] for idx in range(channelCount * 2)]
new_data = []
# combine each min and max value back into one array per channel
for ch in range(channelCount):
idx1 = 2 * ch
idx2 = 2 * ch + 1
ch_data = [None] * (len(deinterleaved[idx1]) + len(deinterleaved[idx2]))
ch_data[::2] = deinterleaved[idx1]
ch_data[1::2] = deinterleaved[idx2]
new_data.append(ch_data)
return new_data
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python scale_json.py file.json")
exit()
filename = sys.argv[1]
scale_json(filename)
Loading the peaks in wavesurfer
fetch('/audio/long_clip.json')
.then(response => {
if (!response.ok) throw new Error('HTTP error ' + response.status)
return response.json()
})
.then(peaks => {
console.log('loaded peaks, sample_rate:', peaks.sample_rate)
ws.load('/audio/long_clip.mp3', peaks.data)
})
.catch(err => console.error('error loading peaks', err))
See Pre-decoded peaks for a full reference on the peaks data format and integration options.
Can the audio start playing before the waveform is drawn?
Yes. Use backend: 'MediaElement' — audio playback starts immediately while the waveform decoding happens in the background. A thin placeholder line is shown until the full file has been downloaded and decoded.
WaveSurfer.create({
container: '#waveform',
backend: 'MediaElement',
url: '/audio/track.mp3',
})
See the audio element example for a live demo.
Can the waveform draw progressively as the file loads?
No. The Web Audio API requires the complete audio file before it can decode and generate peak data. There is no streaming decode path in the browser.
The workaround is to supply pre-decoded peaks so that the waveform renders immediately from server-generated data — no download or decode step needed. See the “Pre-recorded Peaks” section of the audio element example and the full Pre-decoded peaks guide.