# AAF

> Reading, writing and changing AAF files, the options each takes, and how a written file relinks in Media Composer.

AAF is how a cut reaches Avid Media Composer. Reading and writing are ports
of upstream's AAF adapter, checked byte for byte against what upstream reads
and writes, and AAF is available from every language.

## Reading an AAF

An AAF reads as upstream's adapter reads it, byte for byte once written as
OTIO JSON, on every sample file in upstream's test data. Upstream's four
reading options are all here, with upstream's defaults:

- `simplify` (on) collapses the nesting AAF has and OTIO does not need.
- `attach_markers` (on) moves each marker from the slot that carries it onto
  the item it points at.
- `bake_keyframed_properties` (off) records each keyframed effect
  parameter's value at every frame of its effect as `keyframe_baked_values`,
  interpolated the way pyaaf2 interpolates it.
- `transcribe_log` (off) prints a line for each thing the reader makes,
  word for word what upstream prints. From Rust it takes a `TranscribeLog`,
  which hands each line to a function of your choosing. It is not in the C
  ABI or the SDKs built on it, since it would need a callback across it.

From C and the SDKs, the first two are spelled as what turning them off
does, `aaf_keep_nesting` and `aaf_markers_on_slots`, so that options left at
zero read an AAF the way upstream does. The third is `aaf_bake_keyframes`.

Baked curves go through the platform's `pow`, `acos` and `cos`, so off Linux
the last digit of a baked value can differ from upstream's. Three log lines
upstream prints for rare failures embed a Python object's memory address or
a whole track, and those are worded differently here.

## Writing an AAF

AAF is how a cut reaches Avid Media Composer, and writing one is a port of
upstream's AAF adapter rather than a design of its own. Each track becomes a
slot of a composition. Each clip becomes a chain of three objects: a master
clip, a file describing the media, and a tape carrying its timecode, which
is the chain Media Composer expects to relink through. Gaps, dissolves,
nested tracks, stacks and markers all have an AAF form; any other transition
is left out, as upstream leaves it out.

Rust:

```rust
use otio_aaf::{Aaf, WriteOptions};
use otio_adapter::Adapter;

fn main() -> Result<(), otio_adapter::Error> {
    // AAF is written from a timeline. A cut that was read from an AAF keeps
    // what the reader found under metadata["AAF"], and that is written back,
    // so its clips point at the same master mobs and the same media.
    let document = otio_core::from_str(&std::fs::read_to_string("cut.otio")?)?;

    // Every clip needs a MobID, from its metadata, its media's metadata or
    // the AAF its media names. A cut built from scratch has none, so let
    // the writer make them up rather than refuse the clip.
    let options = WriteOptions::new().with_use_empty_mob_ids(true);
    Aaf::write_to_file(&document, "cut.aaf", &options)?;

    Ok(())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/rust.rs</sub>

Python:

```python
import opentimelineio as otio

# An AAF clip is cut from media of a known length, so each clip's media says
# how much of it there is. A cut read from an AAF already carries that, and
# what the reader found under metadata["AAF"], and both are written back.
timeline = otio.schema.Timeline(name="Cut")
track = otio.schema.Track(name="V1", kind="Video")
timeline.tracks.append(track)

for name in ["A001C003", "A001C004"]:
    one_second = otio.opentime.TimeRange(
        start_time=otio.opentime.RationalTime(0, 24),
        duration=otio.opentime.RationalTime(24, 24),
    )
    track.append(otio.schema.Clip(
        name=name,
        media_reference=otio.schema.ExternalReference(
            target_url=f"file:///media/{name}.mov",
            available_range=one_second,
        ),
        source_range=one_second,
    ))

# Every clip needs a MobID, from its metadata, its media's metadata or the
# AAF its media names. A cut built from scratch has none, so let the writer
# make them up rather than refuse the clip.
otio.adapters.write_to_file(timeline, "cut.aaf", use_empty_mob_ids=True)

print([clip.name for clip in otio.adapters.read_from_file("cut.aaf").find_clips()])
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/python.py</sub>

TypeScript:

