Skip to content

Instantly share code, notes, and snippets.

@leleogere
Forked from gepron1x/beatfinder.py
Last active January 26, 2026 14:03
Show Gist options
  • Select an option

  • Save leleogere/0b21605a3512e8f6abd49ce39a7d6dc3 to your computer and use it in GitHub Desktop.

Select an option

Save leleogere/0b21605a3512e8f6abd49ce39a7d6dc3 to your computer and use it in GitHub Desktop.
Kdenlive beat sync
import argparse
import datetime
import librosa
def parse_delta(s):
date = datetime.datetime.strptime(s, "%H:%M:%S")
return datetime.timedelta(
hours=date.hour, minutes=date.minute, seconds=date.second
)
def main():
parser = argparse.ArgumentParser(
prog='Kdenlive beatfinder',
description='Creates timeline guides matching the song beat'
)
parser.add_argument('input_file',
help="Audio file to analyze for beat detection")
parser.add_argument('--output_file', default="beats.txt",
help="Output text file for timeline guides (default: beats.txt)")
parser.add_argument('--bpm', type=float, default=None,
help="Force a specific tempo in beats per minute")
parser.add_argument('--offset', default="0:00:0",
help="Time offset added to all timestamps (H:MM:SS)")
parser.add_argument('--tightness', type=float, default=100,
help="Beat rigidity; higher values enforce a steadier tempo")
parser.add_argument('--beats-per-bar', type=int, default=None,
help="Beats per bar; label guide as <bar>:<beat> (e.g. 2:3 for 2nd bar, 3rd beat)")
parser.add_argument('--first-bar-offset', type=int, default=0,
help="Number of pickup beats before bar 1 (labels as 0:y)")
args = parser.parse_args()
offset = parse_delta(args.offset)
y, sr = librosa.load(args.input_file)
onset_envelope = librosa.onset.onset_strength(y=y, sr=sr)
tempo, beat_frames = librosa.beat.beat_track(
onset_envelope=onset_envelope, bpm=args.bpm, tightness=args.tightness
)
beat_times = librosa.frames_to_time(beat_frames, sr=sr)
print(f"{args.input_file} BPM: {tempo[0]:.2f}")
print(f"Writing timeline guides to {args.output_file}...")
with open(args.output_file, "wt") as f:
for i, seconds in enumerate(beat_times):
if args.beats_per_bar is None:
label = str(i)
else:
if i < args.first_bar_offset:
bar = 0
beat_in_bar = i + 1
else:
j = i - args.first_bar_offset
bar = int((j // args.beats_per_bar) + 1)
beat_in_bar = int((j % args.beats_per_bar) + 1)
label = f"{bar}:{beat_in_bar}"
timestamp = datetime.timedelta(seconds=seconds) + offset
f.write(f"{timestamp} {label}\n")
if __name__ == '__main__':
main()
@leleogere
Copy link
Author

This fork adds support for labelling bars in guides.

The --beat-per-bar specify the number of beat in each bar (must be constant). When enabled, this option will label the guides as bar:beat. For example, --beat-per-bar 4 will create measures of 4 beats, in which 13:3 would label for the third beat of the thirteenth bar

The option --first-bar-offset allows skipping the first beats (labelled as 0:x) in order to align the guides with the first bar of the song. For example, --first-bar-offset 5 will mark the first 5 beats as 0:1 to 0:5, and start the first measure on beat 6 (labelled as 1:1).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment