How to Turn an AI-Generated Track Into a Seamless Game Loop With FFmpeg, Godot, Unity, and Web Audio
The short version: a generated track almost never loops on its own. To fix that, generate a steady instrumental and work from a lossless copy. Cut a whole number of bars on a downbeat, then crossfade the audio that follows the loop end into the loop start. Check the seam numerically and by ear, ship it as OGG or WAV (not MP3), and give the engine an intro plus a loop point. The rest of this post walks through each step with copy-paste commands.
The question that keeps coming up
On r/SunoAI, a thread titled "Loop music ?" (April 2026) collected the usual answers. One reply sums up the problem well:
"I've been trying to do this with some of my instrumentals, and it has been a struggle. The closest success I've had is adding in the style prompt to have 'outro matches intro for consistent loop' - but even then, the tonal quality and fade in/out can still be jarring." (u/KinkyHuggingJerk, r/SunoAI)
Game developers ask the same thing about music they wrote themselves. A February 2026 r/gamedev thread asks "How do I make music loop seamlessly when it has a slight tail of reverb", and the highest-voted reply is the classic answer: "Use two clips. One that starts the track and one that loops."
Prompting alone won't get you there. You need a small, repeatable editing step, and that's what this post covers.
Why generated tracks don't loop
Four separate things go wrong, and each has its own fix:
- Song form. Generators write intros, builds, and endings. If you loop the whole file, the ending crashes into the intro.
- The cut is not on the grid. If the loop isn't an exact number of bars long, the beat stumbles every time it wraps.
- Discontinuity and tails. If you cut in the middle of a waveform, you get a click. If you cut off a reverb or cymbal tail, the sound collapses audibly at the seam.
- The file format adds silence. MP3 encoders and decoders add delay and padding. The LAME technical FAQ describes a 528-sample decoder delay in standard decoders, plus an encoder delay on top. As Wikipedia's gapless playback article puts it, MP3 itself "defines no way to record the amount of delay or padding." Some encoders store it in a LAME tag, but only players that read that tag can remove it. Ogg Vorbis and Opus define the start and end exactly, so the padding is trimmed when the file is decoded.
Step 1: Generate something that wants to loop
Ask for material that stays in one place musically:
- Instrumental only. Vocals have phrases with beginnings and endings, so they rarely survive looping.
- A fixed tempo stated as a number ("100 BPM"), plus "steady groove, no tempo changes."
- No dramatic ending. Leave out words like "finale," "build to a climax," or "fade out." If the tool has an exclusion field, put those there.
- Longer than you need. A 2–3 minute render gives you several candidate sections to choose from.
I use EasyMusic for this. In its Advanced Mode you choose Instrumental, write the Style brief, list unwanted elements under Exclude styles, and set a suggested duration between 10 and 360 seconds. The EasyMusic Style brief guide covers how to structure that text. Any generator with an instrumental mode will do, though, and so will a bounce from your own DAW project.
One licensing note before you ship anything: check the terms of the tool you used. Many generators, including ours, limit free-tier output to personal use and include commercial licensing only on paid plans. A game you sell is commercial use.
Step 2: Work from a lossless copy
Download WAV if your tool offers it. If you only have MP3, decode it once (ffmpeg -i track.mp3 track.wav) and do every edit on the WAV. Because you're cutting a region from inside the track, the padding at the very start of the MP3 doesn't affect your loop points. It only causes trouble if you ship an MP3 and expect the engine to loop it gaplessly.
If FFmpeg is new to you, HackerNoon's audio handling basics post covers trimming and conversion from the command line.
Step 3: Find the bar grid
In 4/4 time, one bar lasts 240 ÷ BPM seconds:
|
BPM |
1 bar |
16 bars |
16 bars at 48 kHz |
|---|---|---|---|
|
90 |
2.667 s |
42.667 s |
2,048,000 samples |
|
100 |
2.4 s |
38.4 s |
1,843,200 samples |
|
120 |
2.0 s |
32.0 s |
1,536,000 samples |
|
140 |
1.714 s |
27.429 s |
1,316,571 samples (rounded) |
Work in samples, not seconds, because rounding errors in seconds add up into audible drift. Check the file's sample rate with ffprobe -v error -show_entries stream=sample_rate -of csv=p=0 track.wav.
If you don't know the tempo or where the first downbeat falls, a beat tracker gets you close. This uses librosa's beat_track:
import librosa, numpy as np
y, sr = librosa.load("track.wav", sr=None, mono=True)
tempo, beats = librosa.beat.beat_track(y=y, sr=sr, units="time")
print("tempo estimate:", float(np.atleast_1d(tempo)[0]))
print("first beats (s):", np.round(beats[:8], 3))
Use the output as a starting point, not as the answer. The beat times are snapped to analysis frames, so they can be off by a few tens of milliseconds. Tracker tempos can also come out at half or double the real value. Confirm the downbeat by ear, and pick a loop start after the intro, where the arrangement has settled.
AI-generated audio isn't locked to a click track, so the tempo can drift slightly. For long loops, measure the loop length directly: find the downbeat 16 bars later and use the actual sample distance between the two downbeats, not the BPM formula.
Step 4: Cut and crossfade with FFmpeg
The approach: take exactly N bars starting at sample S. Then take the next X samples, the audio that naturally follows the loop end, including any reverb tail. Fade that tail out while fading the head of the loop in, using an equal-power curve. When the loop wraps, playback runs from the original audio just before the loop end into the original audio just after it, so the seam is continuous. The script also writes an intro-plus-loop file for engines that support a loop offset. It uses the atrim, afade, and amix filters.
#!/usr/bin/env bash
# usage: ./cut.sh input.wav START_SAMPLE LOOP_SAMPLES XFADE_SAMPLES
set -euo pipefail
IN="$1"; S="$2"; L="$3"; X="$4"
E=$((S + L)); T=$((E +