May 21, 2026
Why does looped audio have that annoying click? A technical dive into gapless audio on iOS
If you’ve ever used a white noise or rain app and heard a faint tick every 20 or 30 seconds, you’re not imagining it, and it’s not a bad recording. It’s the loop point, the exact moment the track starts over. Getting rid of it completely turned out to be a real engineering problem. It’s not just a matter of trimming the file in the right spot.
Where the click comes from
There are two separate culprits, and most apps only fix one of them.
The first is encoder padding. Compressed formats like AAC and MP3 don’t encode your audio one sample at a time. They work in fixed-size blocks, 1024 samples for AAC and 1152 for MP3. If your audio doesn’t divide evenly into those blocks, the encoder pads the start and end with a few milliseconds of silence to make the math work. You’d never see it in a waveform editor, but you can absolutely hear it when the track loops, because now there are a few silent samples sitting right on the seam.
The second is how the loop gets restarted. Even with a perfectly encoded file, how you tell the phone to loop it matters. The obvious way is to call play() again when the track ends, and that always leaves a gap, because there’s real lag between “the track finished” and “the next play() actually runs.” On iOS that gap is small, but it isn’t zero. And on a sound that’s supposed to run for eight hours next to your head, small but not zero is exactly what you end up noticing.
What fixes it
Two things, together.
First, strip the codec padding at the source. Trim to the right sample count before encoding, accounting for the encoder’s block size, so there’s no silent padding sitting on the seam to begin with.
Second, schedule the loop instead of restarting it. Rather than waiting for playback to end and firing off a new play(), the next buffer gets scheduled ahead of time against the audio hardware’s clock. The handoff happens at the sample level, with no gap, no matter how much lag there is in the app itself. On iOS that means working with AVAudioEngine and scheduling buffers directly, instead of trusting a player’s built-in “loop” switch.
None of this shows up the way a redesign does. You can’t take a screenshot of “there’s no click.” But it’s the difference between a sound that fades into the background of your night and one that keeps poking you every 20 seconds, which for a sleep app kind of defeats the whole point.
Why I cared this much
A rain loop that clicks every 30 seconds might not wake you up outright. But sleep is sensitive to exactly that kind of low-level, repeating interruption. The sound you put on specifically to stop noticing your surroundings ends up doing the opposite. A seamless loop isn’t a bit of polish I added at the end. It’s the product.
It’s also one of the reasons Repose’s audio is built as a native module instead of leaning entirely on a cross-platform audio library. Precise buffer scheduling on iOS needed direct access to AVAudioEngine, and the off-the-shelf React Native audio packages don’t give you that. More on that decision in a future post.