Skip to content

Commit

Permalink
Use pure v3, fix de-sync issue
Browse files Browse the repository at this point in the history
  • Loading branch information
WyattBlue committed Aug 9, 2023
1 parent 8c42f2c commit 7afe7ca
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 138 deletions.
7 changes: 5 additions & 2 deletions auto_editor/ffwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def __init__(self, path: str, ffmpeg: FFmpeg, log: Log, label: str = ""):
self.audios: list[AudioStream] = []
self.subtitles: list[SubtitleStream] = []
self.description = None
self.duration = ""
self.duration = 0.0

_dir = os.path.dirname(ffmpeg.path)
_ext = os.path.splitext(ffmpeg.path)[1]
Expand Down Expand Up @@ -258,7 +258,10 @@ def get_attr(name: str, dic: dict[Any, Any], default: Any = -1) -> str:
self.description = json_info["format"]["tags"]["description"]

if "duration" in json_info["format"]:
self.duration = json_info["format"]["duration"]
try:
self.duration = float(json_info["format"]["duration"])
except Exception:
pass

for stream in json_info["streams"]:
lang = None
Expand Down
231 changes: 103 additions & 128 deletions auto_editor/formats/final_cut_pro.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

from fractions import Fraction
from typing import TYPE_CHECKING, Any, cast
from xml.etree.ElementTree import Element, ElementTree, SubElement, indent

from auto_editor.ffwrapper import FileInfo
from auto_editor.timeline import v3
if TYPE_CHECKING:
from collections.abc import Sequence

from .utils import indent
from auto_editor.ffwrapper import FileInfo
from auto_editor.timeline import TlAudio, TlVideo, v3

