Recipe

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

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.

view .md auddmusic cue sheetcue listpost-production

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:

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; for a DJ mix with blended transitions, see Turn a DJ set or mix into a 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. 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.

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:

RecordingChunks (requests)
22-minute TV episode110
45-minute fitness class225
2-hour feature film or broadcast block600

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 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.

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

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 [email protected] to request it. Uploads are metered and must not be retried automatically, since a retry after an upload that landed is charged again.

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:

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

UseWhat to watch for
TV and film post-productionScan 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 classesA 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.
EventsPlayback 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.
BroadcastsFor 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.

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

Reading this as an AI agent? The raw Markdown is at recipes/music-cue-sheet.md, and the full index is /resources/llms.txt.