Why are there gaps between joined MP3 files, and how do you remove them?
Short answer: the gap is stored inside each MP3 file, not added by the joiner. An MP3 encoder pads the start and the end of every file to whole frames, and when the parts are played one after another, that padding becomes a short silence at each seam. Whether you hear it depends on one detail: whether the files carry a LAME tag that tells the decoder how much padding to strip.
- Parts with a LAME tag (written by LAME and by FFmpeg's libmp3lame): 0 ms at every seam in our test. Three 10-second parts joined back into the exact 30.000 s original.
- Parts without the tag (including MP3s exported by this site): 31.0 ms of silence at every seam and 25.1 ms at the start, so the same three parts came out 30.093 s long.
- WAV parts: 0 ms. Nothing is padded.
- Joining the bytes without re-encoding does not avoid it: 31.0 ms per seam, with or without the tag.
To remove the gap from untagged parts, open each one in the audio cutter, type 0.025 as the start and the clip length minus 0.006 s as the end, export WAV, then join the WAVs in the audio joiner. Done that way, our parts landed within 2 samples of the original with 0 ms at each seam.
Where the gap comes from
An MP3 stores audio in frames of 1,152 samples (ISO/IEC 11172-3), which is 26.1 ms at 44.1 kHz. The encoder cannot write a part of a frame, so it does two things to every file it makes:
- Encoder delay. The encoder's filters push the audio 576 samples later, so the first 576 samples of the first frame hold nothing. The decoder's own filters add 529 more, which is why decoded untagged MP3s start 1,105 samples (25.1 ms) late. We measured that figure in Can you trim an MP3 to the exact millisecond?.
- Padding. The last frame is filled up with silence to reach a whole 1,152 samples.
The LAME encoder records both amounts in a small header at the start of the file, the LAME tag: 12 bits for the delay and 12 bits for the padding. The fields are written by the function PutLameVBR in libmp3lame/VbrTag.c (LAME source, read 2026-09-30). FFmpeg's MP3 reader looks for that tag and, when it finds the encoder name LAME, Lavf or Lavc, splits the 24 bits into a start pad and an end pad and drops those samples; see mp3_parse_info_tag in libavformat/mp3dec.c (FFmpeg 4.4.2, read 2026-09-30). Chromium decodes audio for web pages with FFmpeg (media/filters/audio_file_reader.cc, read 2026-09-30), so the joiner on this site, which asks the browser to decode each file, gets the same trimming for free. Without the tag, nobody knows how much to trim, and the padding stays in the audio.
A joiner that decodes the parts and writes them one after the other, which is what the audio joiner does, therefore reproduces whatever the decoder gave it: trimmed parts join without a gap, untrimmed parts join with the padding of one part followed by the delay of the next.
How we tested
- Music: 30 seconds (1:00 to 1:30) of Mozart's Symphony No. 40, first movement, played by the Musopen Symphony, public domain on Wikimedia Commons, converted to 44.1 kHz 16-bit stereo and split into three 10.000-second parts. This is the "one song cut into parts, then rejoined" case, and it lets us find each part in the joined file by cross-correlation with the original, to the sample.
- Tones: three 2.000-second sine tones (500, 1,000 and 1,500 Hz at -6 dBFS) that are loud from the first sample to the last, so silence at a seam can be counted directly. We counted samples where both the left and the right side stayed below -40 dBFS.
- Three encoders, all 128 kbps: lamejs, the encoder this site uses, which writes no tag; FFmpeg 4.4.2 libmp3lame, which writes the LAME tag; and the same FFmpeg encoder with the tag switched off (-write_xing 0).
- Joiner: this site's audio joiner in Chromium 153 through Playwright 1.63, crossfade 0, no fades, WAV export, plus one MP3 export at 128 kbps. Byte-joined files were made with cat and with FFmpeg's concat demuxer in copy mode, then decoded with FFmpeg 4.4.2 and, separately, with Chromium's decodeAudioData.
Results: three parts of one song, joined in the browser
| Parts | Length shown per part | Silence before part 1 | Gap at seam 1 | Gap at seam 2 | Joined length |
|---|---|---|---|---|---|
| WAV | 10.000 s | 0 | 0 | 0 | 30.000 s |
| MP3 with LAME tag (FFmpeg libmp3lame) | 10.000 s | 0 | 0 | 0 | 30.000 s |
| MP3 without tag (this site's encoder) | 10.031 s | 25.1 ms (1,105 samples) | 31.0 ms (1,368 samples) | 31.0 ms (1,368 samples) | 30.093 s |
| MP3 without tag (FFmpeg, -write_xing 0) | 10.031 s | 25.1 ms (1,105 samples) | 31.0 ms (1,368 samples) | 31.0 ms (1,368 samples) | 30.093 s |
The 1,368 samples per seam are exactly the frame arithmetic: 10 s is 441,000 samples, plus the 576-sample encoder delay is 441,576, which needs 384 frames, or 442,368 samples. The 792 samples of padding at the end of one part, plus the 576-sample delay at the start of the next, make 1,368. The joiner showed each untagged part as 10.031 s (442,368 samples) because that is what the browser decoded; the tagged parts showed 10.000 s.
With the tone parts, the silence a listener would get at each seam measured 24 to 38 ms at the -40 dBFS threshold, depending on how the encoder faded the frame edges. The joined tagged tones and the joined WAV tones had no seam silence at all.
The two untagged rows are identical because the tag is the only difference. The tag costs 417 bytes per file: our tagged 10-second parts were 160,958 bytes, the untagged FFmpeg parts 160,541 bytes.
Joining the bytes without re-encoding does not remove the gap
A common suggestion is to join MP3s by simply appending the files, or with FFmpeg's concat demuxer in copy mode, so nothing is re-encoded. That keeps the quality, but the padding stays. We decoded the byte-joined files with FFmpeg and with Chromium:
| Byte-joined file | Silence at start | Gap per seam | Decoded length |
|---|---|---|---|
| Three tagged parts, cat | 0 | 1,368 samples (31.0 ms) | 1,325,999 samples |
| Three tagged parts, FFmpeg concat copy | 0 | 1,368 samples (31.0 ms) | 1,325,736 samples |
| Three untagged parts (this site's encoder), cat | 1,105 samples | 1,368 samples (31.0 ms) | 1,327,104 samples |
| Three untagged FFmpeg parts, cat | 1,105 samples | 216 samples, but one whole frame missing per seam | 1,324,800 samples |
With tagged parts, the tag of the first file strips the silence at the start, but the tags of the second and third file now sit in the middle of the stream, where the decoder ignores them, so the inner seams keep the full 1,368 samples. FFmpeg's concat copy rewrote the header and gave the same seams. The last row is a warning: the untagged FFmpeg parts each start with a small ID3 text tag, and when those tags land between frames, both decoders dropped one whole frame (1,152 samples) at each seam. The gap looks smaller only because 26 ms of the file went missing. We did not check which samples were lost.
Removing the gap with the cutter, then the joiner
The joiner has no per-clip trim, but the audio cutter cuts to the millisecond, so we used it to strip the padding from the untagged parts made with this site's own encoder. Each 10.031 s part was opened in the cutter with start 0.025 and end 10.025 typed in, then exported as WAV.
| Step | Result |
|---|---|
| Cutter, untagged 10.031 s part, start 0.025, end 10.025, WAV export | 441,000 samples, music 2 samples (0.05 ms) later than in the original |
| Joiner, the three cut WAVs, WAV export | 30.000 s, 0 samples at each seam, every part 2 samples late |
| Joiner, the three cut WAVs, MP3 128 kbps export | 0 samples at each seam; 1,107 samples of silence at the start of the new file |
| Joiner, the three untagged parts, crossfade 0.1 s, WAV export | no silence below -40 dBFS at the seams; joined tones 5.913 s instead of 6.113 s |
Why 0.025 and 10.025: the decoded part starts 1,105 samples late (0.02506 s) and ends with 263 samples of padding (0.006 s), so 10.031 minus 0.006 is 10.025. The cutter's fields take whole milliseconds, which is where the 2-sample offset comes from; it is the same for every part, so the seams are still exact. For a 2-second part, the ends were 0.025 and 2.025.
A crossfade hides the silence rather than removing it: the 0.1 s overlap covers the padding, but it also overlaps 100 ms of music at every seam and shortens the result by 0.1 s per seam. Use it when the parts are different songs and a blend is what you want, not when the parts are one continuous recording.
What to do
- If you still have the WAV or the original file, join that, or cut the parts fresh and export them as WAV. WAV has no padding.
- If you only have MP3 parts, add them to the audio joiner and look at the length shown next to each part. A part that shows a few hundredths of a second more than it should, such as 10.031 s for a 10-second clip, is untagged and will leave a gap. Preview the seam to be sure.
- For untagged parts, open each one in the audio cutter, type 0.025 as the start and the shown length minus 0.006 as the end, export WAV, then join the WAVs. Export the joined file as MP3 only once, at the end.
- Do not use this site's MP3 export for parts you plan to rejoin. It writes no LAME tag, so every part carries 25.1 ms at the start and padding at the end. Export parts as WAV instead; this is what we measured above.
Questions
Which MP3 files carry the LAME tag?
Files made by LAME itself and by FFmpeg's libmp3lame do, and those joined with no gap in our test. Files made by this site's encoder do not. We did not test files from other encoders, so we cannot say what a given music store or ripping program writes.
Is the gap the same at other bitrates or lengths?
We measured 128 kbps and 2-second and 10-second parts only. The frame arithmetic says the padding depends on the part length, not the bitrate: it is whatever fills the last 1,152-sample frame, so between 0 and 26.1 ms, plus the fixed 576-sample delay. We did not verify other bitrates.
Does exporting the joined file as MP3 add a gap of its own?
Not at the seams. The MP3 export of our fixed join had 0 samples at each seam and 1,107 samples of silence at the very start, which is the encoder delay of the new file. The seams are inside the file, so they are untouched.