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.
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
scorerequire a Startup plan or higher. - Python 3.10+ with
pip install audd, andffprobe(part offfmpeg) 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:
| 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
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
| 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. |
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.AudDConnectionErroron 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.
