Flag and remove music in a live-event recording
Find where copyrighted music plays in a long broadcast or livestream recording, flag each segment in a report, and produce a cleaned copy with those segments muted or cut.
A recording of a live event — a sports broadcast, a radio show, a livestream VOD, a conference session — is mostly speech, crowd, and commentary, with stretches of copyrighted music threaded through it: walk-on music, stingers, background beds, a DJ segment. This recipe finds exactly where the music plays and what it is, flags each segment in a report, and then builds a cleaned copy of the recording with those segments muted or cut.
What you’ll build
Two outputs from one recognition pass:
- A flag report — one row per music segment, each with a file-absolute start/end time and the track that plays there (artist, title, label, ISRC, links). This is what you hand to a reviewer, a rights team, or a compliance log.
- A cleaned recording — the same file with every flagged segment either
muted (audio zeroed, video untouched) or cut out entirely, built with
ffmpegfrom the timestamps in the report.
The enterprise endpoint is the right tool: it handles arbitrary-length
recordings and finds every track rather than one. Both the report rows and
the ffmpeg edits are driven by file positions — where in the recording each
music hit sits. Each match carries those as start_seconds and end_seconds,
the song’s position in your file, and recognize_enterprise requests accurate
offsets by default, so they’re precise.
AudD identifies the music; the policy is yours. This recipe locates and names recordings and edits your file. Whether a given segment actually needs muting or cutting — licensing, fair use, your platform’s rules — is a decision you make on top of the report, not something the API decides.
Prerequisites
- An API token from dashboard.audd.io. Get your
own token there; the enterprise endpoint must be enabled on it.
label,isrc, andupccome back on enterprise responses (ISRC/UPC require a Startup plan or higher). - Python 3.10+ with the SDK:
pip install audd ffmpeg(andffprobe) on yourPATHfor the cleaning step.- The recording as a URL or a local file.
https://audd.tech/example.mp3is a public, reproducible file you can run against end-to-end.
Walkthrough
Step 1: Recognize the recording
Send the recording to recognize_enterprise as a URL, file bytes, or a path.
A multi-hour broadcast comes back as a flat list[EnterpriseMatch] — one
entry per recognized fragment, in time order — and each entry has a precise
start_seconds / end_seconds, since accurate offsets are requested by
default.
from audd import AudD
audd = AudD("your-api-token") # token from dashboard.audd.io
# limit caps metered fragments while you get the report and the edit right
matches = audd.recognize_enterprise(
"https://audd.tech/example.mp3",
limit=25,
return_metadata=["apple_music", "spotify"], # optional streaming links per match
)
for m in matches:
print(f"{m.start_seconds:.1f}s {m.artist} — {m.title} (score {m.score})")
Each printed line is one recognized fragment, in time order. A music bed that runs for a minute appears as several consecutive matches naming the same track (Step 3 merges them into one segment). Stretches that are pure speech or crowd noise produce no match at all — that’s a gap, not an error, and exactly what you want: those stretches stay untouched.
Always set
limitduring development. The enterprise endpoint bills per 12 seconds of audio processed. A two-hour broadcast is hundreds of fragments; an unbounded call meters all of them. Start withlimit=25, get the report and the edit right, then raise the cap for the full recording.
Step 2: File positions, not song positions
The mute filter and the cut math both consume file positions, and
start_seconds / end_seconds are exactly that: where this song plays in
your file, in seconds (e.g. 64.2 to 71.6). Feed them straight to a player
or ffmpeg. They’re None only when a fragment arrived without a usable
position.
Don’t reach for start_offset / end_offset — those are the raw offsets the
seconds are derived from, milliseconds within AudD’s internal 12-second scan
fragment (0–~12000), not file seconds. And don’t use timecode as a file
position: it’s the position inside the matched recording, telling you which
part of the song was playing, not where in your file it plays.
Step 3: Build music segments from the matches
The report and the edit both want one segment per continuous play — “this
music runs from 64.2s to 96.0s” — not a row per fragment. Collapse consecutive
matches naming the same (artist, title) into one segment spanning from the
first match’s start_seconds to the last match’s end_seconds.
def build_segments(matches):
"""Collapse consecutive same-track matches into one segment with a time range."""
segments = []
for m in matches:
if m.start_seconds is None:
continue # no usable position for this match — skip it
end = m.end_seconds if m.end_seconds is not None else m.start_seconds + 12
extending = (
segments
and segments[-1]["artist"] == m.artist
and segments[-1]["title"] == m.title
)
if extending:
segments[-1]["end"] = max(segments[-1]["end"], end) # still playing
continue
segments.append({
"artist": m.artist,
"title": m.title,
"album": m.album,
"label": m.label,
"isrc": m.isrc,
"upc": m.upc,
"song_link": m.song_link,
"score": m.score,
"start": m.start_seconds,
"end": end,
})
return segments
A 30-second stinger becomes one segment with a real start and end. Because the
SDK parses responses leniently, any field can be absent or None; the
start_seconds guard and the end_seconds fallback cover the fragment that
arrives without a position.
Step 4: Format positions into HH:MM:SS
A report wants clock times, not raw seconds. Format the file-absolute positions:
def fmt_hms(total_seconds: float) -> str:
s = int(total_seconds)
h, rem = divmod(s, 3600)
m, sec = divmod(rem, 60)
return f"{h:02d}:{m:02d}:{sec:02d}"
print(fmt_hms(64.2)) # -> "00:01:04"
This always gives a zero-padded HH:MM:SS, which reads cleanly for a recording
that runs past an hour.
Step 5: Render the flag report
Turn the segments into a report a reviewer can act on — both as structured rows (for a compliance log or a downstream tool) and as a readable summary.
def flag_report(matches, source: str) -> list[dict]:
rows = []
for seg in build_segments(matches):
rows.append({
"start_seconds": round(seg["start"], 2),
"end_seconds": round(seg["end"], 2),
"start": fmt_hms(seg["start"]),
"end": fmt_hms(seg["end"]),
"artist": seg["artist"],
"title": seg["title"],
"label": seg["label"],
"isrc": seg["isrc"],
"song_link": seg["song_link"],
"score": seg["score"],
"source": source,
})
return rows
def render_report(rows: list[dict]) -> str:
lines = [f"Music segments flagged: {len(rows)}", ""]
for i, r in enumerate(rows, 1):
lines += [
f"[{i}] {r['start']}–{r['end']} {r['artist']} — {r['title']}",
f" label: {r['label']} isrc: {r['isrc']}",
f" link: {r['song_link']} score: {r['score']}",
"",
]
return "\n".join(lines)
rows = flag_report(matches, "https://audd.tech/example.mp3")
print(render_report(rows))
Each row names the recording and locates it by a file-absolute span you can seek straight to. An empty report means the scan recognized no music in the covered range — a clean result, not an error.
Step 6: Mute the flagged segments
If you want to keep the recording’s full length and timeline (so existing
chapter marks, subtitles, and clip references still line up), mute the audio
over each flagged span and leave everything else intact. ffmpeg’s volume
filter with an enable expression zeroes the audio only between the start and
end of each segment:
import subprocess
def build_mute_filter(rows: list[dict]) -> str:
"""One volume filter that drops audio to 0 across every flagged span."""
clauses = [
f"volume=enable='between(t,{r['start_seconds']},{r['end_seconds']})':volume=0"
for r in rows
]
return ",".join(clauses)
def mute_segments(input_path: str, output_path: str, rows: list[dict]) -> None:
if not rows:
return # nothing flagged — leave the file as-is
af = build_mute_filter(rows)
subprocess.run(
[
"ffmpeg", "-y", "-i", input_path,
"-af", af,
"-c:v", "copy", # video untouched; only the audio is re-encoded
output_path,
],
check=True,
)
mute_segments("event.mp4", "event-muted.mp4", rows)
Chaining one volume=...:volume=0 clause per segment produces a single audio
filter that silences each flagged span and passes the rest through. -c:v copy
leaves the video stream byte-for-byte; only the audio is re-encoded. The output
has the same duration and timeline as the source — the music is just gone.
Step 7: Or cut the flagged segments out
If instead you want the music removed — a shorter recording with the music segments excised — keep the spans that are not flagged and concatenate them. Invert the flagged spans against the file duration, then cut and join the keep-spans:
def keep_spans(rows: list[dict], duration: float) -> list[tuple[float, float]]:
"""Invert flagged spans into the spans to keep, across [0, duration]."""
flagged = sorted((r["start_seconds"], r["end_seconds"]) for r in rows)
spans, cursor = [], 0.0
for start, end in flagged:
if start > cursor:
spans.append((cursor, start)) # speech/crowd before this music
cursor = max(cursor, end)
if cursor < duration:
spans.append((cursor, duration)) # tail after the last music
return spans
def media_duration(input_path: str) -> float:
out = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=nokey=1:noprint_wrappers=1", input_path],
check=True, capture_output=True, text=True,
)
return float(out.stdout.strip())
def cut_segments(input_path: str, output_path: str, rows: list[dict]) -> None:
if not rows:
return
spans = keep_spans(rows, media_duration(input_path))
# Build one trimmed video+audio pair per keep-span, then concat them.
parts, filters = [], []
for i, (start, end) in enumerate(spans):
filters.append(
f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}];"
f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]"
)
parts.append(f"[v{i}][a{i}]")
concat = f"{''.join(parts)}concat=n={len(spans)}:v=1:a=1[v][a]"
filtergraph = ";".join(filters + [concat])
subprocess.run(
[
"ffmpeg", "-y", "-i", input_path,
"-filter_complex", filtergraph,
"-map", "[v]", "-map", "[a]",
output_path,
],
check=True,
)
cut_segments("event.mp4", "event-cut.mp4", rows)
This trims each keep-span out of the source and concatenates them, dropping the
music entirely. The result is shorter than the source by the total duration of
the flagged segments. (For an audio-only recording, drop the [0:v]/v=1
halves and keep just the atrim/audio concat.)
Muting preserves the timeline; cutting changes it. Mute when other assets (captions, chapter marks, highlight clips) reference timestamps in the original — their positions still line up. Cut when you want the music gone and a shorter file, and you’re willing to re-derive any timestamped assets against the new timeline.
Step 8: Run it end to end
def process(source: str, input_path: str, mode: str = "mute") -> list[dict]:
matches = audd.recognize_enterprise(source, limit=50,
return_metadata=["apple_music", "spotify"])
rows = flag_report(matches, source)
print(render_report(rows))
if mode == "mute":
mute_segments(input_path, "event-cleaned.mp4", rows)
elif mode == "cut":
cut_segments(input_path, "event-cleaned.mp4", rows)
return rows
rows = process("https://audd.tech/example.mp3", "event.mp4", mode="mute")
source is what AudD recognizes (URL or uploaded bytes); input_path is the
local file you edit. They’re often the same recording — recognize a hosted copy
or upload the bytes, then apply the resulting spans to your local master.
What you get back
One EnterpriseMatch per recognized fragment, in time order. The fields this
recipe reads:
| Field | Type | Role here |
|---|---|---|
start_seconds, end_seconds | float | None | Where this song plays in your file, in seconds. The values you flag, mute, and cut on. None only when a fragment had no usable position. |
artist, title | str | None | The recording, named. |
album, release_date | str | None | The release it appears on, and when. |
label | str | None | The releasing label. |
isrc, upc | str | None | Recording/release identifiers (Startup plan or higher). |
song_link | str | None | lis.tn universal link to the track. |
score | int | None | Match confidence for the fragment. |
start_offset, end_offset | int | None | Raw milliseconds within the 12-second scan fragment that start_seconds/end_seconds are derived from. Rarely needed directly. |
timecode | str | None | Position inside the matched recording, not your file. Never use it to locate a segment. |
For the example file, a match might come back with start_seconds = 64.2 and
end_seconds = 71.6 — the span that feeds both the
volume=enable='between(t,64.2,71.6)':volume=0 mute filter and the cut/keep
math.
Handling errors
The SDK raises typed exceptions; catch the ones you can act on.
- Authentication errors (
AudDAuthenticationError) — bad or missing token. Fail at startup. - Quota / subscription errors (
AudDSubscriptionError) — request limit hit, or the enterprise endpoint isn’t enabled on your token. The enterprise endpoint and ISRC/UPC both depend on plan tier; surface these to the account owner rather than retrying. - Invalid-audio errors — the URL or file wasn’t decodable audio/video. Treat it as a hard failure for that source, not a silent empty report.
- Connection errors (
AudDConnectionError) — transient. Retry with backoff. ffmpeg/ffprobefailures — a non-zero exit raisessubprocess.CalledProcessError. A bad input path or codec issue surfaces here, separate from recognition; don’t conflate it with an empty report.
from audd.errors import AudDError
try:
rows = process(source, input_path, mode="mute")
except AudDError as e:
# any AudD API/transport failure — log and decide; retry connection errors
print(f"Recognition failed: {e}")
rows = []
An empty report (no flagged segments) is a valid, distinct outcome from an error — the covered range recognized no music, so there’s nothing to mute or cut, and the cleaning steps no-op.
Going further
- Re-running recognition re-meters every fragment. Run the scan once per recording and persist the report; rebuild the muted or cut file from the stored spans rather than re-scanning.
- For a multi-hour broadcast where you only need a rough map of where music
sits, sample with
every/skipto trade coverage for metered fragments — see Enterprise cost optimization. - If your goal is a takedown filing rather than an in-place edit, the same
start_seconds/end_secondsfeed an evidence pack — see Generate DMCA takedown evidence. - To scan files as they’re uploaded rather than after the fact, see Build a copyright scanner for user-uploaded content.
Related
Reading this as an AI agent? The raw Markdown is at recipes/flag-music-in-live-recording.md, and the full index is /resources/llms.txt.
