Skip to content

MiniMax Music 3 Seamless Loop in ComfyUI: Make Loopable Background Music (Free Workflow and Node)

17–26 minutes
Winking woman in a burnt orange blazer in a colourful podcast studio holding a card that reads "MINIMAX", with the text "Endless AI music loops"

MiniMax Music 3 writes full songs on your own GPU, but it has no “loop” switch. Every track has a start and an end, and if you set it to repeat in a game, a stream or a video, you hear the jump.

I wanted background music that repeats forever without anyone noticing. So I built a small ComfyUI node that finds the best bar-aligned loop inside a Music 3 track and stitches the end back into the start.

This guide gives you the whole thing: the model files, the prompts that make loop-friendly music, the free workflow and node, my test results on an RTX 5080, and fixes for the problems I hit.

TL;DR

  • Music 3 has no loop mode. It makes songs with a beginning and an end, and the model card warns the tempo may not match your prompt exactly.
  • The fix: prompt for a steady instrumental with no intro or outro, render 75 s, then run my Seamless Audio Loop (FJ) node. It picks an exact 8-bar window and folds the next few milliseconds back over the start.
  • My results (RTX 5080 16 GB): lo-fi beat and ambient pad loops both measured a seam ratio of about 1.2 (1.0 = as smooth as the music itself) with no click at the wrap. Each 75 s track took 8 to 9 minutes on the int8 model.
  • Two gotchas: Music 3 drifted 2 to 3 BPM off the prompt, so leave bpm at 0 (auto). It also ended a 75 s request at 30.7 s until I fixed the latent length.
  • Use WAV, OGG or FLAC for real loops. MP3 adds a short silence at the start that breaks gapless repeat.

New to MiniMax’s models in ComfyUI? Start with our MiniMax H3 ComfyUI guide, the hub for every MiniMax workflow on this site.

Why Music 3 Tracks Don’t Loop on Their Own

MiniMax Music 3 is an open music model that MiniMax released in August 2026. ComfyUI supports it natively through the “MiniMax Music 3 Text to Music” template. It makes 32 kHz stereo songs up to about five minutes long from a structured caption and lyrics.

It was built to write songs, not loops. That causes three problems for looping:

  • Songs have shape. The model likes intros, builds and endings, so the last bar rarely sounds like the first.
  • Tempo is a suggestion. The model card says the generated tempo, key and structure “may not always match every requested detail exactly”. In my tests, 84 BPM came out near 86, and 70 came out near 72.7.
  • No edit modes. The card lists only non-streaming text-to-music. There is no continuation, repaint or loop option to lean on.

So looping has to happen after generation. The trick is to cut in the right place and hide the join.

How the Seamless Loop Works

My node, Seamless Audio Loop (FJ), takes any ComfyUI AUDIO and does four things:

  1. Measures the real tempo. It builds an onset envelope (where notes and drums hit) and finds the repeating period. You can type the BPM instead, but auto is safer with Music 3.
  2. Finds the best window. For a loop of N bars, it slides through the track and scores how closely the audio right after the end matches the audio right after the start, using 48 frequency bands.
  3. Lines up the waveforms. It nudges the length by up to 20 ms so the waves at the seam are in phase, which stops clicks and flams.
  4. Folds the overhang. It takes the few milliseconds that follow the loop in the original track and crossfades them over the loop’s first few milliseconds (equal-power). The last sample now flows into the first just as the music did originally.

Why not just crossfade the ends? A plain crossfade blends two unrelated moments and smears the beat. Folding the overhang mixes the start with the audio that really followed the end, so the rhythm stays on the grid.

It also prints a seam ratio: how big the spectral jump at the wrap is, compared with the jump at the same spot in the uncut track. About 1.0 means the seam is as smooth as the music itself.

I treat anything up to 1.5 as clean, and I still give each loop one listen on headphones.

Files, Folders and VRAM

These are the official ComfyUI files from Comfy-Org/MiniMax-Music-3. Pick one diffusion model, then grab the text encoder and VAE.

File What it is Size Folder
minimax_music3_dit_int8_convrot.safetensors Diffusion model, int8 (what I used) 2.33 GB ComfyUI/models/diffusion_models/
minimax_music3_dit_fp16.safetensors Diffusion model, fp16 (template default) 4.58 GB ComfyUI/models/diffusion_models/
minimax_music3_text_encoder_pruned_int8_convrot.safetensors Text encoder (the 8B planning LLM) 8.57 GB ComfyUI/models/text_encoders/
minimax_music3_dav.safetensors Audio VAE (decoder) 206.7 MB ComfyUI/models/vae/

The model card says full precision fits under 24 GB, and about 22 GB with automatic CPU offload. It adds that layer-by-layer streaming can fit 8 GB cards, though much slower.

On my 16 GB RTX 5080, the int8 model with the int8 encoder ran fine, and its output came out about the same as fp16 in my comparison. Need help with folders? See the ComfyUI model folders cheat sheet.

Licence: the weights use the MiniMax-Music3 Community License. According to Comfy’s tutorial, companies with under 20 million USD yearly revenue can use it commercially; bigger companies need a commercial licence from MiniMax.

Install the Loop Node

The node is one Python file that uses only numpy and torch, which ComfyUI already has. Nothing to pip install.

  1. Update ComfyUI. Music 3 needs a recent build; the template appears under Template Library, Audio. Our update guide covers every install type.
  2. Create the folder ComfyUI/custom_nodes/ComfyUI-FJ-SeamlessAudioLoop/.
  3. Save the code below as __init__.py inside that folder (use the download button).
  4. Restart ComfyUI. Double-click the canvas and search “Seamless Audio Loop”. It sits under audio/loop.

