Skip to main content

beatfit

Retime a video clip to fill part of a song so that chosen moments in it land on beats.

You give it the moments (as source frames), the song's beat times and the stretch of the song the clip has to fill. It returns the clip as a list of constant-speed pieces.

Footage plays in real time unless the gap between two moments is longer than 5.5 s. Then the middle of the gap is sped up to at most 1.5x, with short eased ramps and about a second of real time either side of each moment. The lead-in is picked so the first moment falls on a beat, a downbeat if possible, and each sped-up gap gets whatever length puts the next moment on a beat. If a gap is still too long, its sped-up middle gets a jump cut. With the default settings that happens to every gap long enough to speed up; raise max_gap_out to speed up without cutting. Ranges you mark as protected are never sped up or cut.

The result fills the requested number of frames exactly and never plays slower than real time or faster than max_speed.

beatfit doesn't analyse audio. Use a beat tracker such as beat_this, All-In-One or librosa for beats and downbeats. It also doesn't decide which clip goes where in the song; fit_clip returns None when a clip can't fill a slot, so you can try clips against slots and keep the ones that fit.

Pure Python with no dependencies, 3.10 or newer.

Example

from beatfit import Clip, Event, Params, fit_clip

fps = 60
beats = [0.5 + 0.5 * k for k in range(200)]  # 120 BPM, from your beat tracker (seconds)
downbeats = beats[::4]

# moments at 12.0 s, 13.1 s and 31.5 s of the recording (source frames at `fps`)
clip = Clip(
    events=[Event(12 * fps), Event(round(13.1 * fps)), Event(round(31.5 * fps), protect=(4.0, 1.0))],
    src_start=0,             # usable footage
    src_end=40 * fps,
)
start = round(downbeats[4] * fps)          # fill bars 4-12 of the song
frames = round(downbeats[12] * fps) - start
fit = fit_clip(clip, start, frames, beats, downbeats, Params(fps=fps))

for p in fit.pieces:  # constant-speed pieces: source frames -> output frames
    print(p.src, p.src_frames, p.out_frames, round(p.speed, 3), "jump" if p.jump else "")
print(fit.event_out, fit.event_on_beat, fit.event_err_ms)

To render it with ffmpeg (video only):

import subprocess
from beatfit.ffmpeg import command

subprocess.run(command("recording.mp4", fit.pieces, fps, "clip.mp4"), check=True)

beatfit.ffmpeg.filter_graph and input_args return the parts of that command, for use in a larger filter graph.

Concepts

All times are integer frames at Params.fps (default 60) on two timelines: source (the recording) and output (the song). Beats are in seconds on the output timeline. A source frame is 1 / fps seconds whatever the recording's own frame rate, so a 30 fps recording works with fps=60.

A Clip has events in increasing source order and the usable source range [src_start, src_end). Optional fields:

Field Meaning
lead_from Start the lead-in here, e.g. where the tension begins. A long wait is sped up after a moment of real time.
lead Lead(pref_min, pref_max, max_realtime, hold): preferred lead-in length in seconds, the longest lead-in played entirely in real time, and the real time kept before the first event when the lead-in is longer than that.
tail_min Frames of real time after the last event, e.g. for a reaction. The tail then ends on a bar line, up to tail_slack_bars later.
protect Source ranges [start, end) that always play in real time.

Event.protect = (before, after) protects that many seconds around one event. Event.soft marks an event whose timing is approximate, so putting it on a beat is optional.

A protected range between two events is always shown in real time, and the dead time on either side of it can still be sped up or cut. A range around the first event is shown from its start: the clip never begins inside it, and the lead-in stays in real time as long as the range needs. A range earlier than that is shown only if the lead-in reaches back to it. A clip can end before a range around its last event is over.

Params holds the other settings: real time around events (pre_hold, post_hold), the speed cap (max_speed), the shortest gap that gets sped up (speed_gap), the longest sped-up stretch before a jump cut (max_gap_out), lead-in and tail limits, and beats_per_bar. last=True allows a longer tail for the final clip of a song, and music_end penalises events that would land after the music stops.

A Fit has the pieces; each event's output frame relative to the slot (event_out); whether it is on a beat or downbeat and how far it is from the nearest beat (event_err_ms); and max_speed, jumps, lead_out, tail_out and cost. locate and source_at convert between the two timelines.

How it works

A dynamic program over (event, landing frame), where landing frames are the beats inside the slot. Gaps shorter than speed_gap play in real time, so the event after one lands wherever that puts it; the solver favours lead-ins that bring those events close to a beat. A speed-up is up to three eased steps on each side of a constant plateau, so renderers and editors only ever see constant speeds.

Limits

  • It fits one clip into one slot. Choosing and ordering clips is up to you.
  • Times are whole frames, so an event lands within half a frame of its beat (8 ms at 60 fps).
  • The ffmpeg helper renders video only. Retiming or muting the audio is up to you.
  • It was written for game highlight montages at 60 fps. Other footage and tempos have only been tested with synthetic data.
  • visbeat (Davis & Agrawala, "Visual Rhythm and Beat", SIGGRAPH 2018) warps video so visible motion lines up with musical beats. beatfit takes the moments as given and keeps real time around them.
  • mugen and similar tools cut video to the beat. They put cuts on beats rather than moving moments inside a clip.
  • otio-music-arrangement builds beat-aligned OpenTimelineIO timelines from placeholder clips.

License

MIT

Metadata

Release files for beatfit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for beatfit 0.1.0
File Size Uploaded Provenance Actions
beatfit-0.1.0.tar.gz 27.8 kB Without provenance Details for beatfit-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for beatfit 0.1.0
File Interpreter ABI Platform Actions
beatfit-0.1.0-py3-none-any.whl Python 3 none any Details for beatfit-0.1.0-py3-none-any.whl

Total release size: 43.6 kB

Release files / beatfit-0.1.0.tar.gz

Download URL beatfit-0.1.0.tar.gz
Size 27.8 kB
Tags Source
SHA-256 checksum
How to use checksums
7277fb6972eb597e9861a6466a039b7f8827d05e47be12d70397685a7ab3a3e6
BLAKE2b-256 checksum
How to use checksums
f7042a1fedda672a09ac3ba0e785d402f474ab2900ce4b36f4df6c489bfc29f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / beatfit-0.1.0-py3-none-any.whl

Download URL beatfit-0.1.0-py3-none-any.whl
Size 15.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b2d7c623e84b59e34c449932e2438eeaf30581630b1cfff6141f2ceccc48aad8
BLAKE2b-256 checksum
How to use checksums
760b0d82483c99c230c5c5fe0136a4c0632741dbb293df4bf4d5df59fd316f65
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for beatfit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page