technique

Seamless music loops

How to cut a generated track into a loop that plays forever without a click, a hiccup, or a tempo jump.

Before this

This page assumes you are comfortable with:

Why you need this

A menu or a match can last ten minutes, and nobody generates ten minutes of music for it. The game plays one stretch of music over and over instead. A music model writes a piece with a beginning and an end, not a circle, so the raw clip almost never repeats cleanly. This page is stage 4 of the pipeline: turning a good take into a loop the player never notices.

The idea

A loop has two ends that must meet. When playback reaches the loop end, it jumps back to the loop start and continues. Three things can go wrong at that jump, and each has its own fix.

1. The beat stumbles. If the loop is not a whole number of bars long, every pass shifts the beat. Fix: put both loop points on bar lines. A bar is one measure; in 4/4 time it holds 4 beats. At a tempo of BB beats per minute (BPM):

bar length (s)=4×60B\text{bar length (s)} = 4 \times \frac{60}{B}

At 120 BPM, one beat is 60/120=0.560/120 = 0.5 s and one bar is 2 s.

2. It clicks. Audio is stored as samples: measurements of the wave taken at a fixed rate, here 48 kHz, meaning 48,000 samples per second. If the last sample before the jump is 0.4 and the first sample after it is -0.3, the speaker is told to move instantly from one position to another. That instant jump is a click. Fix: make the two sides match, either by moving each cut to a zero crossing (a point where the wave passes through 0) or by crossfading: over a short stretch, fading the end out while fading matching material in.

3. The ending is chopped off. A note played just before the loop end keeps ringing, and any reverb (the echo of the room) keeps sounding after it. A hard cut silences them, and the loop sounds like someone hit pause. Fix: carry that tail across the seam. A crossfade with audio from before the loop start does this, because in a well-chosen take that audio already contains the tail of a similar bar.

Why a raw take rarely loops. Models write intros, build-ups, and endings. Their tempo can drift by a few milliseconds per bar. And the last bar of a generated clip is usually a fade or a final chord, not the bar that leads back to the first. So the loop is cut from the middle of a longer take, where the music is steady, and the earlier part can serve as an intro: played once, before the loop begins.

Finding the bar lines. If the model honored the tempo you asked for, bar lines sit at whole multiples of the bar length from the first downbeat (the first beat of the first bar). Find the first downbeat by ear and by eye in an editor's waveform view, then count forward. Check the count against a later downbeat: if the 9th bar line lands a few milliseconds off, the tempo drifted, and you place the loop points by ear on the actual downbeats instead.

The demo above loops a two-bar phrase in 4/4 at 120 BPM (4 s long). The picture zooms in on 30 ms either side of the seam. Move the loop end off the bar line and the beat stumbles every time around. At 0 ms of crossfade the ringing last note is cut off and the wave jumps: the readout reports the size of the jump, and you hear it as a click. Raise the crossfade and the last stretch of the loop fades out while the audio just before the loop start fades in, so the jump lands on matching material.

Worked example

This take is an illustration, not a record of a real run. The Lumen Clash lobby loop is take t03, generated with ACE-Step 1.5 (the sft variant), seed 4129, asking for 48 s (24 bars) at 120 BPM in 4/4. The take is at 48 kHz. Listening, the first 2 bars are an intro and the music is steady from bar 3 onward; the last bars wind down. The 8 steady bars from bar 3 make the loop.

Loop points in samples. One bar is 2 s, which is 2×48,000=96,0002 \times 48{,}000 = 96{,}000 samples.

Point Bars from start Seconds Samples
Intro start 0 0 s 0
Loop start (bar 3 downbeat) 2 4 s 192,000
Loop end (bar 11 downbeat) 10 20 s 960,000
Loop length 8 16 s 768,000

Write loop points in samples, not seconds. A sample count is exact; "4.0 s" might be rounded by a tool into a sample or two either way. Tempos that do not divide evenly make this matter: at 128 BPM and 44.1 kHz one beat is 20,671.875 samples, which is not a whole number, but 32 beats (8 bars) is exactly 661,500 samples. Round once, for the whole loop, never per beat.

