Why Your Music Has Gaps
Media Source Extensions (MSE) were originally built for Dynamic Adaptive Streaming over HTTP (DASH) video players. But the same buffering and playback controls can solve a distinctly audio problem: gapless playback. MP3 and AAC codecs work on blocks of samples, not continuous streams. When an encoder processes a file, it adds padding—sections of silence—at the beginning and end of the audio. Play files back-to-back and these padding regions translate to audible glitches between tracks.
The effect is easy to hear. This demo takes the first thirty seconds of the Sintel soundtrack, splits it into five separately encoded MP3 files, and reassembles them with MSE. Red lines mark where padding introduces glitches:
The same files can play seamlessly if you use MSE to strip the padding. On Chrome 38+, the green lines in this second demo show where gaps have been removed:
There are several ways to prepare gapless content, but this article focuses on the common case: files that were encoded independently, with no special handling for the audio before or after them.
Setting Up a MediaSource
MSE extends the standard media elements. You create a MediaSource instance, generate an object URL from it, and assign that URL to an audio element's src attribute:
Once the MediaSource connects, it fires a sourceopen event. At that point you can create a SourceBuffer. In the example, we create an audio/mpeg buffer that parses MP3 segments. Other buffer types are available in the byte stream format registry.
Where the Silence Comes From
If you inspect the end of a file like sintel_0.mp3, you'll see hundreds of zero samples. The waveform below shows the last 3000 samples, averaged across channels:
These zeros are compression artifacts. The LAME encoder, for instance, added exactly 576 padding samples to the end of this file. The beginning of the next file (sintel_1.mp3) contains the same amount of front padding:
These silent regions at file boundaries cause the audible glitches. To fix them, we remove the padding via MSE's append window and timestamp offset features, as shown in this modified onAudioLoaded() handler:
With those windows applied, the waveform shows a clean join. The red section (end-of-file padding in sintel_0.mp3) and blue section (front padding in sintel_1.mp3) are gone:
Works With Any Container or Codec
The append-window and timestamp-offset logic makes no assumptions about container or codec. The same techniques apply to fragmented MP4 with AAC audio. Here's the same demo using DASH-ready fragmented MP4 instead of MP3:
The complete code powering these demos is in gapless.js.
Appendix: Creating Gapless Content
Producing gapless media requires deliberate tooling choices. Start with the lossless FLAC soundtrack for Sintel. You'll need FFmpeg, MP4Box, LAME, and afconvert (on macOS).
First, extract the first 31.5 seconds from the 1-Snow_Fight.flac track and add a 2.5-second fade out beginning at 28 seconds:
Then split the result into five six-second WAV files, which virtually every encoder can ingest. That yields sintel_0.wav through sintel_4.wav:
For MP3, LAME offers a --nogap batch mode that avoids cross-file padding. But to demonstrate gap removal, encode each WAV separately with standard high-quality VBR settings:
For fragmented MP4, Apple's "mastered for iTunes" guidelines recommend converting WAV to intermediate CAF files, then encoding to AAC in an MP4 container:
The resulting M4A files must be fragmented before MSE will accept them. A one-second fragment size works well. MP4Box writes out sintel_#_dashinit.mp4 plus a manifest you can discard:
Appendix: Parsing Gapless Metadata
There is no single standard for storing gapless metadata. Two formats dominate: iTunes-style text and the Xing header used by open-source MP3 encoders.
iTunes (and afconvert) writes a compact ASCII block inside an MP3 ID3 tag or an MP4 metadata atom:
Ignore the leading 0000000 token. The next three values are front padding samples, end padding samples, and the total non-padding sample count. Divide each by the sample rate to get durations:
Most open-source MP3 encoders instead embed metadata in a Xing header inside a silent MPEG frame. Decoders that don't recognize the tag simply play the silence. The header may not always be present, and its fields are optional, so production code should verify availability.
The 32-bit total frame count appears exactly four bytes after the Xing or Info tag identifier. Multiply that by samples per frame for the total sample count:
Padding values appear under a nested LAME or Lavf tag, 17 bytes into that header. Three bytes encode front and end padding in two 12-bit fields:
Those two parsing paths cover the vast majority of gapless content. Edge cases exist, so test thoroughly before using similar code in production.
Managing SourceBuffer Memory
Memory held by SourceBuffer instances is subject to platform-specific garbage collection, which triggers based on content type and the current playback position. In Chrome, the browser first reclaims memory from buffers that have already been played. However, if memory usage exceeds platform limits, Chrome will also purge data from unplayed portions of the timeline.
When playback encounters a gap caused by reclaimed memory, the result is either a brief glitch (if the gap is small) or a full stall (if the gap is large). Both outcomes degrade the user experience, so it is wise to avoid appending excessively large chunks of data at once. Instead, actively remove ranges from the media timeline that are no longer needed.
To delete buffered ranges, call remove() on the relevant SourceBuffer, passing a [start, end] range in seconds. Like appendBuffer(), each remove() operation fires an updateend event upon completion, and you should wait for that event before issuing subsequent appends or removals.
On desktop Chrome, you can typically keep around 12 megabytes of audio and 150 megabytes of video buffered in memory at once. These figures are not stable across browsers or platforms and almost certainly do not reflect limits on mobile devices, so do not design your logic around them.
Note that garbage collection only applies to data appended to SourceBuffer instances. There is no limit on how much data you retain in JavaScript variables, and you can always re-append the same data to the same position if needed.



