---
title: "Generate a music cue sheet from a video, broadcast, or class recording"
description: "Scan a finished video, broadcast, or class recording with the AudD enterprise endpoint and turn the matches into a cue list CSV: title, artist, album, label, ISRC/UPC, in and out times, and duration, ready for your licensing and reporting workflow."
slug: "/resources/recipes/music-cue-sheet"
section: "recipes"
keywords: [audd, music cue sheet, cue list, post-production, broadcast, fitness class music, isrc, enterprise endpoint]
---

# Generate a music cue sheet from a video, broadcast, or class recording

A cue sheet lists every piece of music in a production with where it starts,
where it ends, and how long it runs. Post-production teams deliver one with a
TV episode or a film; broadcasters keep as-aired logs; fitness studios and
event producers need to know which tracks a recorded class or event used. If
nobody logged the music while editing, you can rebuild the list from the
finished file. This recipe scans the file with AudD's enterprise endpoint
and writes a cue list CSV.

What you get is a starting point. AudD identifies the recording: title,
artist, album, label, and (on a Startup plan or higher) ISRC and UPC. It
does not return composers, publishers, ISWCs, or PRO shares, and it doesn't
file anything with a PRO or broadcaster. Cue sheet formats differ by PRO and
by broadcaster, so the CSV leaves those columns for you to fill in from your
licensing records.

## What you'll build

A script that takes one file and writes `cue-sheet.csv`:

```text
cue,title,artist,album,label,isrc,upc,in,out,duration,usage,composer,publisher,pro,source,notes
1,Everybody Wants To Rule The World,Tears For Fears,Songs From The Big Chair,UMC (Universal Music Catalogue),GBUM71403885,00602547037169,00:00:00,00:00:57,0:57,,,,,public,
```

One row per continuous use of a track. If the same song plays in the cold
open and again over the end credits, that is two cues, as cue sheets expect.

If you need show notes for a podcast, see
[Generate music credits for a podcast episode](/resources/recipes/podcast-music-credits);
for a DJ mix with blended transitions, see
[Turn a DJ set or mix into a tracklist](/resources/recipes/dj-set-tracklist).
This page is about the cue list: in and out times, durations, identifiers,
and the columns a rights workflow needs.

## Prerequisites

- An API token. Get your own token at
  [dashboard.audd.io](https://dashboard.audd.io). ISRC, UPC, and `score`
  require a Startup plan or higher.
- Python 3.10+ with `pip install audd`, and `ffprobe` (part of `ffmpeg`) to
  read the file's duration.
- The finished file: a video, an audio export, or a URL. Use the final mix;
  a music-only stem matches more cues if you have one (see Step 5).

## Step 1: Scan the whole file

A cue sheet needs every cue, so scan every 12-second chunk. Don't sample
with `skip`: a 15-second sting between probes would never be found. Set
`limit` to the number of chunks in the file, so the call covers the whole
file and nothing more.

```python
import math
import subprocess
from audd import AudD

audd = AudD("your-api-token")  # get your own token at dashboard.audd.io

def duration_seconds(path: str) -> float:
    out = subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration",
         "-of", "default=nw=1:nk=1", path],
        capture_output=True, text=True, check=True,
    ).stdout
    return float(out.strip())

path = "episode-104-final.mp4"
chunks = math.ceil(duration_seconds(path) / 12)
matches = audd.recognize_enterprise(path, limit=chunks)
print(f"{chunks} chunks scanned, {len(matches)} matches")
```

The enterprise endpoint is billed one request per 12 seconds of audio
processed, so the cost is set by the running time:

| Recording | Chunks (requests) |
|---|---|
| 22-minute TV episode | 110 |
| 45-minute fitness class | 225 |
| 2-hour feature film or broadcast block | 600 |

Each match has `start_seconds` and `end_seconds`: where the match sits in
your file, in seconds. The SDK computes them from the chunk's position plus
the offset inside the chunk. If you call the HTTP API yourself, the
[enterprise fields reference](/resources/reference/enterprise-match-fields)
has the formula.

## Step 2: Collapse matches into cues

A song that plays for two minutes comes back as about ten matches, one per
chunk. Merge consecutive matches of the same recording into one cue. Allow
a gap of one chunk, because a quiet passage or a line of dialogue over the
music can make a single chunk miss. A different recording starting ends
nothing by itself: two songs can overlap in a crossfade, and each keeps its
own cue.

```python
GAP = 24  # seconds: tolerate one missed 12-second chunk inside a cue

def recording_key(m) -> str:
    audio_id = getattr(m, "audio_id", None)
    if audio_id is not None:
        return f"custom:{audio_id}"
    return (m.isrc or "").upper() or f"{m.artist}|{m.title}".casefold()

def build_cues(matches) -> list[dict]:
    cues: list[dict] = []
    open_cue: dict[str, dict] = {}          # recording key -> its latest cue
    timed = sorted((m for m in matches if m.start_seconds is not None),
                   key=lambda m: m.start_seconds)
    for m in timed:
        key = recording_key(m)
        end = m.end_seconds if m.end_seconds is not None else m.start_seconds + 12
        cue = open_cue.get(key)
        if cue and m.start_seconds - cue["out"] <= GAP:
            cue["out"] = max(cue["out"], end)
            continue
        cue = {
            "title": m.title or "", "artist": m.artist or "",
            "album": m.album or "", "label": m.label or "",
            "isrc": m.isrc or "", "upc": m.upc or "",
            "in": m.start_seconds, "out": end,
            "source": key if key.startswith("custom:") else "public",
        }
        cues.append(cue)
        open_cue[key] = cue
    cues.sort(key=lambda c: c["in"])
    return cues
```

The key prefers the ISRC, so two matches with slightly different title
strings for the same recording still merge. When there's no ISRC (on plans
below Startup, or for a custom match) it falls back to artist and title, or
to the custom catalog's `audio_id`.