Crossfade length in samples. A 20 ms crossfade at 48 kHz is 0.020×48,000=9600.020 \times 48{,}000 = 960 samples. The last 960 samples of the loop (samples 959,040 to 959,999) are blended with the 960 samples just before the loop start (samples 191,040 to 191,999). An equal-power crossfade, which keeps the perceived level steady through the blend, uses a cosine curve for the part fading out and a sine curve for the part fading in:

Position in fade Sample Gain of loop tail Gain of pre-start audio
0% 0 1.000 0.000
25% 240 0.924 0.383
50% 480 0.707 0.707
75% 720 0.383 0.924
100% 960 0.000 1.000

By the last sample of the fade, the output is entirely the audio that comes right before sample 192,000. When playback jumps to sample 192,000, it continues from the very next sample of the original take, so there is no jump at all.

Playing it with an intro. In the browser, Web Audio's buffer source can play from the start, then loop between two points. This sample builds a synthetic 22 s buffer (a plucked note on every beat) so it runs as pasted:

const ctx = new AudioContext({ sampleRate: 48000 });
const SR = ctx.sampleRate, BEAT = 0.5;              // 120 BPM
const buf = ctx.createBuffer(1, SR * 22, SR);       // 4 s intro, 16 s loop, 2 s spare
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
  const t = i / SR;
  d[i] = 0.3 * Math.sin(2 * Math.PI * 220 * t) * Math.exp(-(t % BEAT) * 6);
}
const src = ctx.createBufferSource();
src.buffer = buf;
src.loop = true;
src.loopStart = 192000 / SR;    // bar 3 downbeat
src.loopEnd = 960000 / SR;      // bar 11 downbeat
src.connect(ctx.destination);
ctx.resume().then(() => src.start());   // intro once, then bars 3 to 10 forever

Testing. Play the loop at least ten times in a row in any audio editor with a loop playback mode, then once more listening only to the seam. A loop that is 10 ms short loses 10 ms every pass: after ten passes, anything the game times to the music is 100 ms off.

In a game's audio pipeline

Looping comes right after curating takes picks a keeper, and before Loudness for games sets its level. Keep the loop points, the crossfade length, and the seed with the file; Licensing and provenance puts them in the provenance record. When a loop is exported, check it again: Exporting game audio explains why some compressed formats add silence that breaks a perfect loop. Stingers and transitions uses the same bar arithmetic to cut away from the loop cleanly.

Common mistakes

  • Loop points off the bar line. Symptom: the groove limps once per pass, like a skipped step.
  • Cut with no crossfade and no zero crossing. Symptom: a tick or pop at the same point every pass, loudest on a sustained bass note.
  • Crossfade too long. Symptom: a soft "smear" or doubled drum hit at the seam. Past roughly a beat, two different drum hits overlap. Keep it short enough that only tails overlap.
  • Hard cut through a reverb tail. Symptom: the room suddenly goes dry at the seam, as if the music stopped and restarted.
  • Looping the model's ending. Symptom: a final-chord feeling, a short silence, then the music starts again. Cut from the steady middle.
  • Trusting the requested tempo. Symptom: the loop is correct for one pass but layers or effects synced to the beat drift. Check bar lines against the audio.

Cost

Generation time is unchanged; the cost is the maker's editing and listening time. Finding loop points is a few minutes per track when the tempo is steady, longer when it drifts. A crossfade costs one multiply-add per crossfade sample, so it is trivial at playback or at export. File size: the shipped file is much shorter than the take (the 4 s intro plus the 16 s loop, 20 s of a 48 s take here). Generating a longer take costs more model time but gives you more steady bars to choose from, which is often the faster route overall.

Going further

  • Seamless loops in a game engine: most engines and Web Audio take loop start and end points, so one file can hold intro and loop.
  • Equal-power versus linear crossfades: try both in the demo's spirit and listen for the dip in the middle of a linear one.
  • Variations and audio-to-audio, for regenerating a bar that spoils an otherwise good loop.
  • Music that changes with the game: horizontal re-sequencing (jumping between loops) and vertical layering (adding and removing stems on one loop).

Leads to

Back to Local text-to-audio for games