CoProducer Measure a mix
One continuous gold-to-teal waveform across gold-veined black marble, split by a thin dark gap with a magenta spark at the cut

Delivery · 16 October 2026 · 7 min

Why your gapless album gets gaps after MP3 or AAC

NoDAW Labs · Lacquer Vol. 05

Every MP3 and AAC encoder adds a little silence at the start of a file and pads the end out to a full frame, so two tracks that flowed perfectly as WAVs can play with a 5–30 ms hole or a tick between them unless the file carries gapless info and the player reads it. Render the album in one pass, split it at exact sample positions, deliver lossless, and count samples on anything you encode yourself.

Live record, DJ mix, ambient album, a concept project where track 4 pours into track 5 under a held chord. In the session it’s one long, beautiful sentence. Then you make MP3s for the press folder, or a client plays the AAC previews in a car, and right at the segue there’s a hiccup. A tiny hole, sometimes a tick. Nobody touched the transition. The files did.

Single-song releases don’t care about any of this. A few tens of milliseconds of silence before a single is inaudible. It only bites when one track is supposed to keep flowing into the next.

Where the hole comes from

Lossy codecs work in frames, and the encoder needs a run-up before the first one. So every MP3 and AAC file starts with a short stretch of priming (encoder delay) and ends with padding that fills out the last frame. The file is a little longer than your audio, on both ends.

There’s a fix built into the formats, as long as everyone plays along. LAME writes the exact delay and padding into a header in the first frame (the LAME/Info tag). MP4 files use an edit list (Apple’s encoders also write an iTunSMPB tag). Opus stores a pre-skip in its header. A player that reads these trims the priming and padding and plays your audio sample for sample. A player that doesn’t plays the silence, and two of those silences back to back are your gap.

What we measured

We rendered a synthetic 20-second segue at 44.1 kHz (a sustained pad chord, a low pulse, a little noise for air) and split it into two tracks at sample 441,123. That’s deliberately mid-note, at −12.4 dBFS, not on a zero crossing. Then we encoded both halves the common ways, decoded them back to back with ffmpeg the way a player would, and compared the join with the continuous original.

Delivery / playbackLead-inExtra at endTrack 2 arrives lateDip at the joinJoin null (mid-track)
24-bit WAV or FLAC, split from one render000none−128.7 dB (−128.5)
Each track bounced separately, 10 ms edge fades000−6.0 dB−10.2 dB (−184.6)
MP3 (LAME 320k), tag read000none−36.7 dB (−36.5)
Same MP3, player ignores the tag1,10514028.2 mssilence (−90 dB)—
MP3 written without the tag1,10514028.2 mssilence (−90 dB)—
AAC .m4a (ffmpeg aac 256k), edit list read02215.0 ms−23.1 dB−16.1 dB (−38.2)
Same .m4a, edit list ignored / raw .aac1,02422128.2 ms−29.4 dB−16.1 dB (−38.2)

Lead-in and extra are in samples. “Dip” is the quietest 2 ms right at the join compared with the same spot in the original. “Join null” re-aligns track 2 and nulls ±50 ms around the cut against the original, with the same measure from the middle of the track in brackets, so you can see whether the join is any worse than the codec already is.

What’s yours to control

For streaming releases you upload WAV or FLAC and the platform does its own encoding and gapless playback. You can’t control their decoder, but you control whether the files you hand them are continuous to the sample, and that’s the part that breaks most often. If your distributor’s form asks whether the album should play continuously or gapless, say yes.

Anything you encode yourself is all on you: press MP3s, a DJ pool upload, the player on your own site, previews for a client. Use an encoder that writes gapless info (LAME does by default) and check the result.

The workflow

  1. Do the transitions in one session and render once. Crossfades, reverb tails, room tone between live songs: all of it lives in the long render. Dither once on that render if you’re going to 16-bit, then split.
  2. Split at sample positions, not by bouncing regions. No auto-fades at the cut points, no per-track normalize, no tail trimming. The tracks should butt together and reassemble into the original exactly.
  3. For CD, author from the continuous file. Red Book audio is addressed in frames of 1/75 s (588 samples at 44.1 kHz), and track marks in a DDP land on those frames. If a plant insists on individual WAVs, put the splits on 588-sample boundaries so the authoring tool has nothing to pad.
  4. Encode with gapless metadata, then count samples. Every encoded track should decode to the same number of samples as its WAV.

Here’s steps 2 and 4 as a script. It cuts one album render at the sample positions you give it, writes 24-bit WAVs for delivery, and makes LAME-tagged MP3s for anything you hand out. The optional --cd flag snaps the splits to CD frames.

#!/usr/bin/env bash
# split_album.sh — cut one continuous album render into tracks at exact sample positions,
# write 24-bit WAVs for delivery plus gapless-tagged MP3s for anything you hand out yourself.
#   ./split_album.sh album_full.wav markers.txt [--cd]
# markers.txt: one track START per line, in samples (0 for track 1). --cd snaps starts to 588-sample CD frames.
set -euo pipefail
src=$1; markers=$2; cd_snap=${3:-}
sr=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 "$src")
total=$(ffprobe -v error -select_streams a:0 -count_packets -show_entries stream=duration_ts -of csv=p=0 "$src")
mapfile -t starts < <(grep -v '^\s*$' "$markers")
[[ $cd_snap == --cd ]] && for i in "${!starts[@]}"; do starts[$i]=$(( (starts[$i] + 294) / 588 * 588 )); done
starts+=("$total")
for (( i = 0; i < ${#starts[@]} - 1; i++ )); do
  a=${starts[$i]}; b=${starts[$((i + 1))]}; n=$(printf "%02d" $((i + 1)))
  ffmpeg -v error -y -i "$src" -af "atrim=start_sample=$a:end_sample=$b,asetpts=N/SR/TB" -c:a pcm_s24le "track$n.wav"
  ffmpeg -v error -y -i "track$n.wav" -c:a libmp3lame -b:a 320k "track$n.mp3"   # LAME/Xing tag carries delay + padding
  echo "track$n  samples $a-$b  ($(( b - a )) samples, $(awk "BEGIN{printf \"%.3f\", ($b-$a)/$sr}") s)"
done

On our segue, with markers at 0 and 441123:

track01  samples 0-441123  (441123 samples, 10.003 s)
track02  samples 441123-882000  (440877 samples, 9.997 s)

Then the check. Each encoded file should decode to exactly the sample count of its WAV:

#!/usr/bin/env bash
# count_samples.sh — does each encoded track decode to exactly as many samples as its WAV?
#   ./count_samples.sh track01 track02 ...   (expects trackNN.wav next to trackNN.mp3 / .m4a)
for t in "$@"; do
  wav=$(ffmpeg -v error -i "$t.wav" -f s16le -ac 1 - | wc -c)
  for enc in "$t".mp3 "$t".m4a; do
    [[ -f $enc ]] || continue
    dec=$(ffmpeg -v error -i "$enc" -f s16le -ac 1 - | wc -c)
    printf '%-14s %9d samples   wav %9d   extra %+d\n' "$enc" $((dec / 2)) $((wav / 2)) $(((dec - wav) / 2))
  done
done

From the split above (MP3 from the script, plus an ffmpeg AAC for comparison):

track01.mp3       441123 samples   wav    441123   extra +0
track01.m4a       441344 samples   wav    441123   extra +221
track02.mp3       440877 samples   wav    440877   extra +0
track02.m4a       441344 samples   wav    440877   extra +467

And the same MP3s written without the tag came back +1,245 and +1,491 samples longer. That’s the gap, measured in advance instead of discovered by a listener. +0 means the decoder knows where your audio starts and stops. Anything else is a gap waiting for a segue.

Screw tapes were continuous by design, and the rule carries over: whether the slowdowns come from ScrewAI or a turntable pass, blend the tape in one session, render it once and cut it after. Don’t bounce every song separately and hope the joins line up.

Why is there a gap between tracks on my album after converting to MP3?
MP3 encoders add priming silence at the start and padding at the end of every file. LAME records both in a header so players can trim them. If the tag is missing or the player ignores it, the silences play. In our test that was a 28.2 ms hole at 44.1 kHz.
Do I need to split album tracks at zero crossings?
Not for lossless delivery. Two WAVs cut from one render reassemble exactly, even mid-note. What matters is cutting at a single sample position with no fades, trims or per-track processing at the cut.
Do streaming platforms fix gapless playback for me?
They encode from your lossless files and handle playback themselves, so you can’t control their decoder. You do control whether the files you upload are continuous to the sample. A dip or fade baked into your WAVs survives every platform.
How do I check that an MP3 or AAC file is gapless?
Decode it and compare its sample count with the WAV it came from. A tagged LAME MP3 decoded by ffmpeg matched exactly in our test. An untagged one was 1,245 samples longer, and an ffmpeg AAC .m4a kept 221 samples of end padding.

A free one-pager

The checklist we run before every release.

Loudness, true peak, mono, tails, export settings and the metadata basics, on one page. Leave your email and it opens straight away, or read it here first. No email needed for that.