```typescript
import { writeFile } from "node:fs/promises";
import {
  init,
  Clip,
  ExternalReference,
  RationalTime,
  Timeline,
  TimeRange,
  Track,
  readFromBytes,
  writeToBytes,
} from "@alchemist-edit/otio";

await init();

const timeline = new Timeline({ name: "Cut" });
const track = new Track({ name: "V1", kind: "Video" });
timeline.tracks?.appendChild(track);

const oneSecond = new TimeRange(new RationalTime(0, 24), new RationalTime(24, 24));

// An AAF clip is cut from media of a known length, so each clip's media says
// how much of it there is. A new clip has no media at all, so its reference
// goes in under upstream's key and is made the active one.
for (const name of ["A001C003", "A001C004"]) {
  const media = new ExternalReference({ targetUrl: `file:///media/${name}.mov` });
  media.availableRange = oneSecond;

  const clip = new Clip({ name });
  clip.setMediaReference("DEFAULT_MEDIA", media);
  clip.activeMediaReferenceKey = "DEFAULT_MEDIA";
  clip.sourceRange = oneSecond;
  track.appendChild(clip);
}

// Every clip needs a MobID, from its metadata, its media's metadata or the
// AAF its media names. A cut built from scratch has none, so let the writer
// make them up rather than refuse the clip. The module has no clock or
// randomness of its own, so this side supplies the time and a seed.
const aaf = writeToBytes("aaf", timeline, { aafUseEmptyMobIds: true });
await writeFile("cut.aaf", aaf);

for (const clip of readFromBytes("aaf", aaf).findClips()) {
  console.log(clip.name);
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/typescript.ts</sub>

Go:

```go
package main

import (
	"fmt"
	"log"

	otio "github.com/alchemist-editor/otio-rust/sdk/go"
)

func main() {
	timeline, err := otio.NewTimeline("Cut")
	if err != nil {
		log.Fatal(err)
	}
	defer timeline.Close()

	stack, err := otio.NewStack("tracks")
	if err != nil {
		log.Fatal(err)
	}
	track, err := otio.NewTrack("V1", "Video")
	if err != nil {
		log.Fatal(err)
	}
	if err := timeline.SetTracks(&stack.Node); err != nil {
		log.Fatal(err)
	}
	if err := stack.AppendChild(track.Node); err != nil {
		log.Fatal(err)
	}

	oneSecond := otio.TimeRange{
		StartTime: otio.RationalTime{Value: 0, Rate: 24},
		Duration:  otio.RationalTime{Value: 24, Rate: 24},
	}

	// An AAF clip is cut from media of a known length, so each clip's media
	// says how much of it there is. A new clip has no media at all, so its
	// reference goes in under upstream's key and is made the active one.
	for _, name := range []string{"A001C003", "A001C004"} {
		media, err := otio.NewExternalReference("", "file:///media/"+name+".mov")
		if err != nil {
			log.Fatal(err)
		}
		if err := media.SetAvailableRange(oneSecond); err != nil {
			log.Fatal(err)
		}

		clip, err := otio.NewClip(name)
		if err != nil {
			log.Fatal(err)
		}
		if err := clip.SetMediaReference("DEFAULT_MEDIA", media.Node); err != nil {
			log.Fatal(err)
		}
		if err := clip.SetActiveMediaReferenceKey("DEFAULT_MEDIA"); err != nil {
			log.Fatal(err)
		}
		if err := clip.SetSourceRange(oneSecond); err != nil {
			log.Fatal(err)
		}
		if err := track.AppendChild(clip.Node); err != nil {
			log.Fatal(err)
		}
	}

	// Every clip needs a MobID, from its metadata, its media's metadata or
	// the AAF its media names. A cut built from scratch has none, so let the
	// writer make them up rather than refuse the clip.
	options := &otio.WriteOptions{AAFUseEmptyMobIds: true}
	if err := otio.WriteToFile(otio.FormatAAF, timeline.Node, "cut.aaf", options); err != nil {
		log.Fatal(err)
	}

	root, err := otio.ReadFromFile(otio.FormatAAF, "cut.aaf", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	clips, err := root.FindClips()
	if err != nil {
		log.Fatal(err)
	}
	for _, clip := range otio.Filter(clips, otio.Node.AsClip) {
		name, _ := clip.Name()
		fmt.Println(name)
	}
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/go.go</sub>

Swift:

```swift
import OpenTimelineIO

let timeline = try Timeline(name: "Cut")
let stack = try Stack(name: "tracks")
let track = try Track(name: "V1", kind: "Video")

try timeline.setTracks(stack)
try stack.appendChild(track)

let oneSecond = TimeRange(
    startTime: RationalTime(value: 0, rate: 24),
    duration: RationalTime(value: 24, rate: 24))

// An AAF clip is cut from media of a known length, so each clip's media says
// how much of it there is. A new clip has no media at all, so its reference
// goes in under upstream's key and is made the active one.
for name in ["A001C003", "A001C004"] {
    let media = try ExternalReference(targetURL: "file:///media/\(name).mov")
    try media.setAvailableRange(oneSecond)

    let clip = try Clip(name: name)
    try clip.setMediaReference("DEFAULT_MEDIA", reference: media)
    try clip.setActiveMediaReferenceKey("DEFAULT_MEDIA")
    try clip.setSourceRange(oneSecond)
    try track.appendChild(clip)
}

// Every clip needs a MobID, from its metadata, its media's metadata or the
// AAF its media names. A cut built from scratch has none, so let the writer
// make them up rather than refuse the clip.
try OTIO.writeToFile(
    .aaf, root: timeline, path: "cut.aaf", options: WriteOptions(aafUseEmptyMobIds: true))

for case let clip as Clip in try OTIO.readFromFile(.aaf, path: "cut.aaf").findClips() {
    print(try clip.name())
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/swift.swift</sub>

C++:

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    otio::Timeline timeline = otio::Timeline::create("Cut");
    otio::Stack stack = otio::Stack::create("tracks");
    otio::Track track = otio::Track::create("V1", "Video");

    timeline.set_tracks(stack);
    stack.append_child(track);

    const otio::TimeRange one_second(otio::RationalTime(0, 24), otio::RationalTime(24, 24));

    // An AAF clip is cut from media of a known length, so each clip's media
    // says how much of it there is. A new clip has no media at all, so its
    // reference goes in under upstream's key and is made the active one.
    const std::vector<std::string> names = {"A001C003", "A001C004"};
    for (const std::string &name : names) {
        otio::ExternalReference media =
            otio::ExternalReference::create(std::nullopt, "file:///media/" + name + ".mov");
        media.set_available_range(one_second);

        otio::Clip clip = otio::Clip::create(name);
        clip.set_media_reference("DEFAULT_MEDIA", media);
        clip.set_active_media_reference_key("DEFAULT_MEDIA");
        clip.set_source_range(one_second);
        track.append_child(clip);
    }

    // Every clip needs a MobID, from its metadata, its media's metadata or
    // the AAF its media names. A cut built from scratch has none, so let the
    // writer make them up rather than refuse the clip.
    otio::WriteOptions options = otio::write_options_default();
    options.aaf_use_empty_mob_ids = true;
    otio::write_to_file(otio::Format::AAF, timeline, "cut.aaf", options);

    const otio::SerializableObject root =
        otio::read_from_file(otio::Format::AAF, "cut.aaf", std::nullopt);

    for (const otio::SerializableObject &node : root.find_clips()) {
        if (std::optional<otio::Clip> clip = node.as<otio::Clip>()) {
            std::cout << clip->name() << "\n";
        }
    }

    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/cpp.cpp</sub>

Zig:

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    const document = try otio.Document.init();
    defer document.deinit();

    const timeline = try otio.Timeline.init(document, "Cut");
    const stack = try otio.Stack.init(document, "tracks");
    const track = try otio.Track.init(document, "V1", "Video");
    try timeline.setTracks(stack.asNode());
    try stack.appendChild(track.asNode());

    const one_second = otio.TimeRange{
        .start_time = .{ .value = 0, .rate = 24 },
        .duration = .{ .value = 24, .rate = 24 },
    };

    // An AAF clip is cut from media of a known length, so each clip's media
    // says how much of it there is. A new clip has no media at all, so its
    // reference goes in under upstream's key and is made the active one.
    inline for (.{ "A001C003", "A001C004" }) |name| {
        const url = "file:///media/" ++ name ++ ".mov";
        const media = try otio.ExternalReference.init(document, null, url);
        try media.setAvailableRange(one_second);

        const clip = try otio.Clip.init(document, name);
        try clip.setMediaReference("DEFAULT_MEDIA", media.asNode());
        try clip.setActiveMediaReferenceKey("DEFAULT_MEDIA");
        try clip.setSourceRange(one_second);
        try track.appendChild(clip.asNode());
    }

    try document.setRoot(timeline.asNode());

    // Every clip needs a MobID, from its metadata, its media's metadata or
    // the AAF its media names. A cut built from scratch has none, so let the
    // writer make them up rather than refuse the clip. Every other option
    // keeps its default.
    try document.writeToFile(.aaf, "cut.aaf", .{ .aaf_use_empty_mob_ids = true });

    const written = try otio.Document.readFromFile(.aaf, "cut.aaf", null);
    defer written.deinit();

    const root = (try written.root()) orelse return error.Empty;
    for (try root.findClips(allocator)) |node| {
        if (node.asClip()) |clip| {
            std.debug.print("{s}\n", .{try clip.name(allocator)});
        }
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/zig.zig</sub>

C:

```c
#include <stdio.h>
#include <stdlib.h>

#include "otio.h"

int main(void) {
    OtioDocument *document = otio_document_new();

    OtioNode timeline, stack, track;
    otio_timeline_new(document, "Cut", &timeline, NULL);
    otio_stack_new(document, "tracks", &stack, NULL);
    otio_track_new(document, "V1", "Video", &track, NULL);
    otio_timeline_set_tracks(document, timeline, stack, NULL);
    otio_composition_append_child(document, stack, track, NULL);

    OtioTimeRange one_second;
    one_second.start_time.value = 0.0;
    one_second.start_time.rate = 24.0;
    one_second.duration.value = 24.0;
    one_second.duration.rate = 24.0;

    /* An AAF clip is cut from media of a known length, so each clip's media
     * says how much of it there is. A new clip has no media at all, so its
     * reference goes in under upstream's key and is made the active one. */
    const char *names[] = {"A001C003", "A001C004"};
    for (size_t index = 0; index < 2; index += 1) {
        char url[64];
        snprintf(url, sizeof url, "file:///media/%s.mov", names[index]);

        OtioNode reference;
        otio_external_reference_new(document, NULL, url, &reference, NULL);
        otio_media_reference_set_available_range(document, reference, one_second, NULL);

        OtioNode clip;
        otio_clip_new(document, names[index], &clip, NULL);
        otio_clip_set_media_reference(document, clip, "DEFAULT_MEDIA", reference, NULL);
        otio_clip_set_active_media_reference_key(document, clip, "DEFAULT_MEDIA", NULL);
        otio_item_set_source_range(document, clip, one_second, NULL);

        otio_composition_append_child(document, track, clip, NULL);
    }

    otio_document_set_root(document, timeline, NULL);

    /* Every clip needs a MobID, from its metadata, its media's metadata or
     * the AAF its media names. A cut built from scratch has none, so let the
     * writer make them up rather than refuse the clip. */
    OtioWriteOptions options;
    options.struct_size = sizeof options;
    otio_write_options_init(&options);
    options.aaf_use_empty_mob_ids = true;

    OtioBuffer error;
    if (otio_write_to_file(OTIO_FORMAT_AAF, document, "cut.aaf", &options, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }
    otio_document_free(document);

    OtioDocument *written = NULL;
    if (otio_read_from_file(OTIO_FORMAT_AAF, "cut.aaf", NULL, &written, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }

    OtioNode root;
    otio_document_root(written, &root, NULL);

    size_t count = 0;
    otio_node_find_clips(written, root, NULL, 0, &count, NULL);

    OtioNode *clips = malloc(count * sizeof *clips);
    otio_node_find_clips(written, root, clips, count, &count, NULL);

    for (size_t index = 0; index < count; index += 1) {
        OtioBuffer name;
        if (otio_node_name(written, clips[index], &name, NULL) == OTIO_STATUS_OK) {
            printf("%s\n", name.data);
            otio_buffer_free(name);
        }
    }

    free(clips);
    otio_document_free(written);
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/c.c</sub>

C#:

```csharp
using System;
using OpenTimelineIO;

var timeline = new Timeline("Cut");
var stack = new Stack("tracks");
var track = new Track("V1", "Video");

timeline.SetTracks(stack);
stack.AppendChild(track);

var oneSecond = new TimeRange(new RationalTime(0, 24), new RationalTime(24, 24));

// An AAF clip is cut from media of a known length, so each clip's media says
// how much of it there is. A new clip has no media at all, so its reference
// goes in under upstream's key and is made the active one.
foreach (var name in new[] { "A001C003", "A001C004" })
{
    var media = new ExternalReference(targetUrl: $"file:///media/{name}.mov");
    media.SetAvailableRange(oneSecond);

    var clip = new Clip(name);
    clip.SetMediaReference("DEFAULT_MEDIA", media);
    clip.SetActiveMediaReferenceKey("DEFAULT_MEDIA");
    clip.SetSourceRange(oneSecond);
    track.AppendChild(clip);
}

// Every clip needs a MobID, from its metadata, its media's metadata or the
// AAF its media names. A cut built from scratch has none, so let the writer
// make them up rather than refuse the clip.
Otio.WriteToFile(Format.Aaf, timeline, "cut.aaf", new WriteOptions(aafUseEmptyMobIds: true));

foreach (var node in Otio.ReadFromFile(Format.Aaf, "cut.aaf", null).FindClips())
{
    if (node is Clip clip)
    {
        Console.WriteLine(clip.Name());
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/csharp.cs</sub>

Objective-C:

```objectivec
#import <Foundation/Foundation.h>

#import <OpenTimelineIO/OpenTimelineIO.h>

int main(void) {
    @autoreleasepool {
        NSError *error = nil;

        OTIOTimeline *timeline = [OTIOTimeline timelineWithName:@"Cut" error:&error];
        OTIOStack *stack = [OTIOStack stackWithName:@"tracks" error:&error];
        OTIOTrack *track = [OTIOTrack trackWithName:@"V1" kind:@"Video" error:&error];

        [timeline setTracks:stack error:&error];
        [stack appendChild:track error:&error];

        OTIOTimeRange oneSecond =
            OTIOTimeRangeMake(OTIORationalTimeMake(0, 24), OTIORationalTimeMake(24, 24));

        // An AAF clip is cut from media of a known length, so each clip's
        // media says how much of it there is. A new clip has no media at
        // all, so its reference goes in under upstream's key and is made
        // the active one.
        for (NSString *name in @[ @"A001C003", @"A001C004" ]) {
            NSString *url = [NSString stringWithFormat:@"file:///media/%@.mov", name];
            OTIOExternalReference *media =
                [OTIOExternalReference externalReferenceWithName:nil targetURL:url error:&error];
            [media setAvailableRange:oneSecond error:&error];

            OTIOClip *clip = [OTIOClip clipWithName:name error:&error];
            [clip setMediaReference:@"DEFAULT_MEDIA" reference:media error:&error];
            [clip setActiveMediaReferenceKey:@"DEFAULT_MEDIA" error:&error];
            [clip setSourceRange:oneSecond error:&error];
            [track appendChild:clip error:&error];
        }

        // Every clip needs a MobID, from its metadata, its media's metadata
        // or the AAF its media names. A cut built from scratch has none, so
        // let the writer make them up rather than refuse the clip.
        OTIOWriteOptions options = OTIOWriteOptionsDefault();
        options.aafUseEmptyMobIds = YES;
        if (!OTIOWriteToFile(OTIOFormatAAF, timeline, @"cut.aaf", &options, &error)) {
            NSLog(@"%@", error.localizedDescription);
            return 1;
        }

        OTIOSerializableObject *root = OTIOReadFromFile(OTIOFormatAAF, @"cut.aaf", NULL, &error);
        for (OTIOSerializableObject *node in [root findClips:&error]) {
            if ([node isKindOfClass:[OTIOClip class]]) {
                NSLog(@"%@", [(OTIOClip *)node name:&error]);
            }
        }
    }
    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/write-an-aaf/objectivec.m</sub>

A clip is tied to its media by a MobID. A cut read from an AAF already has
one on every clip, kept under `metadata["AAF"]`, and it is written back, so
the file relinks to the media it came from. A clip whose media is itself an
AAF holding one master clip takes that clip's MobID. Anything else, such as
a cut built from scratch, has none, and writing it needs
`use_empty_mob_ids`, which makes MobIDs up. A made-up MobID links to no
media Media Composer knows, which is why upstream refuses a clip without one
by default, and so does this.

Before writing anything, the writer checks the whole timeline the way
upstream does: every item at the timeline's rate, every clip saying how much
media it has, and every transition carrying what an AAF dissolve is built
from. It reports everything it finds wrong at once rather than stopping at
the first.

The file is the same, byte for byte, as the one upstream's adapter writes
from the same timeline, given the same clock and the same random
identifiers. The tests check that on twelve files, and it holds on all 33
samples in upstream's own test data that upstream can write.
That parity is the evidence the file suits Media Composer: none of the files
has been imported into Media Composer as part of testing.

Embedding the media in the file works as upstream's `embed_essence` does.
Each clip's media URL is taken as a path, relative to the working directory
unless it is absolute. An `.aaf` there has the master mob with the clip's
MobID copied out of it, with its source mob and essence. A `.dnx` on a video
track is imported as a raw DNxHD stream. Anything else is refused as
upstream refuses it, and that includes a WAV file, which upstream sends to
the DNxHD import too.

From Python, upstream's four AAF hooks run where upstream runs them:
`otio_aaf_pre_read_transcribe` and `otio_aaf_post_read_transcribe` before and
after the file is transcribed, and `otio_aaf_pre_write_transcribe` and
`otio_aaf_post_write_transcribe` before and after it is written. A
`pre_write` hook is where to transcode other media into something
`embed_essence` can embed, as upstream suggests: what it returns is what is
written. Each hook is handed upstream's arguments, except that `aaf_handle`,
upstream's open pyaaf2 file, is `None`, because there is no pyaaf2 file here.
From Rust, `ReadOptions::with_post_transcribe` runs a callback at the one
point inside the read; the other three run around the read or write, so a
caller runs them itself.

AAF writing is available from every language. From Python,
`otio.adapters.write_to_file(timeline, "cut.aaf")` takes upstream's keyword
arguments, `embed_essence=True` among them. From C and the SDKs, the same options are `aaf_`-prefixed fields of the write
options, along with three a library caller needs and a Python one does not:
`aaf_user`, whom a new marker is credited to when no login name is set;
`aaf_time`, the time the file records; and `aaf_id_seed`, which seeds the
identifiers it makes up. The same time, seed and timeline write the same
file. WebAssembly has no clock or randomness of its own, so the TypeScript
package passes the time and a fresh seed on every write unless you give
your own. Nor has it a file system, so it cannot embed media: with
`aafEmbedEssence` set, a clip whose media names a file stops the write, as
the file cannot be found.

## Changing an existing AAF

Below the adapter, the `aaf` crate can open an AAF file that already exists,
change it and save it, as pyaaf2 does when it opens a file with `'r+'`. The
same edits in the same order leave the same bytes pyaaf2 leaves, which
twenty scenarios in its tests check against files pyaaf2 changed: properties
changed, added and removed, mobs and slots added and taken out, new
definitions and classes, and essence moved and dropped. It works on the AAF
object model, not on a timeline, and is available from Rust only. The
[crate's README](https://github.com/alchemist-editor/otio-rust/blob/main/crates/aaf/README.md#changing-a-file)
has an example, and
[ADR 0005](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0005-aaf-modify-path.md)
the design.

---

Page: https://otio-rust-docs.vercel.app/docs/formats/aaf/
