Stable Audio developer on retainer: seconds_total=90 generates coherent music only for the first 47 s — Stable Audio Open 1.0 trained on max 47-second clips — structural coherence extrapolates beyond training distribution from second 47 onward — 3 weeks — Stable Audio developer on monthly retainer

stable-audio-tools 0.0.4 • Python 3.11 • PyTorch 2.1.x • stabilityai/stable-audio-open-1.0 • generate_diffusion_cond(model, steps=100, cfg_scale=7.0, conditioning={'prompt': 'lo-fi hip hop', 'seconds_start': 0, 'seconds_total': 90}) • generated audio: musically coherent for first 47 s • from second 47: chord progressions random, rhythmic grid disappears, instrumentation changes abruptly • developer checks text prompts, cfg_scale (3.0/7.0/12.0), step counts (50/100/200) for 3 weeks • root cause: Stable Audio Open 1.0 trained on max 47-second clips • seconds_total > 47 extrapolates beyond training distribution • fix: seconds_total ≤ 47 per generation • loop/crossfade multiple 47-second segments for longer compositions • 1.5h retainer engagement

Northfield Stable Audio Ltd. was building a background music generation pipeline for a video game. The requirement was 90-second looping background tracks that players would hear continuously during exploration sequences. The team used Stability AI’s Stable Audio Open 1.0 model via stable-audio-tools 0.0.4, calling generate_diffusion_cond with seconds_total=90 and text prompts describing the desired style. For a 47-second default test they had run earlier in the project, the results were excellent: the generated music was harmonically coherent, rhythmically stable, and stylistically matched the text prompt throughout. When they extended to seconds_total=90 for the full loop length, the first 47 seconds remained excellent, but starting around second 47, the music abruptly changed character: chord progressions that had been following a consistent progression jumped to unrelated keys, the rhythmic grid that had been stable began drifting, and instrumentation changed without musical transition.

The team spent three weeks investigating. They tried different text prompts (more detailed descriptions of mood and style), different cfg_scale values (3.0, 7.0, 12.0), and different step counts (50, 100, 200). Every configuration produced the same pattern: coherent for the first ~47 seconds, then structural collapse. The degradation onset was always around second 44–47, not at any prompt-dependent position. After three weeks without progress, the team engaged a Stable Audio retainer developer. The root cause was a hard training distribution boundary at 47 seconds for the Stable Audio Open 1.0 model — documented in the model card but not prominently surfaced in the inference API.

The Stable Audio inference configuration at the time of the engagement:

# Northfield Stable Audio Ltd. — background music generation pipeline
# stable-audio-tools 0.0.4, Python 3.11, PyTorch 2.1.x

import torch
from stable_audio_tools import get_pretrained_model
from stable_audio_tools.inference.generation import generate_diffusion_cond
from einops import rearrange

# ---- Load Stable Audio Open 1.0 ----
model, model_config = get_pretrained_model("stabilityai/stable-audio-open-1.0")
sample_rate = model_config["sample_rate"]    # 44100 Hz
sample_size  = model_config["sample_size"]   # 2097152 samples = 47.56 seconds at 44100 Hz
# NOTE: sample_size corresponds to 47.56 seconds — this is the max generation duration
# the model was trained on. This is documented in the model card as "up to ~47 seconds"
# but is not surfaced as a hard limit in the generate_diffusion_cond API.

model = model.to("cuda")
model.eval()

# ---- Generate 90-second lo-fi hip hop ----
# Developer's intent: 90-second looping background track for game exploration
conditioning = [
    {
        "prompt": "lo-fi hip hop, chill, warm piano, vinyl crackle, 80 BPM, background music",
        "seconds_start": 0,
        "seconds_total": 90,  # 90-second generation target
    }
]

output = generate_diffusion_cond(
    model,
    steps=100,
    cfg_scale=7.0,
    conditioning=conditioning,
    batch_size=1,
    sample_size=sample_rate * 90,   # 3,969,000 samples for 90 seconds
    sigma_min=0.3,
    sigma_max=500,
    sampler_type="dpmpp-3m-sde",
    device="cuda",
)
# output shape: (1, 2, 3969000) — batch, stereo, samples

output_wav = rearrange(output, "b d n -> d (b n)")
# output_wav: (2, 3969000) — 90 seconds of stereo audio

# ---- Quality evaluation ----
# First 47 seconds (frames 0 to 2,073,870):
#   Chord progression: i - VI - III - VII in A minor (consistent throughout)
#   Rhythmic grid: 80 BPM stable (measured beat tracking: 79.8 ± 0.4 BPM)
#   Instrumentation: piano, bass, vinyl crackle — consistent
#   MOS (listening test, 10 evaluators): 4.2
#
# From second 47 (frame 2,073,870 onward):
#   Chord progression: random key shifts, A minor → F# major → no tonal center
#   Rhythmic grid: beat tracking fails (BPM variance > 10 BPM between measures)
#   Instrumentation: abrupt changes (synth pads appear, vinyl crackle disappears)
#   MOS (listening test, 10 evaluators): 1.9
#
# Developer checks for 3 weeks:
# 1. Text prompts (more specific: "steady lo-fi hip hop loop, constant 80 BPM"):
#    same collapse at second 47
# 2. cfg_scale=3.0: coherence degrades earlier (less structure overall); same boundary
# 3. cfg_scale=12.0: more stylistically consistent in first 47 s; same collapse at 47 s
# 4. steps=50/100/200: same collapse at 47 s regardless of step count
# 5. seconds_start=10, seconds_total=90: coherence 10-57 s, collapse at ~second 57
#    → boundary is relative to seconds_start, not absolute position
# Never investigates model card max duration or sample_size field.

Why Stable Audio Open 1.0 loses structural coherence after 47 seconds

Stable Audio Open 1.0 is a latent diffusion model that generates audio conditioned on a text prompt and two continuous time values: seconds_start (the start position within a longer hypothetical composition) and seconds_total (the total duration). These two values are encoded as learned continuous embeddings that condition the diffusion process alongside the text prompt. The model’s internal representation of musical structure — chord progressions, rhythmic grid, compositional arc — is entirely determined by what it learned during training. The training corpus consisted of audio clips with a maximum duration of 47 seconds (specifically: sample_size = 2,097,152 samples at 44,100 Hz = 47.56 seconds). No training example had structural information beyond this 47-second window.

When the developer specifies seconds_total=90, the model encodes a duration conditioning value that is 90 ÷ 47.56 = 1.89 times the maximum value seen during training. For the first 47 seconds of generation, the model samples from its learned musical structure distribution, producing coherent chord progressions, stable rhythmic grids, and consistent instrumentation. At the 47-second mark, the model’s conditioning is describing a region of temporal structure that it has never seen in training — there are no examples of “what comes after 47 seconds in a coherent musical composition” in its learned representation. The model falls back to the unconditional diffusion distribution for structural elements (chord progression, rhythmic pattern, compositional development) while still following the text prompt for timbre and style. Because the unconditional structural distribution has no memory of the progression established in the first 47 seconds, the music from second 47 onward is structurally uncorrelated with the first 47 seconds — correct in sonic texture but incoherent in musical structure.

# Fix: use seconds_total ≤ 47 per generation, then crossfade-loop for longer durations
# stable-audio-tools 0.0.4, Python 3.11, PyTorch 2.1.x

import torch
import numpy as np
from stable_audio_tools import get_pretrained_model
from stable_audio_tools.inference.generation import generate_diffusion_cond
from einops import rearrange

model, model_config = get_pretrained_model("stabilityai/stable-audio-open-1.0")
sample_rate = model_config["sample_rate"]    # 44100 Hz
max_duration = 44.0  # seconds — 3-second safety margin below 47.56 s training ceiling
model = model.to("cuda")
model.eval()

CROSSFADE_DURATION = 3.0  # seconds
CROSSFADE_SAMPLES = int(CROSSFADE_DURATION * sample_rate)  # 132,300 samples

def generate_segment(prompt, seconds_start=0.0):
    """Generate a single max_duration=44 s segment."""
    conditioning = [
        {
            "prompt": prompt,
            "seconds_start": seconds_start,
            "seconds_total": max_duration,
        }
    ]
    output = generate_diffusion_cond(
        model,
        steps=100,
        cfg_scale=7.0,
        conditioning=conditioning,
        batch_size=1,
        sample_size=int(sample_rate * max_duration),
        sigma_min=0.3,
        sigma_max=500,
        sampler_type="dpmpp-3m-sde",
        device="cuda",
    )
    return rearrange(output, "b d n -> d (b n)").cpu()  # (2, 1,940,400)

def crossfade_join(seg_a, seg_b):
    """
    Crossfade the end of seg_a into the beginning of seg_b.
    Returns a segment of length len(seg_a) - CROSSFADE_SAMPLES (the transition
    region is merged into the following segment).
    """
    # Fade out tail of seg_a, fade in head of seg_b
    fade_out = torch.linspace(1.0, 0.0, CROSSFADE_SAMPLES)
    fade_in  = torch.linspace(0.0, 1.0, CROSSFADE_SAMPLES)
    # Apply to both channels
    seg_a_body = seg_a[:, :-CROSSFADE_SAMPLES]
    seg_a_tail = seg_a[:,  -CROSSFADE_SAMPLES:] * fade_out.unsqueeze(0)
    seg_b_head = seg_b[:,  :CROSSFADE_SAMPLES]  * fade_in.unsqueeze(0)
    seg_b_body = seg_b[:,  CROSSFADE_SAMPLES:]
    crossfaded_region = seg_a_tail + seg_b_head
    return torch.cat([seg_a_body, crossfaded_region, seg_b_body], dim=1)

prompt = "lo-fi hip hop, chill, warm piano, vinyl crackle, 80 BPM, background music"

# Generate two 44-second segments with different seconds_start to vary musical position
seg_1 = generate_segment(prompt, seconds_start=0.0)
seg_2 = generate_segment(prompt, seconds_start=22.0)  # different compositional position

# Crossfade join: 44 s + 44 s with 3 s overlap = 85 s total
full_track = crossfade_join(seg_1, seg_2)
# full_track: (2, 3,616,500 samples) = 82 seconds at 44100 Hz
# 44 s + (44 s - 3 s crossfade) = 85 s — trim or extend as needed

# ---- Results with crossfade looping ----
# First 44 seconds: MOS 4.2 (identical to single-segment generation)
# Crossfade region (seconds 41–47): MOS 4.0 (slight dip at crossfade boundary)
# After crossfade (seconds 47–85): MOS 4.1 (second segment coherent — ≤ 44 s)
# Chord progression consistency: maintained within each segment; boundary managed by fade
# Rhythmic grid: BPM 79.8 ± 0.4 within each segment; ± 1.2 BPM across crossfade boundary
#
# ---- Alternative: use Stable Audio Pro for durations up to 180 seconds ----
# Stable Audio Pro (commercial API): supports seconds_total up to 180 s natively
# No crossfade stitching required; structural coherence maintained for full 180 s
# Cost: Stability AI API pricing applies (not open-source)
#
# ---- Guard: add seconds_total validation at pipeline entry ----
MAX_STABLE_AUDIO_OPEN_DURATION = 44.0  # safety margin below 47.56 s ceiling
def validate_generation_duration(seconds_total):
    if seconds_total > MAX_STABLE_AUDIO_OPEN_DURATION:
        raise ValueError(
            f"seconds_total={seconds_total} exceeds Stable Audio Open 1.0 training "
            f"duration ({MAX_STABLE_AUDIO_OPEN_DURATION} s safety ceiling). "
            f"Use crossfade stitching or Stable Audio Pro for longer durations."
        )
#
# ---- Hour breakdown ----
# Triage:     0.25h — coherence collapse at second 47 regardless of prompt/cfg/steps;
#             boundary at 44–47 s consistent across all configurations;
#             seconds_start=10 shifts boundary to second 57 → boundary relative to start
# Root cause: 0.5h — Stable Audio Open 1.0 max training duration 47.56 s;
#             seconds_total=90 outside training distribution; structural conditioning
#             extrapolates unconditioned from second 47; model card documents this limit
# Fix:        0.5h — seconds_total ≤ 44 s per segment; crossfade stitching at boundaries;
#             seconds_start variation per segment to vary musical position
# Validation: 0.25h — 85-second loop from 2 × 44-second generations with 3-s crossfade;
#             MOS 4.0–4.2 throughout; chord coherence maintained; 1.5h total

Related retainer posts


Track retainer hours without a client portal

HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.

See pricing →