Show the node code (MIT)
"""ComfyUI-FJ-SeamlessAudioLoop (freakyjolly.com, MIT)

One node that turns a generated track (for example MiniMax Music 3) into a
bar-aligned, seamless loop:

  1. estimate the tempo (or use the BPM you prompted for),
  2. search for the N-bar window whose end sounds most like its start,
  3. nudge the length a few ms so the waveforms line up at the seam,
  4. fold the audio that naturally follows the loop back over its first
     few milliseconds (equal-power crossfade), so the last sample flows
     straight into the first.

No extra pip packages: numpy and torch only (both ship with ComfyUI).
"""
import numpy as np

HOP = 512
NFFT = 2048


def _stft_mag(x):
    win = np.hanning(NFFT).astype(np.float32)
    n = 1 + max(0, (len(x) - NFFT) // HOP)
    idx = np.arange(NFFT)[None, :] + HOP * np.arange(n)[:, None]
    frames = x[idx] * win
    return np.abs(np.fft.rfft(frames, axis=1)).astype(np.float32)  # [frames, bins]


def _bands(mag, n_bands=48):
    # log-spaced band energies, log compressed (a cheap mel stand-in)
    nb = mag.shape[1]
    edges = np.unique(np.geomspace(2, nb - 1, n_bands + 1).astype(int))
    out = np.stack([mag[:, a:b].mean(axis=1) for a, b in zip(edges[:-1], edges[1:])], axis=1)
    return np.log1p(out * 10.0)


def _onset_env(bands):
    flux = np.maximum(0.0, np.diff(bands, axis=0)).sum(axis=1)
    flux = np.concatenate([[0.0], flux])
    flux -= flux.mean()
    return flux / (flux.std() + 1e-9)


def _estimate_bpm(env, sr):
    fps = sr / HOP
    ac = np.correlate(env, env, mode="full")[len(env) - 1:]
    lags = np.arange(len(ac))
    lo, hi = int(fps * 60 / 200), int(fps * 60 / 60)
    seg = ac[lo:hi]
    best = lo + int(np.argmax(seg))
    # parabolic refine
    if 1 <= best < len(ac) - 1:
        a, b, c = ac[best - 1], ac[best], ac[best + 1]
        best = best + 0.5 * (a - c) / (a - 2 * b + c + 1e-9)
    bpm = 60.0 * fps / best
    while bpm < 70:
        bpm *= 2
    while bpm > 170:
        bpm /= 2
    return float(bpm)


def find_loop(x_stereo, sr, bpm=0.0, beats_per_bar=4, bars=8,
              search_start=4.0, search_end=0.0, window_sec=2.0):
    """x_stereo: float32 [channels, samples]. Returns dict with start, length, bpm, score."""
    mono = x_stereo.mean(axis=0).astype(np.float32)
    bands = _bands(_stft_mag(mono))
    env = _onset_env(bands)
    est = _estimate_bpm(env, sr)
    use_bpm = float(bpm) if bpm and bpm > 0 else est
    L = int(round(bars * beats_per_bar * 60.0 / use_bpm * sr))
    Lf = L / HOP
    W = max(8, int(window_sec * sr / HOP))
    total_f = bands.shape[0]
    s0 = int(search_start * sr / HOP)
    s1 = total_f - int(np.ceil(Lf)) - W - 2
    if search_end and search_end > 0:
        s1 = min(s1, int(search_end * sr / HOP))
    if s1 <= s0:
        raise ValueError(
            f"Track too short for {bars} bars at {use_bpm:.1f} BPM "
            f"({L / sr:.1f}s loop + {window_sec:.1f}s window after {search_start:.1f}s). "
            "Lower bars, lower search_start or render a longer track.")
    # normalise band frames for cosine similarity
    nb = bands / (np.linalg.norm(bands, axis=1, keepdims=True) + 1e-9)
    # 1) refine the loop length: the lag (within +-2.5 % of the nominal bar length)
    #    at which the track is most similar to itself over the whole search region
    lo_f = int(np.floor(Lf * 0.975)); hi_f = int(np.ceil(Lf * 1.025))
    region_end = min(total_f, s1 + W + hi_f)
    best_lag, best_sim = int(round(Lf)), -1e9
    for lag in range(lo_f, hi_f + 1):
        a = nb[s0:region_end - lag]
        b = nb[s0 + lag:region_end]
        if len(a) < W:
            continue
        sim = float((a * b).sum(axis=1).mean())
        if sim > best_sim:
            best_sim, best_lag = sim, lag
    if bpm and bpm > 0:
        # trust the prompted BPM unless the audio clearly disagrees by >1 frame
        if abs(best_lag - Lf) <= 1.0:
            best_lag = Lf
    L = int(round(best_lag * HOP))
    Lf = L / HOP
    # the refined length can be longer than the nominal one: re-check the search range
    s1 = min(s1, total_f - int(np.ceil(Lf)) - W - 2)
    if s1 <= s0:
        raise ValueError("Track too short for this loop length after tempo refinement. "
                         "Lower bars or search_start_sec, or render a longer track.")
    best = (-1e9, s0)
    for s in range(s0, s1):
        e = int(round(s + Lf))
        a = nb[s:s + W]
        b = nb[e:e + W]
        cos = float((a * b).sum(axis=1).mean())
        pa = nb[max(0, s - W // 2):s]
        pb = nb[max(0, e - W // 2):e]
        pre = float((pa * pb).sum(axis=1).mean()) if len(pa) and len(pa) == len(pb) else cos
        onset_bonus = 0.02 * max(0.0, float(env[s]))
        score = 0.6 * cos + 0.4 * pre + onset_bonus
        if score > best[0]:
            best = (score, s)
    score, sf = best
    start = sf * HOP
    # sample-accurate length nudge (max +-20 ms) by waveform cross-correlation at the seam
    span = int(0.02 * sr)
    ref_len = int(0.05 * sr)
    ref = mono[start:start + ref_len]
    best_d, best_c = 0, -1e9
    for d in range(-span, span + 1, 4):
        seg = mono[start + L + d:start + L + d + ref_len]
        if len(seg) < ref_len:
            continue
        c = float(np.dot(ref, seg) / (np.linalg.norm(ref) * np.linalg.norm(seg) + 1e-9))
        if c > best_c:
            best_c, best_d = c, d
    # fine pass around the coarse best
    for d in range(best_d - 4, best_d + 5):
        seg = mono[start + L + d:start + L + d + ref_len]
        if len(seg) < ref_len:
            continue
        c = float(np.dot(ref, seg) / (np.linalg.norm(ref) * np.linalg.norm(seg) + 1e-9))
        if c > best_c:
            best_c, best_d = c, d
    L = int(L + best_d)
    bpm_from_len = bars * beats_per_bar * 60.0 * sr / L
    return {"start": int(start), "length": L, "bpm": bpm_from_len,
            "bpm_estimated": est, "score": float(score), "wave_corr": float(best_c)}


def build_loop(x_stereo, start, length, crossfade_samples):
    C, N = x_stereo.shape
    K = int(min(crossfade_samples, length // 4, max(0, N - (start + length))))
    seg = x_stereo[:, start:start + length].copy()
    if K > 0:
        t = np.linspace(0.0, 1.0, K, dtype=np.float32)
        fin, fout = np.sin(t * np.pi / 2), np.cos(t * np.pi / 2)
        tail = x_stereo[:, start + length:start + length + K]
        seg[:, :K] = seg[:, :K] * fin + tail * fout
    return seg, K


def seam_ratio(loop, src, start, sr):
    """Spectral jump across the wrap divided by the jump at the same point in the
    original, continuous track. About 1.0 = as smooth as the music itself."""
    n = loop.shape[1]
    ctx = NFFT * 4
    wrap = np.concatenate([loop[:, -ctx:], loop[:, :ctx]], axis=1).mean(axis=0)
    a = max(0, start + n - ctx)
    orig = src[:, a:a + 2 * ctx].mean(axis=0)
    def jump(sig):
        b = _bands(_stft_mag(sig))
        d = np.linalg.norm(np.diff(b, axis=0), axis=1)
        mid = len(d) // 2
        return d[max(0, mid - 3):mid + 3].max(), np.median(d)
    jw, mw = jump(wrap)
    jo, mo = jump(orig)
    return float(jw / max(jo, mo, 1e-9))


class FJSeamlessAudioLoop:
    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {
            "audio": ("AUDIO",),
            "bpm": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 300.0, "step": 0.1,
                               "tooltip": "0 = estimate from the audio. Otherwise the BPM you put in the caption."}),
            "beats_per_bar": ("INT", {"default": 4, "min": 1, "max": 12}),
            "bars": ("INT", {"default": 8, "min": 1, "max": 128,
                              "tooltip": "Loop length in bars. 8 bars at 90 BPM = 21.3 s."}),
            "search_start_sec": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 600.0, "step": 0.5,
                                            "tooltip": "Skip the intro: never start the loop before this point."}),
            "search_end_sec": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 600.0, "step": 0.5,
                                          "tooltip": "Latest loop start (0 = anywhere). Use it to keep away from the outro."}),
            "crossfade_ms": ("INT", {"default": 40, "min": 0, "max": 2000,
                                      "tooltip": "Equal-power fold-over at the seam. 20-80 ms for beats, 200-800 ms for ambient pads."}),
            "preview_repeats": ("INT", {"default": 3, "min": 1, "max": 400,
                                         "tooltip": "Second output plays the loop this many times so you can audition the seam (or build a long BGM file)."}),
        }}

    RETURN_TYPES = ("AUDIO", "AUDIO", "STRING")
    RETURN_NAMES = ("loop", "repeated", "report")
    FUNCTION = "run"
    CATEGORY = "audio/loop"

    def run(self, audio, bpm, beats_per_bar, bars, search_start_sec, search_end_sec,
            crossfade_ms, preview_repeats):
        import torch
        wav = audio["waveform"]
        sr = int(audio["sample_rate"])
        x = wav[0].detach().float().cpu().numpy()
        info = find_loop(x, sr, bpm, beats_per_bar, bars, search_start_sec, search_end_sec)
        loop, K = build_loop(x, info["start"], info["length"], int(sr * crossfade_ms / 1000))
        ratio = seam_ratio(loop, x, info["start"], sr)
        rep = np.tile(loop, (1, preview_repeats))
        report = (f"start {info['start'] / sr:.3f}s | length {info['length'] / sr:.3f}s "
                  f"({info['length']} samples) | bars {bars} x {beats_per_bar} | "
                  f"loop tempo {info['bpm']:.2f} BPM (from audio {info['bpm_estimated']:.2f}) | "
                  f"match {info['score']:.3f} | wave corr {info['wave_corr']:.3f} | "
                  f"crossfade {K / sr * 1000:.0f} ms | seam ratio {ratio:.2f} "
                  f"({'clean' if ratio <= 1.5 else 'check by ear / try more bars or another start'})")
        print("[FJ Seamless Audio Loop]", report)
        to_t = lambda a: {"waveform": torch.from_numpy(np.ascontiguousarray(a)).unsqueeze(0).to(wav.dtype),
                          "sample_rate": sr}
        return (to_t(loop), to_t(rep), report)


NODE_CLASS_MAPPINGS = {"FJSeamlessAudioLoop": FJSeamlessAudioLoop}
NODE_DISPLAY_NAME_MAPPINGS = {"FJSeamlessAudioLoop": "Seamless Audio Loop (FJ)"}

Tip: if the node doesn’t appear, look in the ComfyUI console for an “IMPORT FAILED” line under ComfyUI-FJ-SeamlessAudioLoop. Usually the file was saved as __init__.py.txt.

Build the Workflow Step by Step

The quickest route is to load my workflow below. It is the official Music 3 template with two changes and the loop node added. Here is what it does, so you can rebuild it yourself.

  1. Open the template. Template Library, Audio, “MiniMax Music 3 Text to Music”. Let ComfyUI download any missing files.
  2. Swap to int8 (optional). In the UNETLoader inside the subgraph, pick minimax_music3_dit_int8_convrot.safetensors on 12 to 16 GB cards.
  3. Fix the length. Open the subgraph and remove the link from the text encoder’s seconds output to EmptyMiniMaxMusic3LatentAudio. Then type 75 into that node’s seconds field. Without this, my first lo-fi track stopped at 30.7 s.
  4. Paste a loop-friendly caption and lyrics from the next section. Set max_duration to 75 as well.
  5. Add Seamless Audio Loop (FJ) and connect the subgraph’s AUDIO output to its audio input.
  6. Save the results. Wire loop to a SaveAudio node (lossless FLAC, for real use) and to SaveAudioAdvanced (MP3, for sharing). Wire repeated to another SaveAudioAdvanced so you can listen to the seam 4 times in a row.
  7. Show the report. Connect report to a Preview Any node. It prints the start, length, tempo and seam ratio.
  8. Queue. Files land in ComfyUI/output/audio/.