## Step 3: Format times and write the CSV

```python
import csv

FIELDS = ["cue", "title", "artist", "album", "label", "isrc", "upc", "in",
          "out", "duration", "usage", "composer", "publisher", "pro",
          "source", "notes"]

def hms(seconds: float) -> str:
    s = round(seconds)
    return f"{s // 3600:02d}:{s % 3600 // 60:02d}:{s % 60:02d}"

def mmss(seconds: float) -> str:
    s = round(seconds)
    return f"{s // 60}:{s % 60:02d}"

def write_cue_sheet(cues: list[dict], out_path: str = "cue-sheet.csv") -> None:
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=FIELDS, restval="")
        w.writeheader()
        for n, c in enumerate(cues, start=1):
            w.writerow({
                **{k: c[k] for k in ("title", "artist", "album", "label",
                                     "isrc", "upc", "source")},
                "cue": n,
                "in": hms(c["in"]),
                "out": hms(c["out"]),
                "duration": mmss(c["out"] - c["in"]),
            })

write_cue_sheet(build_cues(matches))
```

`usage`, `composer`, `publisher`, and `pro` stay empty. Usage (background,
visual vocal, theme, and so on) depends on how the music sits in the
picture, which audio recognition can't see. Composer, publisher, and PRO
affiliation come from your licence paperwork or the music supervisor.

A cue sheet in seconds is fine for most workflows. If yours needs frames,
convert from seconds at your project frame rate after the fact.

## Step 4: How precise the times are

Times are approximate, and how approximate depends on one setting.

**Without accurate offsets**, a match only tells you which 12-second chunk
it came from. In and out times snap to chunk boundaries, so each edge can be
off by up to 12 seconds, and a duration by up to about 24.

**With accurate offsets** (`accurate_offsets=true` on the HTTP API), each
match also carries `start_offset` and `end_offset`: where inside the chunk,
in milliseconds, the matched audio starts and ends. In and out times then
land inside the chunk instead of on its boundary. The Python SDK turns this
on by default, and so does the CLI; if you call the HTTP API directly, send
the parameter yourself.

Even with accurate offsets, check the result against the edit before you
deliver it:

- A fade-in, a fade-out, or a tail buried under dialogue may not match, so
  the cue can look a few seconds shorter than it plays.
- A cue cut into pieces in the edit shows up as separate cues if a gap is
  longer than `GAP`. That is often correct for a cue sheet, which counts
  each use separately.
- Round durations the way your PRO or broadcaster asks; some want whole
  seconds, some want rounding up.

If you have the edit decision list, use it for the final in and out points
and use AudD's list to find and identify the cues.

## Step 5: Library music and unreleased tracks

AudD's public database covers about 160 million commercially released
songs. Production library music, a score written for the show, and tracks
not yet released may not be in it, and those cues come back as no match.

If you have the audio files for that music (the library tracks you
licensed, the composer's stems), you can add them to a **custom catalog**:
a private fingerprint database for your account. A later scan then returns
those cues with an `audio_id` you assigned, which your own table maps to
the library, composer, and publisher. Custom catalog access is not enabled
by default; email api@audd.io to request it. Uploads are metered and must
not be retried automatically, since a retry after an upload that landed is
charged again.

```python
LIBRARY = {  # audio_id -> your own cue metadata
    5001: {"title": "Night Drive (Underscore)", "composer": "A. Rivera",
           "publisher": "Example Library Music"},
}

audd.custom_catalog.add(5001, "library/night-drive.wav")   # once per track, no retry
```

In `write_cue_sheet`, look up rows whose `source` is `custom:<id>` in
`LIBRARY` and fill in title, composer, and publisher. A custom match can
have empty `artist` and `title` fields, so your table is the source of
truth for those cues.

A music-only stem, if the mix has one, gives cleaner matches than the full
mix, where dialogue and effects sit on top of the music. Times carry over
as long as the stem starts at the same point as the program.

## The CLI way

For a one-off cue list, the AudD CLI does the scan and the merge:

```bash
audd recognize episode-104-final.mp4 --enterprise --limit 110 --dry-run
audd recognize episode-104-final.mp4 --enterprise --limit 110 --tracklist \
  --format csv > cues.csv
```

`--limit` is the number of 12-second chunks (110 for 22 minutes);
`--dry-run` shows the cost without sending anything. `--tracklist` merges
consecutive matches into tracks, and the CSV has `artist`, `title`,
`album`, `label`, `isrc`, `upc`, `start`, `end`, `start_seconds`, and
`end_seconds` per track. Compute duration as `end_seconds - start_seconds`
in your spreadsheet. Accurate offsets are on by default.

## By use

| Use | What to watch for |
|---|---|
| TV and film post-production | Scan the locked cut. Take final in and out points from the EDL; use the scan to catch cues nobody logged and to fill identifiers. |
| Fitness classes | A class recording is mostly continuous music, so expect a cue every few minutes. Instructors often use remixes and workout edits, which match only if that version is a released recording in the database. |
| Events | Playback from a DJ or a playlist matches. A live band playing someone else's song does not: AudD matches recordings, not compositions, and doesn't detect covers. |
| Broadcasts | For a recorded block, this recipe works as-is. For a channel you want logged continuously, monitor the live stream instead: see [Monitor radio airplay for your music catalog](/resources/recipes/radio-airplay-monitor). |

## Handling errors

- `AudDAuthenticationError`: bad token. Stop and fix it.
- `AudDQuotaError`, `AudDSubscriptionError`: out of requests, or a feature
  your plan doesn't include. Fix the account; retrying won't help.
- `AudDCustomCatalogAccessError`: custom catalog access isn't enabled yet.
- `AudDInvalidAudioError`: the file couldn't be decoded. Export the audio
  to WAV or MP3 and try again.
- `AudDConnectionError` on a scan: safe to retry. Cancelling a request
  mid-flight might still consume requests, and a retried scan is billed
  again, so save the matches as soon as they arrive.

---

**Related**

- [Generate music credits for a podcast episode](/resources/recipes/podcast-music-credits)
- [Turn a DJ set or mix into a tracklist](/resources/recipes/dj-set-tracklist)
- [Enterprise response and match fields](/resources/reference/enterprise-match-fields)
- [Public database vs your custom catalog](/resources/concepts/custom-vs-public-db)
- [AudD for PROs and rights organizations](/resources/for/pros-and-rights-orgs)
- [Controlling enterprise endpoint cost](/resources/concepts/enterprise-cost-control)

## Sitemap

See the [resources sitemap](https://audd.io/resources/sitemap.md) for every guide and the [glossary](https://audd.io/resources/reference/glossary.md) for AudD terms.