"""
Export a FCPXML 9 file readable with Final Cut Pro 10.4.9 or later.
Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
See docs here:
https://developer.apple.com/documentation/professional_video_applications/fcpxml_reference
Expand Down Expand Up @@ -38,128 +40,101 @@ def get_colorspace(src: FileInfo) -> str:
return "1-1-1 (Rec. 709)"


def fraction(_a: float, tb: Fraction) -> str:
if _a == 0:
return "0s"

a = Fraction(_a)
frac = Fraction(a, tb).limit_denominator()
num = frac.numerator
dem = frac.denominator

if dem < 3000:
factor = int(3000 / dem)

if factor == 3000 / dem:
num *= factor
dem *= factor
else:
# Good enough but has some error that are impacted at speeds such as 150%.
total = Fraction(0)
while total < frac:
total += Fraction(1, 30)
num = total.numerator
dem = total.denominator

return f"{num}/{dem}s"


def fcp_xml(group_name: str, output: str, tl: v3) -> None:
assert tl.v1 is not None
src = tl.v1.source
chunks = tl.v1.chunks
tb = tl.tb

total_dur = chunks[-1][1]
pathurl = src.path.resolve().as_uri()
width, height = tl.res
name = src.path.stem
colorspace = get_colorspace(src)

with open(output, "w", encoding="utf-8") as outfile:
outfile.write('<?xml version="1.0" encoding="UTF-8"?>\n')
outfile.write("<!DOCTYPE fcpxml>\n\n")
outfile.write('<fcpxml version="1.9">\n')
outfile.write("\t<resources>\n")
outfile.write(
f'\t\t<format id="r1" name="FFVideoFormat{height}p{float(tb)}" '
f'frameDuration="{fraction(1, tb)}" '
f'width="{width}" height="{height}" '
f'colorSpace="{colorspace}"/>\n'
)
outfile.write(
f'\t\t<asset id="r2" name="{name}" start="0s" hasVideo="1" format="r1" '
'hasAudio="1" audioSources="1" audioChannels="2" '
f'duration="{fraction(total_dur, tb)}">\n'
)
outfile.write(
f'\t\t\t<media-rep kind="original-media" src="{pathurl}"></media-rep>\n'
)
outfile.write("\t\t</asset>\n")
outfile.write("\t</resources>\n")
outfile.write("\t<library>\n")
outfile.write(f'\t\t<event name="{group_name}">\n')
outfile.write(f'\t\t\t<project name="{name}">\n')
outfile.write(
indent(
4,
'<sequence format="r1" tcStart="0s" tcFormat="NDF" audioLayout="stereo" audioRate="48k">',
"\t<spine>",
# TODOs:
# - Allow fractional timebases
# - Don't hardcode stereo audio layout

assert int(tl.tb) == tl.tb

def fraction(val: int) -> str:
if val == 0:
return "0s"
return f"{val}/{tl.tb}s"

for _, _src in tl.sources.items():
src = _src
break

proj_name = src.path.stem
tl_dur = tl.out_len()
src_dur = int(src.duration * tl.tb)

fcpxml = Element("fcpxml", version="1.11")
resources = SubElement(fcpxml, "resources")
SubElement(
resources,
"format",
id="r1",
name=f"FFVideoFormat{tl.res[1]}p{int(tl.tb)}",
frameDuration=fraction(1),
width=f"{tl.res[0]}",
height=f"{tl.res[1]}",
colorSpace=get_colorspace(src),
)
r2 = SubElement(
resources,
"asset",
id="r2",
name=proj_name,
start="0s",
hasVideo="1" if tl.v and tl.v[0] else "0",
format="r1",
hasAudio="1" if tl.a and tl.a[0] else "0",
audioSources="1",
audioChannels="2",
duration=fraction(tl_dur),
)
SubElement(r2, "media-rep", kind="original-media", src=src.path.resolve().as_uri())

lib = SubElement(fcpxml, "library")
evt = SubElement(lib, "event", name=group_name)
proj = SubElement(evt, "project", name=proj_name)
sequence = SubElement(
proj,
"sequence",
format="r1",
tcStart="0s",
tcFormat="NDF",
audioLayout="stereo",
audioRate=f"{tl.sr // 1000}k",
)
spine = SubElement(sequence, "spine")

if tl.v and tl.v[0]:
clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
elif tl.a and tl.a[0]:
clips = tl.a[0]
else:
clips = []

for clip in clips:
clip_properties = {
"name": proj_name,
"ref": "r2",
"offset": fraction(clip.start),
"duration": fraction(clip.dur),
"start": fraction(int(clip.offset // clip.speed)),
"tcFormat": "NDF",
}
if clip.start == 0:
del clip_properties["start"]

asset = SubElement(spine, "asset-clip", clip_properties)
if clip.speed != 1:
# See the "Time Maps" section.
# https://developer.apple.com/documentation/professional_video_applications/fcpxml_reference/story_elements/timemap/

timemap = SubElement(asset, "timeMap")
SubElement(timemap, "timept", time="0s", value="0s", interp="smooth2")
SubElement(
timemap,
"timept",
time=fraction(int(src_dur // clip.speed)),
value=fraction(src_dur),
interp="smooth2",
)
)

last_dur = 0.0
for clip in chunks:
if clip[2] == 99999:
continue

clip_dur = (clip[1] - clip[0] + 1) / clip[2]
dur = fraction(clip_dur, tb)

close = "/" if clip[2] == 1 else ""

if last_dur == 0:
outfile.write(
indent(
6,
f'<asset-clip name="{name}" offset="0s" ref="r2" duration="{dur}" tcFormat="NDF"{close}>',
)
)
else:
start = fraction(clip[0] / clip[2], tb)
off = fraction(last_dur, tb)
outfile.write(
indent(
6,
f'<asset-clip name="{name}" offset="{off}" ref="r2" '
+ f'duration="{dur}" start="{start}" '
+ f'tcFormat="NDF"{close}>',
)
)

if clip[2] != 1:
# See the "Time Maps" section.
# https://developer.apple.com/library/archive/documentation/FinalCutProX/Reference/FinalCutProXXMLFormat/StoryElements/StoryElements.html

frac_total = fraction(total_dur, tb)
speed_dur = fraction(total_dur / clip[2], tb)

outfile.write(
indent(
6,
"\t<timeMap>",
'\t\t<timept time="0s" value="0s" interp="smooth2"/>',
f'\t\t<timept time="{speed_dur}" value="{frac_total}" interp="smooth2"/>',
"\t</timeMap>",
"</asset-clip>",
)
)

last_dur += clip_dur

outfile.write("\t\t\t\t\t</spine>\n")
outfile.write("\t\t\t\t</sequence>\n")
outfile.write("\t\t\t</project>\n")
outfile.write("\t\t</event>\n")
outfile.write("\t</library>\n")
outfile.write("</fcpxml>\n")

tree = ElementTree(fcpxml)
indent(tree, space="\t", level=0)
tree.write(output, xml_declaration=True, encoding="utf-8")
7 changes: 0 additions & 7 deletions auto_editor/formats/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ def safe_mkdir(path: str | Path) -> None:
mkdir(path)


def indent(base: int, *lines: str) -> str:
new_lines = ""
for line in lines:
new_lines += ("\t" * base) + line + "\n"
return new_lines


class Validator:
def __init__(self, log: Log):
self.log = log
Expand Down
2 changes: 1 addition & 1 deletion auto_editor/subcommands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class SubtitleJson(TypedDict):


class ContainerJson(TypedDict):
duration: str
duration: float
bitrate: str | None


Expand Down

0 comments on commit 7afe7ca

Please sign in to comment.