Final Cut Pro XML
FCP 7 interchange XML and FCP X XML: what each reads as, and what is kept for the round trip.
Two different formats share the name. FCP 7's .xml is the older one, and
still how a great many tools hand an edit to one another; FCP X's .fcpxml
is Final Cut's current format, and it does not think in tracks. Both are ports
of upstream's adapters, and both keep what OTIO has no field for in metadata
so a file read and written again keeps it.
use otio_adapter::Adapter;
use otio_cmx3600::{Cmx3600, ReadOptions};
use otio_fcpx::FcpxXml;
fn main() -> Result<(), otio_adapter::Error> {
// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
let options = ReadOptions {
rate: 24.0,
..Default::default()
};
let document = Cmx3600::read_from_file("cut.edl", &options)?;
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the
// object model is the interchange, and the file formats are two ways of
// spelling it.
FcpxXml::write_to_file(&document, "cut.fcpxml", &Default::default())?;
Ok(())
}import opentimelineio as otio
# An EDL never says what rate its timecode is at, so this has to be right: a
# file read at the wrong rate puts every event in the wrong place rather than
# failing.
timeline = otio.adapters.read_from_file("cut.edl", rate=24)
# Nothing happens in between. The timeline an EDL parses to is the same
# timeline FCP X writes out, so converting is a read and a write: the object
# model is the interchange, and the file formats are two ways of spelling it.
#
# Both calls pick their adapter from the suffix, as upstream picks it.
otio.adapters.write_to_file(timeline, "cut.fcpxml")import { readFile, writeFile } from "node:fs/promises";
import { init, readFromString, writeToString } from "@alchemist-edit/otio";
await init();
// There is no filesystem behind a WebAssembly module, so getting hold of the
// bytes is the host's line of code: `fs` in Node, `fetch` in a browser.
const edl = await readFile("cut.edl", "utf8");
// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
const timeline = readFromString("cmx3600", edl, { rate: 24 });
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
await writeFile("cut.fcpxml", writeToString("fcpxXml", timeline));package main
import (
"log"
otio "github.com/alchemist-editor/otio-rust/sdk/go"
)
func main() {
// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
root, err := otio.ReadFromFile(otio.FormatCMX3600, "cut.edl", &otio.ReadOptions{Rate: 24})
if err != nil {
log.Fatal(err)
}
defer root.Close()
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the
// object model is the interchange, and the file formats are two ways of
// spelling it.
//
// Writing starts at the object it is given, so handing it the root
// writes the whole file.
if err := otio.WriteToFile(otio.FormatFcpxXML, root, "cut.fcpxml", nil); err != nil {
log.Fatal(err)
}
}import OpenTimelineIO
// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
let options = ReadOptions(rate: 24, nameColumn: "", ignoreTimecodeMismatch: false)
let root = try OTIO.readFromFile(.cmx3600, path: "cut.edl", options: options)
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
try OTIO.writeToFile(.fcpxXML, root: root, path: "cut.fcpxml") <opentimelineio/otio.hpp>
int main() {
// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
otio::ReadOptions options = otio::read_options_default();
options.rate = 24;
const otio::SerializableObject root =
otio::read_from_file(otio::Format::CMX_3600, "cut.edl", options);
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the
// object model is the interchange, and the file formats are two ways of
// spelling it.
otio::write_to_file(otio::Format::FCPX_XML, root, "cut.fcpxml");
return 0;
}const std = ("std");
const otio = ("otio");
pub fn main() !void {
// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
const options = otio.ReadOptions{
.rate = 24,
.name_column = null,
.ignore_timecode_mismatch = false,
};
const document = try otio.Document.readFromFile(.cmx3600, "cut.edl", options);
defer document.deinit();
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the
// object model is the interchange, and the file formats are two ways of
// spelling it.
try document.writeToFile(.fcpx_xml, "cut.fcpxml", null);
}
int main(void) {
/* An EDL never says what rate its timecode is at, so this has to be
* right: a file read at the wrong rate puts every event in the wrong
* place rather than failing. */
OtioReadOptions options;
options.struct_size = sizeof options;
otio_read_options_init(&options);
options.rate = 24;
/* A call that can fail says why in the buffer passed last, which is
* yours to free. Pass NULL where the status is all you want. */
OtioDocument *document = NULL;
OtioBuffer error;
if (otio_read_from_file(OTIO_FORMAT_CMX_3600, "cut.edl", &options, &document, &error) != OTIO_STATUS_OK) {
fprintf(stderr, "%s\n", error.data);
otio_buffer_free(error);
return 1;
}
/* Nothing happens in between. The timeline an EDL parses to is the same
* timeline FCP X writes out, so converting is a read and a write: the
* object model is the interchange, and the file formats are two ways of
* spelling it. */
if (otio_write_to_file(OTIO_FORMAT_FCPX_XML, document, "cut.fcpxml", NULL, &error) != OTIO_STATUS_OK) {
fprintf(stderr, "%s\n", error.data);
otio_buffer_free(error);
otio_document_free(document);
return 1;
}
otio_document_free(document);
return 0;
}using OpenTimelineIO;
// An EDL never says what rate its timecode is at, so this has to be right: a
// file read at the wrong rate puts every event in the wrong place rather
// than failing.
// A value here is a `readonly struct`, as one in .NET should be, so the rate
// is chosen when the options are made rather than set afterwards.
var defaults = Otio.ReadOptionsDefault();
var options = new ReadOptions(24, defaults.NameColumn, defaults.IgnoreTimecodeMismatch);
var timeline = Otio.ReadFromFile(Format.Cmx3600, "cut.edl", options);
// Nothing happens in between. The timeline an EDL parses to is the same
// timeline FCP X writes out, so converting is a read and a write: the object
// model is the interchange, and the file formats are two ways of spelling it.
Otio.WriteToFile(Format.FcpxXml, timeline, "cut.fcpxml", null);
int main(void) {
{
NSError *error = nil;
// An EDL never says what rate its timecode is at, so this has to be
// right: a file read at the wrong rate puts every event in the wrong
// place rather than failing.
OTIOReadOptions options = OTIOReadOptionsDefault();
options.rate = 24;
OTIOSerializableObject *timeline = OTIOReadFromFile(OTIOFormatCMX3600,
@"cut.edl",
&options,
&error);
if (timeline == nil) {
NSLog(@"%@", error);
return 1;
}
// Nothing happens in between. The timeline an EDL parses to is the
// same timeline FCP X writes out, so converting is a read and a
// write: the object model is the interchange, and the file formats
// are two ways of spelling it.
if (!OTIOWriteToFile(OTIOFormatFcpxXML, timeline, @"cut.fcpxml", NULL, &error)) {
NSLog(@"%@", error);
return 1;
}
}
return 0;
}Final Cut Pro 7 XML
FCP 7 itself is long gone, but its XML is not: Premiere Pro, Resolve, Hiero and Media Composer all read or write some dialect of it.
The format carries far more per-element detail than OTIO has fields for.
Everything the adapter does not turn into a real OTIO field is kept under the
fcp_xml key in the relevant object's metadata and written back out on the
way past, so a file read and written again keeps its colour settings, its
effect parameters and its host application's bookkeeping.
One fix to plain reading departs from upstream: a nested sequence reads with its tracks, where upstream reads every one as an empty stack.
Final Cut Pro X XML
Final Cut does not think in tracks. A sequence holds one spine, the main
storyline, and everything layered over or under it hangs off whichever
storyline item it overlaps, carrying a lane number saying how far above or
below it sits. Reading works out where each element really starts and groups
the results by lane, one track per lane. Writing puts lane zero back into the
spine and reattaches the rest.
What comes back from a read depends on what the file holds:
| The file holds | It reads as |
|---|---|
| a library or an event | a SerializableCollection of timelines |
| a bare project | a Timeline |
| loose clips | a collection of clips and compound clips |
What Final Cut knows about a piece of media that OTIO has no field for (its
note, its keywords and its Spotlight metadata) is kept under the fcpx key in
the media reference's metadata and written back out on the way past.
Conversions upstream cannot do
Several conversions that fail outright with upstream's adapters work here, each a documented departure on the adapter it concerns:
- An EDL whose tracks start anywhere but
00:00:00:00, or an audio-only EDL, written as FCP X XML. - An FCP 7 timeline with a 15 fps clip, written as FCP X XML.
- FCP X XML written as FCP 7 XML, which upstream wrote as a file that could not be read back.