Download the workflow and drag it onto the ComfyUI canvas. It needs the loop node installed and the four Music 3 files above.

Show the workflow JSON
{"id":"08882d3e-437d-43da-b498-083de04a60a6","revision":0,"last_node_id":55,"last_link_id":68,"nodes":[{"id":35,"type":"SaveAudioAdvanced","pos":[2150,1250],"size":[750,160],"flags":{},"order":3,"mode":0,"inputs":[{"name":"audio","type":"AUDIO","link":42}],"outputs":[{"name":"audio","type":"AUDIO","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0"},"widgets_values":["audio/music3_raw","mp3","V0"]},{"id":37,"type":"ac99f841-a3de-4329-9564-953b81cf9e16","pos":[1620,630],"size":[490,540],"flags":{},"order":0,"mode":0,"inputs":[{"label":"tiled_decode","name":"switch","type":"BOOLEAN","widget":{"name":"switch"},"link":null}],"outputs":[{"name":"AUDIO","type":"AUDIO","links":[42,64]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","previewExposures":[]},"widgets_values":["Global Metadata: Lo-fi hip-hop, chillhop instrumental. 84 BPM, A minor, steady and unchanging from start to end, loop-friendly with no intro, no build-up, no breakdown and no ending; the same 4-bar chord cycle repeats the whole time. Study and background listening. Warm dusty bedroom production, vinyl crackle, soft tape saturation.\n\nVocal Details: No vocals. Purely instrumental.\n\nArrangement: Steady boom-bap drum loop with soft kick, brushed snare and swung hi-hats playing the same pattern throughout, round sub bass, mellow Rhodes chords cycling Am7 - Dm9 - G7 - Cmaj7, light jazzy guitar fills, constant vinyl crackle. No intro, no fills that change the energy, no outro, no fade.","[Instrumental]\n\n[Instrumental]\n\n[Instrumental]\n\n[Instrumental]",75,197122968890040,"minimax_music3_dit_int8_convrot.safetensors","minimax_music3_text_encoder_pruned_int8_convrot.safetensors","minimax_music3_dav.safetensors",true]},{"id":50,"type":"FJSeamlessAudioLoop","pos":[2150,630],"size":[420,330],"flags":{},"order":10,"mode":0,"inputs":[{"name":"audio","type":"AUDIO","link":64}],"outputs":[{"name":"loop","type":"AUDIO","links":[65,66]},{"name":"repeated","type":"AUDIO","links":[67]},{"name":"report","type":"STRING","links":[68]}],"properties":{"Node name for S&R":"FJSeamlessAudioLoop"},"widgets_values":[0.0,4,8,6.0,0.0,40,4]},{"id":51,"type":"SaveAudioAdvanced","pos":[2650,560],"size":[420,120],"flags":{},"order":11,"mode":0,"inputs":[{"name":"audio","type":"AUDIO","link":65}],"outputs":[],"properties":{"Node name for S&R":"SaveAudioAdvanced"},"widgets_values":["audio/music3_loop","mp3","V0"]},{"id":52,"type":"SaveAudio","pos":[2650,720],"size":[420,120],"flags":{},"order":12,"mode":0,"inputs":[{"name":"audio","type":"AUDIO","link":66}],"outputs":[],"properties":{"Node name for S&R":"SaveAudio"},"widgets_values":["audio/music3_loop_lossless"]},{"id":53,"type":"SaveAudioAdvanced","pos":[2650,880],"size":[420,120],"flags":{},"order":13,"mode":0,"inputs":[{"name":"audio","type":"AUDIO","link":67}],"outputs":[],"properties":{"Node name for S&R":"SaveAudioAdvanced"},"widgets_values":["audio/music3_loop_x4_preview","mp3","V0"]},{"id":54,"type":"PreviewAny","pos":[2650,1040],"size":[420,160],"flags":{},"order":14,"mode":0,"inputs":[{"name":"source","type":"*","link":68}],"outputs":[],"properties":{"Node name for S&R":"PreviewAny"},"widgets_values":[]},{"id":55,"type":"MarkdownNote","pos":[2150,1000],"size":[440,220],"flags":{},"order":15,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["## Seamless loop (freakyjolly.com)\n\n1. Install **ComfyUI-FJ-SeamlessAudioLoop** into `ComfyUI/custom_nodes/`.\n2. Inside the subgraph, the Empty latent is set to a fixed **75 s** (its link from the text encoder's `seconds` output is removed) so Music 3 can't end the song early.\n3. `bpm 0` = measure the real tempo (Music 3 drifts a few BPM off the caption).\n4. Beats: crossfade 20-80 ms. Pads/ambient: 300-600 ms.\n5. Read the report: seam ratio <= 1.5 = clean."]}],"links":[[42,37,0,35,0,"AUDIO"],[64,37,0,50,0,"AUDIO"],[65,50,0,51,0,"AUDIO"],[66,50,0,52,0,"AUDIO"],[67,50,1,53,0,"AUDIO"],[68,50,2,54,0,"STRING"]],"groups":[],"definitions":{"subgraphs":[{"id":"ac99f841-a3de-4329-9564-953b81cf9e16","version":1,"state":{"lastGroupId":0,"lastNodeId":43,"lastLinkId":63,"lastRerouteId":0},"revision":0,"config":{},"name":"Text to Music (MiniMax Music 3)","inputNode":{"id":-10,"bounding":[320,820,128,208]},"outputNode":{"id":-20,"bounding":[3210,380,128,68]},"inputs":[{"id":"9f92b5c5-dadc-4b0f-84d9-b5ed2b3a0842","name":"caption","type":"STRING","linkIds":[45],"pos":[424,844]},{"id":"bb9d2920-ba98-4c51-be7d-4b5736b14407","name":"lyrics","type":"STRING","linkIds":[46],"pos":[424,864]},{"id":"be60d2f1-fc99-4ed6-a057-130a7b83945f","name":"max_duration","type":"FLOAT","linkIds":[47],"pos":[424,884]},{"id":"a7f66596-0413-4906-a08f-475af0521252","name":"seed","type":"INT","linkIds":[54],"pos":[424,904]},{"id":"cbfb7b7b-357f-44ab-80cb-8748020a9880","name":"unet_name","type":"COMBO","linkIds":[55],"pos":[424,924]},{"id":"ec2e20dc-2e7b-4147-922f-5f5f26ac9854","name":"clip_name","type":"COMBO","linkIds":[56],"pos":[424,944]},{"id":"eeefaa94-f342-415e-96e7-eb13dcccbac0","name":"vae_name","type":"COMBO","linkIds":[57],"pos":[424,964]},{"id":"6da74b03-27d0-487e-b8d6-f94018b2ca9a","name":"switch","type":"BOOLEAN","linkIds":[63],"label":"tiled_decode","pos":[424,984]}],"outputs":[{"id":"82bef6fd-2dee-4579-b4ed-7d679d18279f","name":"AUDIO","type":"AUDIO","linkIds":[62],"localized_name":"AUDIO","pos":[3234,404]}],"widgets":[],"nodes":[{"id":6,"type":"UNETLoader","pos":[700,380],"size":[480,90],"flags":{},"order":1,"mode":0,"showAdvanced":false,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":55}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[6]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"UNETLoader","models":[{"name":"minimax_music3_dit_fp16.safetensors","url":"https://huggingface.co/Comfy-Org/MiniMax-Music-3/resolve/main/diffusion_models/minimax_music3_dit_fp16.safetensors","directory":"diffusion_models"}]},"widgets_values":["minimax_music3_dit_int8_convrot.safetensors","default"]},{"id":13,"type":"MiniMaxMusic3TextEncode","pos":[1230,380],"size":[500,750],"flags":{},"order":6,"mode":0,"showAdvanced":true,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":18},{"localized_name":"caption","name":"caption","type":"STRING","widget":{"name":"caption"},"link":45},{"localized_name":"lyrics","name":"lyrics","type":"STRING","widget":{"name":"lyrics"},"link":46},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":52},{"localized_name":"max_duration","name":"max_duration","type":"FLOAT","widget":{"name":"max_duration"},"link":47}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":[15,16]},{"localized_name":"seconds","name":"seconds","type":"FLOAT","links":[]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"MiniMaxMusic3TextEncode"},"widgets_values":["","",222,"fixed",60,1.7,50]},{"id":3,"type":"CLIPLoader","pos":[700,550],"size":[480,120],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":56}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[18]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"CLIPLoader","models":[{"name":"minimax_music3_text_encoder_pruned_int8_convrot.safetensors","url":"https://huggingface.co/Comfy-Org/MiniMax-Music-3/resolve/main/text_encoders/minimax_music3_text_encoder_pruned_int8_convrot.safetensors","directory":"text_encoders"}]},"widgets_values":["minimax_music3_text_encoder_pruned_int8_convrot.safetensors","minimax","default"]},{"id":7,"type":"VAELoader","pos":[700,750],"size":[480,70],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":57}],"outputs":[{"localized_name":"VAE","name":"VAE","type":"VAE","links":[13,59]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"VAELoader","models":[{"name":"minimax_music3_dav.safetensors","url":"https://huggingface.co/Comfy-Org/MiniMax-Music-3/resolve/main/vae/minimax_music3_dav.safetensors","directory":"vae"}]},"widgets_values":["minimax_music3_dav.safetensors"]},{"id":10,"type":"ConditioningZeroOut","pos":[1820,540],"size":[230,40],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"conditioning","name":"conditioning","type":"CONDITIONING","link":16}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":[8]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"ConditioningZeroOut"},"widgets_values":[]},{"id":15,"type":"EmptyMiniMaxMusic3LatentAudio","pos":[1780,380],"size":[350,90],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"seconds","name":"seconds","type":"FLOAT","widget":{"name":"seconds"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[21]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"EmptyMiniMaxMusic3LatentAudio"},"widgets_values":[75,1]},{"id":9,"type":"KSampler","pos":[2170,380],"size":[370,270],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":6},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":15},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":8},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":21},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":53}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[12,58]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"KSampler"},"widgets_values":[1111111112,"fixed",30,1.7,"euler","simple",1]},{"id":12,"type":"VAEDecodeAudio","pos":[2590,380],"size":[230,60],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":12},{"localized_name":"vae","name":"vae","type":"VAE","link":13}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[60]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"VAEDecodeAudio"},"widgets_values":[]},{"id":38,"type":"SeedNode","pos":[890,970],"size":[270,90],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":54}],"outputs":[{"localized_name":"seed","name":"seed","type":"INT","links":[52,53]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"SeedNode"},"widgets_values":[0,"randomize"]},{"id":42,"type":"VAEDecodeAudioTiled","pos":[2580,500],"size":[280,110],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":58},{"localized_name":"vae","name":"vae","type":"VAE","link":59}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[61]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"VAEDecodeAudioTiled"},"widgets_values":[1536,64]},{"id":43,"type":"ComfySwitchNode","pos":[2600,690],"size":[270,110],"flags":{},"order":10,"mode":0,"inputs":[{"localized_name":"on_false","name":"on_false","type":"AUDIO","link":60},{"localized_name":"on_true","name":"on_true","type":"AUDIO","link":61},{"localized_name":"switch","name":"switch","type":"BOOLEAN","widget":{"name":"switch"},"link":63}],"outputs":[{"localized_name":"output","name":"output","type":"AUDIO","links":[62]}],"properties":{"cnr_id":"comfy-core","ver":"0.31.0","Node name for S&R":"ComfySwitchNode"},"widgets_values":[false]}],"groups":[],"links":[{"id":18,"origin_id":3,"origin_slot":0,"target_id":13,"target_slot":0,"type":"CLIP"},{"id":16,"origin_id":13,"origin_slot":0,"target_id":10,"target_slot":0,"type":"CONDITIONING"},{"id":6,"origin_id":6,"origin_slot":0,"target_id":9,"target_slot":0,"type":"MODEL"},{"id":15,"origin_id":13,"origin_slot":0,"target_id":9,"target_slot":1,"type":"CONDITIONING"},{"id":8,"origin_id":10,"origin_slot":0,"target_id":9,"target_slot":2,"type":"CONDITIONING"},{"id":21,"origin_id":15,"origin_slot":0,"target_id":9,"target_slot":3,"type":"LATENT"},{"id":12,"origin_id":9,"origin_slot":0,"target_id":12,"target_slot":0,"type":"LATENT"},{"id":13,"origin_id":7,"origin_slot":0,"target_id":12,"target_slot":1,"type":"VAE"},{"id":45,"origin_id":-10,"origin_slot":0,"target_id":13,"target_slot":1,"type":"STRING"},{"id":46,"origin_id":-10,"origin_slot":1,"target_id":13,"target_slot":2,"type":"STRING"},{"id":47,"origin_id":-10,"origin_slot":2,"target_id":13,"target_slot":4,"type":"FLOAT"},{"id":52,"origin_id":38,"origin_slot":0,"target_id":13,"target_slot":3,"type":"INT"},{"id":53,"origin_id":38,"origin_slot":0,"target_id":9,"target_slot":4,"type":"INT"},{"id":54,"origin_id":-10,"origin_slot":3,"target_id":38,"target_slot":0,"type":"INT"},{"id":55,"origin_id":-10,"origin_slot":4,"target_id":6,"target_slot":0,"type":"COMBO"},{"id":56,"origin_id":-10,"origin_slot":5,"target_id":3,"target_slot":0,"type":"COMBO"},{"id":57,"origin_id":-10,"origin_slot":6,"target_id":7,"target_slot":0,"type":"COMBO"},{"id":58,"origin_id":9,"origin_slot":0,"target_id":42,"target_slot":0,"type":"LATENT"},{"id":59,"origin_id":7,"origin_slot":0,"target_id":42,"target_slot":1,"type":"VAE"},{"id":60,"origin_id":12,"origin_slot":0,"target_id":43,"target_slot":0,"type":"AUDIO"},{"id":61,"origin_id":42,"origin_slot":0,"target_id":43,"target_slot":1,"type":"AUDIO"},{"id":62,"origin_id":43,"origin_slot":0,"target_id":-20,"target_slot":0,"type":"AUDIO"},{"id":63,"origin_id":-10,"origin_slot":7,"target_id":43,"target_slot":2,"type":"BOOLEAN"}],"extra":{}}]},"config":{},"extra":{"ds":{"scale":0.7901285864132895,"offset":[-390.5963888708135,-245.41184011189858]},"frontendVersion":"1.48.7","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}

Prompts That Loop Well

The caption has three parts: Global Metadata, Vocal Details and Arrangement. For loops, every line should push toward “the same thing, all the way through”.

  • Say it out loud: “loop-friendly, no intro, no build-up, no breakdown and no ending”.
  • Name a short chord cycle. “The same 4-bar chord cycle repeats the whole time” gives the node many matching windows to pick from.
  • No vocals. Sung phrases rarely land on a loop boundary. Write “No vocals. Purely instrumental.”
  • Lyrics = tags only. Use [Instrumental] repeated three or four times, nothing else.
  • Avoid risers, drops and fades. They are one-way events, and the loop will repeat them.

Prompt 1: lo-fi beat (study, stream, menus)

Global Metadata: Lo-fi hip-hop, chillhop instrumental. 84 BPM, A minor, steady and unchanging from start to end, loop-friendly with no intro, no build-up, no breakdown and no ending; the same 4-bar chord cycle repeats the whole time. Study and background listening. Warm dusty bedroom production, vinyl crackle, soft tape saturation.

Vocal Details: No vocals. Purely instrumental.

Arrangement: Steady boom-bap drum loop with soft kick, brushed snare and swung hi-hats playing the same pattern throughout, round sub bass, mellow Rhodes chords cycling Am7 - Dm9 - G7 - Cmaj7, light jazzy guitar fills, constant vinyl crackle. No intro, no fills that change the energy, no outro, no fade.

Lyrics: [Instrumental] four times, each on its own line with a blank line between.

Prompt 2: ambient pad (sleep, focus, game worlds)

Global Metadata: Ambient, drone, meditation instrumental. 70 BPM, D major, calm and constant from start to end, loop-friendly with no intro, no climax and no ending. Background for focus, sleep and video game menus. Wide, soft, airy production with long reverb.

Vocal Details: No vocals. Purely instrumental.

Arrangement: Slowly evolving warm synth pads holding D - A - Bm - G, soft sustained strings, gentle sparse piano notes, faint shimmering textures, no drums, no percussion. The same calm texture throughout, no fade-in, no fade-out.

Lyrics: [Instrumental] three times. For more caption ideas, MiniMax offers a caption rewriter skill: npx skills add MiniMax-AI/MiniMax-Music3 --skill music-caption-rewriter.

Loop Node Settings Explained

Setting Default What it does My advice
bpm 0 0 measures the tempo from the audio; any other value uses that BPM Keep 0 for Music 3, which drifts off the caption
beats_per_bar 4 Beats in one bar 3 for waltz time, 6 for 6/8 feels
bars 8 Loop length in bars 8 bars at 86 BPM is about 22 s. Use 16 for less obvious repetition, 4 for short game cues
search_start_sec 6 Never start the loop before this point Skips the first bars, where the model is still settling in
search_end_sec 0 Latest allowed start (0 = anywhere) Set it if the track fades or changes near the end
crossfade_ms 40 Length of the fold-over at the seam 20 to 80 ms for drums; 300 to 600 ms for pads and drones
preview_repeats 3 How many times the repeated output plays the loop 4 to audition; around 160 turns a 22 s loop into an hour

The track must be long enough for the loop plus a 2-second check window after search_start_sec. With 8 bars near 86 BPM that is about 30 s, which is why I render 75 s and give the search plenty of room.

My Test Results

I ran both prompts on the second RTX 5080 (16 GB) in my PC with the int8 text encoder: the lo-fi track on the int8 diffusion model and the ambient pad on fp16. Sampling was 30 steps, CFG 1.7, euler / simple, seed 42, as in the template.

Test Prompted BPM Measured BPM Loop Crossfade Seam ratio Click at wrap Render time
Lo-fi beat 84 about 86 8 bars 40 ms 1.21 None 8 to 9 min
Ambient pad 70 about 72.7 8 bars 400 ms 1.22 None 8 to 9 min

Listen: the loops and the originals

Each loop below plays 4 times in a row, so the seam passes 3 times. Try to hear where it restarts. The waveform marks every seam with a line.

Lo-fi beat: 8-bar loop (22.6 s), played 4x

Lo-fi loop played 4 times. Loop found at 39.1 s of the 75 s track, 40 ms crossfade.
Waveform of the MiniMax Music 3 lo-fi loop repeated four times with seam lines marked
Lo-fi loop x4: the lines mark the seams.
The original 75 s Music 3 lo-fi track, before looping.
Waveform of the full 75 second MiniMax Music 3 lo-fi track with the chosen 8-bar loop region boxed
Full lo-fi track: the box is the 8 bars the node picked.

Ambient pad: 8-bar loop (26.7 s), played 4x

Ambient loop played 4 times, 400 ms crossfade (fp16 model).
Waveform of the MiniMax Music 3 ambient pad loop repeated four times with seam lines marked
Ambient loop x4 with seam lines.
The original 75 s Music 3 ambient track.
Waveform of the full 75 second MiniMax Music 3 ambient track with the chosen loop region boxed
Full ambient track with the loop region boxed.

What I took away from the tests:

  • Auto BPM matters. If I had forced 84 BPM, each 8-bar loop would have been around half a second too long, and the beat would drift every repeat.
  • int8 is enough. It gave about the same result as fp16 and leaves VRAM free.
  • Lock the length. Music 3 can predict a shorter song than you asked for. A fixed latent length stops that.

Warning: the first version of my node crashed with bpm 0 on a short track, because the tempo refinement ran past the end of the audio. The code on this page has the fix. If you copied an earlier version, replace it.

Export for Games, Streams and Video

A loop is only seamless if the player repeats it without a gap. The file format decides that.

Use Format How to loop it
Unity WAV or OGG Tick Loop on the AudioSource
Godot WAV or OGG Turn on loop in the Import dock, then reimport
Website background OGG or AAC (M4A) loop attribute on the audio element; test in each browser
Stream (OBS) FLAC or WAV Media Source with Loop ticked
Long YouTube BGM Any Raise preview_repeats and save the repeated output as one long file

To turn the FLAC into OGG or WAV, use ffmpeg: ffmpeg -i music3_loop_lossless_00001_.flac -c:a libvorbis -q:a 6 loop.ogg, or ffmpeg -i music3_loop_lossless_00001_.flac loop.wav.

Why not MP3? MP3 encoders add a short silent padding at the start of the file. Many players don’t remove it, so every repeat has a tiny gap. Keep MP3 for previews and sharing.

Want visuals that loop too? Pair this with our MiniMax H3 seamless loop video guide, which makes still-camera cinemagraph loops in the same ComfyUI.

Other Ways to Loop Music

Method Inside ComfyUI Bar-aligned Best for
Seamless Audio Loop (FJ), this guide Yes Yes One-click loops right after generation
PyMusicLooper (MIT, command line) No Finds loop points by similarity Existing tracks; its split-audio makes intro, loop and outro files, and extend makes long versions
Audacity or a DAW by hand No If you snap to the grid Fine control and one-off edits
Generate a longer track Yes No Background music where an occasional repeat isn’t a problem

Looking for other local music models? Our best local text-to-song models and best local background music generators compare YuE2, ACE-Step, Stable Audio and more. The same loop node works on their output too, because it takes any ComfyUI audio.

Troubleshooting

Problem Cause Fix
Song ends far earlier than max_duration The text encoder’s seconds output feeds the latent with its own shorter guess Unlink it and type a fixed length into EmptyMiniMaxMusic3LatentAudio
Track too short for 8 bars at ... BPM Loop plus check window doesn’t fit after search_start_sec Lower bars or search_start_sec, or render longer
Beat slips a little on every repeat Forced BPM doesn’t match the real tempo Set bpm to 0
Soft “whoosh” or doubled notes at the seam Crossfade too long for percussive music Drop crossfade_ms to 20 to 40
Click at the seam on pads Crossfade too short for slow sounds Raise crossfade_ms to 300 to 600
Seam ratio above 1.5 The track changes too much between bars Try 16 bars, move search_start_sec, or reroll with a stricter “no changes” caption
Tiny gap when the file repeats MP3 padding Use the FLAC output, or convert it to WAV or OGG
Out of memory fp16 model plus encoder on a small card Use the int8 files and turn on tiled decode; see ComfyUI errors and fixes
Node missing after restart Wrong folder or file name It must be custom_nodes/ComfyUI-FJ-SeamlessAudioLoop/__init__.py

FAQ

Does MiniMax Music 3 have a built-in loop option?

No. It only does text-to-music, so you loop the result afterwards. This guide’s node does that inside ComfyUI.

Can I loop a song with vocals?

You can, but it rarely sounds natural, because sung lines don’t end on loop boundaries. For background use, prompt instrumentals, or loop only an instrumental section.

How long should the loop be?

8 bars (about 20 to 30 s) works for most game and stream music. Use 16 bars when listeners will hear it for a long time, so the repetition is less obvious.

Does the node work with ACE-Step, YuE or Stable Audio?

Yes. It accepts any ComfyUI AUDIO input, so you can connect any music model’s output to it, or a Load Audio node.

Why do you recommend auto BPM when I wrote the BPM in the caption?

Because Music 3 treats tempo as guidance. My 84 and 70 BPM prompts came out near 86 and 72.7, and loops cut at the prompted tempo would drift.

Can I use Music 3 loops commercially?

Under the MiniMax-Music3 Community License, companies with under 20 million USD yearly revenue can, according to Comfy’s tutorial. Larger companies need a commercial licence. Read the licence file on Hugging Face before you ship.

How much VRAM do I need?

16 GB is comfortable with the int8 files. The model card says layer streaming can fit 8 GB cards, at a large speed cost.

How do I make a one-hour version?

Set preview_repeats so the loop length times repeats is about 3,600 s (around 160 for a 22 s loop), and save the repeated output.

Final Thoughts

Music 3 is the strongest open song model I’ve run locally, and a small post-processing step makes it a good loop generator too. Prompt for steadiness, render 75 s, leave the tempo on auto, and let the node choose the cut.

Next, give your loops some visuals with the MiniMax H3 seamless loop video guide, or browse every MiniMax workflow in the MiniMax H3 ComfyUI guide.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Winking woman in a burnt orange blazer in a colourful podcast studio holding a card that reads "MINIMAX", with the text "Endless AI music loops"