You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
4.1 KiB
173 lines
4.1 KiB
#!/usr/bin/env python
|
|
|
|
#
|
|
# bbb-to-edl.py
|
|
#
|
|
# BBB EDL generator
|
|
# Creates an EDL files from a BBB recording shapes.svg
|
|
#
|
|
# Copyright (c) 2023 Sylvain Munaut <tnt@246tNt.com>
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
|
|
import argparse
|
|
import math
|
|
|
|
import bbb
|
|
|
|
|
|
class TimeCode:
|
|
|
|
def __init__(self, framerate, frames):
|
|
self._rate = framerate
|
|
self._aframes = frames
|
|
|
|
@property
|
|
def hours(self):
|
|
return self._aframes // (self._rate * 3600)
|
|
|
|
@property
|
|
def minutes(self):
|
|
tm = self._aframes // (self._rate * 60)
|
|
return tm % 60
|
|
|
|
@property
|
|
def seconds(self):
|
|
ts = self._aframes // self._rate
|
|
return ts % 60
|
|
|
|
@property
|
|
def frames(self):
|
|
return self._aframes % self._rate
|
|
|
|
@property
|
|
def framerate(self):
|
|
return self._rate
|
|
|
|
def __str__(self):
|
|
return f"{self.hours:02d}:{self.minutes:02d}:{self.seconds:02d}:{self.frames:02d}"
|
|
|
|
def __int__(self):
|
|
return self._aframes
|
|
|
|
def __add__(self, other):
|
|
if isinstance(other, int):
|
|
fo = other
|
|
elif isinstance(other, TimeCode):
|
|
if other._rate != self._rate:
|
|
raise ValueError("Can't combine timecode of different framerate")
|
|
fo = other._aframes
|
|
return TimeCode(self._rate, self._aframes + fo)
|
|
|
|
def __sub__(self, other):
|
|
if isinstance(other, int):
|
|
fo = other
|
|
elif isinstance(other, TimeCode):
|
|
if other._rate != self._rate:
|
|
raise ValueError("Can't combine timecode of different framerate")
|
|
fo = other._aframes
|
|
return TimeCode(self._rate, self._aframes - fo)
|
|
|
|
@classmethod
|
|
def parse(self, framerate, s):
|
|
# HH:MM:SS:FF
|
|
m = re.match('^([0-9]{2}):([0-9]{2}):([0-9]{2}):([0-9]{2})$', s)
|
|
if m:
|
|
h = int(m.group(1))
|
|
m = int(m.group(2))
|
|
s = int(m.group(3))
|
|
f = int(m.group(4))
|
|
return TimeCode(framerate, ((((h*60)+m)*60)+s)*framerate+f)
|
|
|
|
# [[HH:]MM:]SS.s
|
|
m = re.match('^((([0-9]{1,2}):)?([0-9]{1,2}):)?([0-9]{1,2}(.[0-9]+)?)$', s)
|
|
if m:
|
|
h = int(m.group(3))
|
|
m = int(m.group(4))
|
|
s = float(m.group(5))
|
|
f = int(round((((h*60)+m)*60+s)*framerate))
|
|
return TimeCode(framerate, f)
|
|
|
|
# SSS.s
|
|
m = re.match('^[0-9]+(.[0-9]+)?)$', s)
|
|
if m:
|
|
s = float(s)
|
|
f = int(round(s*framerate))
|
|
return TimeCode(framerate, f)
|
|
|
|
# No match
|
|
raise ValueError("Unable to parse timecode")
|
|
|
|
|
|
def generate_edl(opts):
|
|
# Load shapes
|
|
data = bbb.parse_shapes_svg(opts.input)
|
|
|
|
# Scan events
|
|
out = opts.output
|
|
i = 1
|
|
|
|
for t_start, t_stop, desc in data:
|
|
# Is this a deskshare segment ?
|
|
if desc is None:
|
|
desc = opts.deskshare
|
|
is_still = False
|
|
else:
|
|
desc = f'{opts.presentation:s}/{desc[0]:s}/slide-{desc[1]:02d}.png'
|
|
is_still = True
|
|
|
|
# Event times
|
|
t_start = math.floor(t_start * opts.framerate)
|
|
t_stop = math.floor(t_stop * opts.framerate)
|
|
|
|
# Source times
|
|
if is_still:
|
|
t_src_start = TimeCode(opts.framerate, 0)
|
|
t_src_stop = TimeCode(opts.framerate, t_stop - t_start)
|
|
else:
|
|
t_src_start = TimeCode(opts.framerate, t_start)
|
|
t_src_stop = TimeCode(opts.framerate, t_stop)
|
|
|
|
# Recorder times
|
|
t_rec_start = TimeCode(opts.framerate, t_start)
|
|
t_rec_stop = TimeCode(opts.framerate, t_stop)
|
|
|
|
# EDL gen
|
|
out.write(f"{i:03d} AX V C {str(t_src_start):s} {str(t_src_stop):s} {str(t_rec_start):s} {str(t_rec_stop):s}\n")
|
|
if is_still:
|
|
out.write(f"M2 AX 000.0 00:00:00:00\n")
|
|
out.write(f"* FROM CLIP NAME: {desc:s}\n")
|
|
out.write("\n")
|
|
|
|
# Next
|
|
i = i + 1
|
|
|
|
|
|
def parse_opt():
|
|
parser = argparse.ArgumentParser(
|
|
prog = 'bbb-to-edl.py',
|
|
description = 'BBB recording EDL generator',
|
|
)
|
|
|
|
parser.add_argument('-i', '--input', required=True, type=argparse.FileType('r'),
|
|
help="Path to the shapes.svg file from BBB recording")
|
|
parser.add_argument('-o', '--output', required=True, type=argparse.FileType('w'),
|
|
help="Path to the output EDL file")
|
|
parser.add_argument('-d', '--deskshare', required=True,
|
|
help="Path to use for the deskshare segments")
|
|
parser.add_argument('-p', '--presentation', required=True,
|
|
help="Prefix Path for the presentation slides segments")
|
|
parser.add_argument('-f', '--framerate', type=int, default=24,
|
|
help="Framerate of the project")
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
opts = parse_opt()
|
|
generate_edl(opts)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|