---
title: "Scoring a Film With MusicGen Locally: The Parts Nobody Documents"
description: "MusicGen gives you thirty good seconds at a time. Scoring a 3:20 film means solving the rest — arranging cues against the shot map, stitching them without an audible seam, ducking under narration, and the licence question nobody answers straight."
canonical: "https://www.razi.pro/blog/scoring-a-film-with-musicgen-locally"
date: "2026-09-07"
tags: ["MusicGen", "Audio", "AI", "ffmpeg", "Video Gen"]
source: "razi.pro"
---

# Scoring a Film With MusicGen Locally: The Parts Nobody Documents

Every result you will find for this stops at the same place: here is how to generate a fifteen-second clip.

Then you have a three-minute film and twenty independently generated audio beds that do not join, and the actual problem starts. This is the write-up of solving it for a finished 3:20 animated short — the arrangement, the stitching, the mix, and the licence question that most pages skip.

The film itself, and everything else that broke making it, is [the Star Patrol production log](https://www.razi.pro/blog/making-an-ai-animated-film-with-veo-and-flow).

## What MusicGen is good and bad at

Setting expectations first, because it reframes the whole job.

MusicGen is very good at short, textured, atmospheric cues — ten to thirty seconds of a mood. It is poor at structure. It cannot hold a melody across minutes, and it does not know where your reveal is.

**Its practical ceiling for coherent output is around thirty seconds.** Ask for two hundred and you get something that wanders away from where it started.

Which means the model is not writing your score. It is producing raw material, and you are arranging it. Once you accept that, the rest of this is straightforward.

## Generating the cues

Run locally through HuggingFace Transformers against `facebook/musicgen-medium` — 1.5 billion parameters — rather than through AudioCraft directly:

    import torch, scipy.io.wavfile as wav
    from transformers import MusicgenForConditionalGeneration, AutoProcessor

    MODEL = "facebook/musicgen-medium"
    proc = AutoProcessor.from_pretrained(MODEL)
    model = MusicgenForConditionalGeneration.from_pretrained(
        MODEL, dtype=torch.float16).to("cuda")
    sr = model.config.audio_encoder.sampling_rate

    TOKENS = 30 * 50   # 50 audio tokens per second -> 30 seconds

    inputs = proc(text=[prompt], padding=True, return_tensors="pt").to("cuda")
    with torch.no_grad():
        audio = model.generate(**inputs, do_sample=True,
                               guidance_scale=3.0, max_new_tokens=TOKENS)
    a = audio[0, 0].float().cpu().numpy()
    a = a / max(abs(a).max(), 1e-6) * 0.9
    wav.write("cue.wav", rate=sr, data=(a * 32767).astype("int16"))

The token arithmetic is the part worth internalising: **fifty audio tokens per second.** Thirty seconds is 1,500 tokens. That is the number you tune, and it is the ceiling you are working against.

## Prompting for score, not for songs

The prompts that worked read like a music director's brief, not like a song request. Instrumentation, mood, tempo, and an explicit exclusion:

> whimsical light orchestral score for a children's animated adventure, playful pizzicato strings, soft woodwinds, warm gentle and storybook, moderate tempo

> dreamy cinematic lullaby, celesta and soft strings under a night sky, magical wonder, calm slow and emotional, no drums

Three cues were generated and two survived audition. That ratio is normal — generate more than you need and throw most away, because auditioning is free and regeneration is cheap.

Note "no drums" in the second. Negative instrumentation works, and it matters for anything sitting under dialogue.

## The length problem, and why looping is wrong

The naive step after generating one good thirty-second cue is to loop it twenty times.

Do not. Three minutes of unchanging texture actively fights the film. A score that does not change is worse than no score, because it flattens the moments that were supposed to be different.

Instead, arrange the cues against the shot map, so the music changes where the story changes:

| Section | Shots | Cue | Length |
|---|---|---|---:|
| Adventure | 1–8 | b | 82 s |
| Wonder, and the quiet beat | 9–14 | c | 62 s |
| Triumph | 15–18 | b | 42 s |
| Home | 19–20 | c | 20 s |

Two cues, four sections. Cue `b` returning for the triumph is doing real narrative work — it is the adventure theme coming back once the problem is solved.

## Stitching without an audible seam

Each section is built by crossfading a cue into *itself* to reach the length it needs. A hard loop point is audible. **A two-second triangular crossfade is not.**

    X=2   # crossfade seconds

    seamless_loop () {
      local src=$1 copies=$2 len=$3 out=$4
      local inputs="" fc="" cur="[0:a]"
      for i in $(seq 0 $((copies-1))); do inputs="$inputs -i $src"; done
      for i in $(seq 1 $((copies-1))); do
        fc="${fc}${cur}[${i}:a]acrossfade=d=${X}:c1=tri:c2=tri[x${i}];"
        cur="[x${i}]"
      done
      ffmpeg -y $inputs -filter_complex \
        "${fc}${cur}atrim=0:${len},asetpts=PTS-STARTPTS,aresample=48000[o]" \
        -map "[o]" -c:a pcm_s16le "$out"
    }

Then the four finished sections are crossfaded to each other the same way, and the whole bed is normalised:

    ffmpeg -y -i _s1.wav -i _s2.wav -i _s3.wav -i _s4.wav \
      -filter_complex "[0][1]acrossfade=d=2:c1=tri:c2=tri[a];\
    [a][2]acrossfade=d=2:c1=tri:c2=tri[b];\
    [b][3]acrossfade=d=2:c1=tri:c2=tri[c];\
    [c]loudnorm=I=-22:TP=-3[out]" \
      -map "[out]" -c:a libmp3lame -b:a 192k music.mp3

The bed goes to **−22 LUFS**, which is deliberately quiet. It is going to sit under clip audio and narration and be normalised again in the final mix. Mastering the bed to delivery loudness here would leave you no headroom later.

## Mixing under narration

Sidechain compression, so the music and clip audio duck automatically when the narrator speaks and come back when they stop:

    [narr_in]volume=1.3,aresample=48000[narr];
    [narr_in]volume=1.3,aresample=48000[narrsc];
    [bed][narrsc]sidechaincompress=threshold=0.04:ratio=9:attack=15:release=350[duck];
    [duck][narr]amix=inputs=2:duration=first:normalize=0[mixed];

Three things in there are not obvious.

**The narration is split into two identical branches.** `[narr]` goes into the mix; `[narrsc]` is the sidechain key. This is necessary because `sidechaincompress` consumes its key input — feed it the same stream you are mixing and the narration disappears.

**The parameters are chosen for intelligibility, not subtlety.** `ratio=9` is aggressive on purpose. `attack=15` ms ducks fast enough not to clip the narrator's first syllable. `release=350` ms is slow enough that the music does not pump between words inside a sentence.

**`normalize=0` on every `amix`.** By default `amix` divides by the number of inputs, which silently halves everything each time you add a layer. If your levels are deliberate, turn the automatic normalisation off or watch the mix quietly collapse.

Clip audio is attenuated by how much is competing with it — full level when it is alone, 0.85 under music, 0.45 under narration, where it becomes texture rather than content.

## Mastering

    loudnorm=I=-16:TP=-1.5:LRA=11,alimiter=limit=0.97

**−16 LUFS with a −1.5 dBTP ceiling.** That is the streaming-delivery target and the right choice for something played back on a tablet or a TV. The limiter after it is a safety net, not a loudness tool.

## The licence question, answered plainly

This is the section people arrive for, so here is the fact rather than a hedge.

MusicGen's model card states it directly: **"Code is released under MIT, model weights are released under CC-BY-NC 4.0."**

CC-BY-NC is a **non-commercial** licence. The code you run is MIT and unencumbered. The weights doing the actual generating are not.

Where the genuine ambiguity sits: the licence governs the model weights, and whether audio *generated by* those weights is itself a derivative work covered by the same terms is not settled, and reasonable people read it differently. I am not going to pretend that is resolved, because it is not.

What that means practically. For a personal film, a portfolio piece or anything unmonetised, this is not a question you need to answer. For a monetised YouTube channel, an advert or client work, you are relying on a reading of a non-commercial licence that has not been tested, and the model card itself says the model is intended for research rather than downstream commercial applications without further evaluation. That is a risk to take deliberately, not by accident.

If the answer matters to your project, the honest options are library music with a clear commercial licence, or a composer.

## When to just license a track

Worth stating plainly. The YouTube Audio Library, the Free Music Archive and Incompetech are free, cleared, and vastly better recorded than anything MusicGen produces.

What you give up is the entire point of this article: **a library track does not know where your reveal is.** You would be doing the same crossfade arrangement work against someone else's cues, with less control over where they change.

And the option it is easy to forget: a person with a keyboard and a free DAW beats all of the above in an afternoon.

Two more from the same production — [what broke in Veo's character consistency](https://www.razi.pro/blog/veo-character-consistency-what-broke), and [the pipeline end to end](https://www.razi.pro/blog/ai-video-creation-pipeline-story-to-youtube). If you need to pull the audio back out of a finished cut to check it, [video to MP3](https://www.razi.pro/tools/video-to-mp3) does that, and [the stem splitter](https://www.razi.pro/tools/vocal-separator) will separate a mix into parts.
