Old 05-03-2022, 09:54 AM   #41
JLP
Human being with feelings
 
Join Date: Jul 2019
Posts: 43
Default

Thank you very much for your work !

I`ve been trying to install it on a macOs computer. As much as I can say, the script is running, ask me to choose an aaf file then simply don't do anything after that. I've install python3, it seems to be well recognized by reaper. The aaf is generated by the latest version of pro tools.

I'll test it with a windows computer later. Does anyone get it to work on macOs ?
JLP is offline   Reply With Quote
Old 05-03-2022, 10:02 AM   #42
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by dsyrock View Post
thank you, it works! also pyaaf2 installed properly! but it didn't affects my issue in any way =( also @mattsoule have the same issue i guess
Quote:
Originally Posted by JLP View Post
ask me to choose an aaf file then simply don't do anything after that.
completely the same here

[edited]

some guys told that perhaps script don't works on macs because of Tkinter won't work on macs. also they recommend to try Gooey

[edited again]

FIXED!!!! by @edkashinsky
tested by me on macOS 12.3.1
Code:
#!/bin/python

import aaf2
import os
import sys
import wave
import urllib.parse
import urllib.request
import pprint
import json

have_reaper = True  
have_tk = False

[NOTICE, WARNING, ERROR, NONE] = range(4)
log_level = WARNING

def log(message, level=NOTICE):
    if log_level > level: return
    if have_reaper:
        RPR_ShowConsoleMsg(message + "\n")
    else:
        print(message)



class ReaperInterface:

    def __init__(self):
        self.insertion_track = None

    def select_aaf(self):
        ok, filename, _, _ = RPR_GetUserFileNameForRead("", "Import AAF", ".aaf")
        if not ok: return None
        return filename

    def get_project_directory(self):
        directory, _ = RPR_GetProjectPath("", 512)
        return directory

    def create_track(self, name):
        track_index = RPR_GetNumTracks()
        RPR_InsertTrackAtIndex(track_index, False)
        track = RPR_GetTrack(0, track_index)
        RPR_GetSetMediaTrackInfo_String(track, "P_NAME", name, True)
        return track

    def set_track_volume(self, track, volume):
        RPR_SetMediaTrackInfo_Value(track, "D_VOL", volume)

    def set_track_volume_envelope(self, track, volume_data):
        RPR_SetOnlyTrackSelected(track)
        RPR_Main_OnCommand(40406, 0)  # ReaSlang for "toggle volume envelope visible"
        envelope = RPR_GetTrackEnvelopeByName(track, "Volume")
        for point in volume_data:
            value = RPR_ScaleToEnvelopeMode(1, point["value"])
            RPR_InsertEnvelopePoint(envelope, point["time"], value, 0, 0.0, False, True)
        RPR_Envelope_SortPoints(envelope)

    def set_track_panning(self, track, panning):
        RPR_SetMediaTrackInfo_Value(track, "D_PAN", panning)

    def set_track_panning_envelope(self, track, panning_data):
        RPR_SetOnlyTrackSelected(track)
        RPR_Main_OnCommand(40407, 0)  # Toggle pan envelope visible
        envelope = RPR_GetTrackEnvelopeByName(track, "Pan")
        for point in panning_data:
            RPR_InsertEnvelopePoint(envelope, point["time"], point["value"], 0, 0.0, False, True)
        RPR_Envelope_SortPoints(envelope)

    def create_item(self, track, src, offset, pos, dur):
        RPR_SetOnlyTrackSelected(self.insertion_track)
        RPR_MoveEditCursor(-1000, False)
        RPR_InsertMedia(src, 0)
        item = RPR_GetTrackMediaItem(self.insertion_track, 0)
        RPR_MoveMediaItemToTrack(item, track)
        RPR_SetMediaItemInfo_Value(item, "D_POSITION", pos)
        RPR_SetMediaItemInfo_Value(item, "D_LENGTH", dur)
        take = RPR_GetMediaItemTake(item, 0)
        RPR_SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offset)
        return item

    def set_item_fades(self, item, fadein=None, fadeout=None, fadeintype=0, fadeouttype=0):
        if fadein:
            RPR_SetMediaItemInfo_Value(item, "D_FADEINLEN", fadein)
            RPR_SetMediaItemInfo_Value(item, "C_FADEINSHAPE", fadeintype)
        if fadeout:
            RPR_SetMediaItemInfo_Value(item, "D_FADEOUTLEN", fadeout)
            RPR_SetMediaItemInfo_Value(item, "C_FADEOUTSHAPE", fadeouttype)

    def set_item_volume(self, item, volume):
        RPR_SetMediaItemInfo_Value(item, "D_VOL", volume)

    def create_marker(self, pos, name="", colour=None):
        colour_code = 0
        if colour:
            colour_code = RPR_ColorToNative(colour["r"], colour["g"], colour["b"]) | 0x1000000
        RPR_AddProjectMarker2(0, False, pos, 0.0, name, 0, colour_code)

    def build_project(self, data):
        self.insertion_track = self.create_track("Insertion")

        for track_data in data["tracks"]:
            track = self.create_track(track_data["name"])

            if "volume" in track_data:
                self.set_track_volume(track, track_data["volume"])
            if "panning" in track_data:
                self.set_track_panning(track, track_data["panning"])
            if "volume_envelope" in track_data:
                self.set_track_volume_envelope(track, track_data["volume_envelope"])
            if "panning_envelope" in track_data:
                self.set_track_panning_envelope(track, track_data["panning_envelope"])

            if "items" not in track_data: continue
            for item_data in track_data["items"]:
                item = self.create_item(
                    track,
                    item_data["source"],
                    item_data["offset"],
                    item_data["position"],
                    item_data["duration"]
                )
                if "fadein" in item_data or "fadeout" in item_data:
                    self.set_item_fades(
                        item,
                        item_data.get("fadein", None),
                        item_data.get("fadeout", None),
                        item_data.get("fadeintype", 0),
                        item_data.get("fadeouttype", 0)
                    )
                if "volume" in item_data:
                    self.set_item_volume(item, item_data["volume"])

        for marker_data in data["markers"]:
            self.create_marker(marker_data["position"], marker_data.get("name", ""), marker_data.get("colour", None))

        RPR_DeleteTrack(self.insertion_track)
        self.insertion_track = None



class AAFInterface:

    def __init__(self):
        self.aaf = None
        self.encoder = ""
        self.aaf_directory = ""
        self.essence_data = {}

    def open(self, filename):
        try:
            self.aaf = aaf2.open(filename, "r")
        except Exception:
            log("Could not open AAF file.", ERROR)
            return False
        try:
            self.encoder = self.aaf.header["IdentificationList"][0]["ProductName"].value
        except Exception:
            log("Unable to find file encoder", WARNING)
        self.aaf_directory = os.path.abspath(os.path.dirname(filename))
        self.essence_data = {}
        return True

    def build_wav(self, fname, data, depth=16, rate=48000, channels=1):
        with wave.open(fname, "wb") as f:
            f.setnchannels(channels)
            f.setsampwidth(int(depth / 8))
            f.setframerate(rate)
            f.writeframesraw(data)
            f.close()

    def aafrational_value(self, rational):
        return rational.numerator / rational.denominator

    def get_point_list(self, varying, duration):
        data = []
        for point in varying["PointList"]:
            data.append({
                "time": point.time * duration,
                "value": point.value
            })
        return data

    def get_linked_essence(self, mob):
        try:
            url = mob.descriptor.locator.pop()["URLString"].value
            # file:///C%3a/Users/user/My%20video.mp4
            url = urllib.parse.urlparse(url)
            url = url.netloc + url.path
            # /C%3a/Users/user/My%20video.mp4
            url = urllib.parse.unquote(url)
            # /C:/Users/user/My video.mp4
            url = urllib.request.url2pathname(url)
            # C:\\Users\\user\\My video.mp4

            # If the AAF was built on another computer,
            # chances are the paths will differ.
            # Typically the source files are in the same directory as the AAF.
            if not os.path.isfile(url):
                local = os.path.join(self.aaf_directory, os.path.basename(url))
                if os.path.isfile(local):
                    url = local
            return url

        except Exception:
            log("Error retrieving file url for %s" % mob.name, WARNING)
            return ""

    def extract_embedded_essence(self, mob, filename):
        log("Extracting essence %s..." % filename)
        stream = mob.essence.open()
        data = stream.read()
        stream.close()

        meta = mob.descriptor
        data_fmt = meta["ContainerFormat"].value.name if "ContainerFormat" in meta else ""
        if data_fmt == "MXF":
            sample_depth = meta["QuantizationBits"].value
            sample_rate = meta["SampleRate"].value
            sample_rate = self.aafrational_value(sample_rate)
            self.build_wav(filename, data, sample_depth, sample_rate)
        else:
            with open(filename, "wb") as f:
                f.write(data)
                f.close()

        return filename

    def extract_essence(self, target, callback):
        for master_mob in self.aaf.content.mastermobs():
            self.essence_data[master_mob.name] = {}
            for slot in master_mob.slots:

                if isinstance(slot.segment, aaf2.components.Sequence):
                    source_mob = None
                    for component in slot.segment.components:
                        if isinstance(component, aaf2.components.SourceClip):
                            source_mob = component.mob
                            break
                    else:
                        self.essence_data[master_mob.name][slot.slot_id] = ""
                        log("Cannot find essence for %s slot %d" % (master_mob.name, slot.slot_id), WARNING)
                elif isinstance(slot.segment, aaf2.components.SourceClip):
                    source_mob = slot.segment.mob

                if slot.segment.media_kind == "Picture":
                    # Video files cannot be embedded in the AAF.
                    self.essence_data[master_mob.name][slot.slot_id] = self.get_linked_essence(source_mob)
                    continue
                if source_mob.essence:
                    filename = os.path.join(target, master_mob.name + slot.name + ".wav")
                    if callback:
                        callback("Extracting %s..." % (master_mob.name + slot.name + ".wav"))
                    self.essence_data[master_mob.name][slot.slot_id] = self.extract_embedded_essence(source_mob, filename)
                else:
                    self.essence_data[master_mob.name][slot.slot_id] = self.get_linked_essence(source_mob)

    def get_essence_file(self, mob_name, slot_id):
        try:
            return self.essence_data[mob_name][slot_id]
        except Exception:
            log("Cannot find essence for %s slot %d" % (mob_name, slot_id), WARNING)
            return ""

    def get_embedded_essence_count(self):
        count = 0
        for master_mob in self.aaf.content.mastermobs():
            for slot in master_mob.slots:
                if isinstance(slot.segment, aaf2.components.Sequence):
                    source_mob = None
                    for component in slot.segment.components:
                        if isinstance(component, aaf2.components.SourceClip):
                            source_mob = component.mob
                            break
                    else:
                        continue
                elif isinstance(slot.segment, aaf2.components.SourceClip):
                    source_mob = slot.segment.mob
                if slot.segment.media_kind == "Sound" and source_mob.essence:
                    count += 1
        return count


    # Instead of using per-item volume curves (aka take volume envelope),
    # we collect data from items and "render" it to the track volume envelope.
    def collect_vol_pan_automation(self, track):
        envelopes = {
            "volume_envelope": [],
            "panning_envelope": []
        }
        for envelope in envelopes:
            for item in track["items"]:
                if envelope in item:
                    for point in item[envelope]:
                        envelopes[envelope].append({
                            "time": item["position"] + point["time"],
                            "value": point["value"]
                        })
                    del item[envelope]
                else:
                    if not envelopes[envelope]: continue
                    # We don't want items without automation to be affected
                    # by automation added by other items
                    envelopes[envelope].append({
                        "time": item["position"],
                        "value": 1.0
                    })
                    envelopes[envelope].append({
                        "time": item["position"] + item["duration"],
                        "value": 1.0
                    })

        # Add only if not empty
        if envelopes["volume_envelope"]:
            track["volume_envelope"] = envelopes["volume_envelope"]
        if envelopes["panning_envelope"]:
            track["panning_envelope"] = envelopes["panning_envelope"]

        return track

    # Function is meant to be called recursively.
    # It is supposed to gather whatever information it can and pass it to
    # its caller, who will append the new data to its own.
    # The topmost caller sets "position" and "duration", as well as fades,
    def parse_operation_group(self, group, edit_rate):

        item = {}

        # We could base volume envelope extraction on either group.operation.name
        # or group.parameters[].name depending on which is more prone to be constant.
        # For now both conditions have to be met, which may cause some automation to
        # be ignored if other software picks different operation or parameter names.
        if group.operation.name in ["Mono Audio Gain", "Audio Gain"]:
            for p in group.parameters:
                if p.name not in ["Amplitude", "Amplitude multiplier", "Level"]: continue
                if isinstance(p, aaf2.misc.VaryingValue):
                    item["volume_envelope"] = self.get_point_list(p, group.length / edit_rate)
                elif isinstance(p, aaf2.misc.ConstantValue):
                    item["volume"] = self.aafrational_value(p.value)

        if group.operation.name == "Mono Audio Pan":
            for p in group.parameters:
                points = self.get_point_list(p, group.length / edit_rate)
                if p.name == "Pan value":
                    item["panning_envelope"] = [{
                        "time": point["time"],
                        "value": point["value"] * -2 + 1
                    } for point in points]

        if group.operation.name == "Audio Effect":
            for p in group.parameters:
                if p.name == "":
                    # Vegas/MC saves per-item volume and panning automation
                    # but I haven't figured out a way to find out which is which
                    # since the parameter name is blank.
                    pass
                if p.name == "SpeedRatio":
                    item["playbackrate"] = self.aafrational_value(p.value)

        segment = group.segments[0]

        # Aaaargh, why is this a thing?
        if isinstance(segment, aaf2.components.Sequence):
            segment = segment.components[0]

        if isinstance(segment, aaf2.components.OperationGroup):
            item.update(self.parse_operation_group(segment, edit_rate))
        elif isinstance(segment, aaf2.components.SourceClip):
            item.update({
                "source": self.get_essence_file(segment.mob.name, segment.slot_id),
                "offset": segment.start / edit_rate,
            })

        return item

    def parse_sequence(self, sequence, edit_rate):
        items = []
        time = 0.0
        fade = 0  # 0 = no fade, 1 = fade, -1 = last component was filler
        fade_length = None
        fade_type = 0  # 0 = linear, 1 = power

        for component in sequence.components:
            try:
                duration = component.length / edit_rate

                if isinstance(component, aaf2.components.SourceClip):
                    item = {
                        "source": self.get_essence_file(component.mob.name, component.slot_id),
                        "offset": component.start / edit_rate,
                        "position": time,
                        "duration": duration,
                    }
                    if fade == 1:
                        item["fadein"] = fade_length
                        item["fadeintype"] = fade_type
                    fade = 0
                    items.append(item)
                    time += duration

                elif isinstance(component, aaf2.components.OperationGroup):
                    item = {
                        "position": time,
                        "duration": duration
                    }
                    item.update(self.parse_operation_group(component, edit_rate))
                    if fade == 1:
                        item["fadein"] = fade_length
                        item["fadeintype"] = fade_type
                    fade = 0

                    if "source" not in item:
                        log("Failed to find item source at %f seconds." % time, WARNING)
                        item["source"] = ""
                    if "offset" not in item:
                        log("Failed to find item offset at %f seconds." % time, WARNING)
                        item["offset"] = 0

                    items.append(item)
                    time += duration

                elif isinstance(component, aaf2.components.Transition):
                    fade_length = duration
                    fade_type = 0
                    try:
                        if component["OperationGroup"].value.parameters.value[0].interpolation.name == "PowerInterp":
                            fade_type = 1
                    except Exception:
                        pass
                    if fade == 0:
                        items[-1]["fadeout"] = fade_length
                        items[-1]["fadeouttype"] = fade_type
                    if fade != 1:
                        fade = 1
                    time -= duration

                elif isinstance(component, aaf2.components.Filler):
                    fade = -1
                    time += duration

            except Exception:
                log("Failed to parse component at %f seconds." % time)

        return items

    def get_picture_tracks(self, slot):
        data = []
        edit_rate = self.aafrational_value(slot.edit_rate)

        if isinstance(slot.segment, aaf2.components.NestedScope):
            for sequence in slot.segment.slots.value:
                seq_data = self.parse_sequence(sequence, edit_rate)
                if seq_data:
                    data.append({
                        "name": "",
                        "items": seq_data
                    })
        elif isinstance(slot.segment, aaf2.components.Sequence):
            seq_data = self.parse_sequence(slot.segment, edit_rate)
            if seq_data:
                data.append({
                    "name": slot.name,
                    "items": seq_data
                })

        return data

    def get_sound_track(self, slot):
        data = {
            "name": slot.name
        }
        edit_rate = self.aafrational_value(slot.edit_rate)
        segment = slot.segment
        if isinstance(segment, aaf2.components.OperationGroup):
            # Maybe we should check for segment.operation.name as well?
            for p in segment.parameters:
                if p.name == "Pan value":
                    data["panning"] = self.aafrational_value(p.value) * 2 - 1
                if p.name in ["Pan", "Pan Level"]:
                    # Sometimes segment.length is wrong so we have to use
                    # the length of the data segment instead.
                    real_length = segment.length / edit_rate
                    if self.encoder == "DaVinci Resolve":
                        real_length = segment.segments[0].length / edit_rate
                    points = self.get_point_list(p, real_length)
                    data["panning_envelope"] = [{
                        "time": point["time"],
                        "value": point["value"] * -2 + 1
                        # Reaper can't make up its mind 
                    } for point in points]
            data["items"] = self.parse_sequence(segment.segments[0], edit_rate)
        elif isinstance(segment, aaf2.components.Sequence):
            data["items"] = self.parse_sequence(segment, edit_rate)
        return data

    def get_markers(self, slot):
        markers = []
        edit_rate = self.aafrational_value(slot.edit_rate)
        for component in slot.segment.components:
            marker = {
                "name": component["Comment"].value,
                "position": component["Position"].value / edit_rate
            }
            if "CommentMarkerColour" in component:
                col = component["CommentMarkerColour"].value
                marker["colour"] = {
                    "r": int(col["red"] / 256),
                    "g": int(col["green"] / 256),
                    "b": int(col["blue"] / 256)
                }
            markers.append(marker)
        return markers

    def get_composition_list(self):
        return [composition.name for composition in self.aaf.content.compositionmobs()]

    def get_composition(self, composition):
        data = {
            "tracks": [],
            "markers": []
        }

        for slot in list(self.aaf.content.compositionmobs())[composition].slots:
            try:
                if slot.media_kind == "Picture":
                    picture_tracks = self.get_picture_tracks(slot)
                    if picture_tracks:
                        data["tracks"] += picture_tracks
                elif slot.media_kind in ["Sound", "LegacySound"]:
                    track_data = self.get_sound_track(slot)
                    track_data = self.collect_vol_pan_automation(track_data)
                    data["tracks"].append(track_data)
                elif slot.media_kind == "DescriptiveMetadata":
                    data["markers"] += self.get_markers(slot)
            except Exception:
                log("Failed parsing slot %s" % slot.name, WARNING)
        return data

    def get_aaf_metadata(self):
        try:
            identity = self.aaf.header["IdentificationList"][0]
            return {
                "company": identity["CompanyName"].value,
                "product": identity["ProductName"].value,
                "version": identity["ProductVersionString"].value,
                "date": identity["Date"].value,
                "platform": identity["Platform"].value
            }
        except Exception:
            warn("Could not get file identity metadata.", WARNING)
            return {}



class UserInteraction:

    @staticmethod
    def show_progressbar(item_count, action):

        def update_call(message):
            if len(message) > 50:
                message = message[:48] + "..."
            try:
                label.config(text=message)
                progressbar.step()
                progressbar.update()
            except Exception:
                # User closed the window, probably
                pass

        window = tkinter.Tk()
        window.title("Importing...")
        window.columnconfigure(0, weight=1)

        frame = tkinter.Frame(window, borderwidth=10)
        frame.grid(column=0, row=0, sticky="NWSE")
        frame.columnconfigure(0, weight=1)

        label = tkinter.Label(frame, text="")
        label.grid(column=0, row=0, sticky="NW")

        progressbar = tkinter.ttk.Progressbar(frame, mode="determinate", maximum=item_count+1, length=500)
        progressbar.grid(column=0, row=1, sticky="WE")

        action(update_call)
        try:
            window.destroy()
            window.mainloop()
        except Exception:
            pass

    @staticmethod
    def get_composition(composition_list):
        if have_reaper:
            if have_tk:
                return UserInteraction.get_composition_gui(composition_list)
            else:
                return UserInteraction.get_composition_awkward(composition_list)
        else:
            return UserInteraction.get_composition_cli(composition_list)

    @staticmethod
    def get_composition_cli(composition_list):
        print("Select composition to parse:")
        for i, t in enumerate(composition_list):
            print("%d. %s" % (i, t))
        while True:
            try:
                composition_id = int(input("> "))
                composition_list[composition_id]
                break
            except Exception:
                print("Invalid input.")
        return composition_id

    @staticmethod
    def get_composition_gui(composition_list):

        selection = 0

        def ok_callback():
            nonlocal selection
            selection = listbox.curselection()[0]
            window.destroy()

        def doubleclick_callback(e):
            ok_callback()

        window = tkinter.Tk()
        window.title("Select composition")
        window.rowconfigure(0, weight=1)
        window.columnconfigure(0, weight=1)

        frame = tkinter.Frame(window, borderwidth=10)
        frame.grid(column=0, row=0, sticky="NWSE")
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(1, weight=1)

        label_text = "AAF contains multiple compositions. Select which one to import:"
        label = tkinter.Label(frame, text=label_text)
        label.grid(column=0, row=0, sticky="NW")

        listbox = tkinter.Listbox(frame)
        for comp in composition_list:
            listbox.insert("end", comp)
        listbox.selection_set(0)
        listbox.see(0)
        listbox.activate(0)
        listbox.bind('<Double-1>', doubleclick_callback)
        listbox.grid(column=0, row=1, sticky="NWSE", pady=10)

        button = tkinter.Button(frame, text="OK", command=ok_callback)
        button.grid(column=0, row=2, sticky="S")

        window.mainloop()
        return selection

    @staticmethod
    def get_composition_awkward(composition_list):
        while True:
            for i, comp in enumerate(composition_list):
                message = "Do you want to import composition %s?" % comp
                result = RPR_MB(message, "Select composition", 4)
                if result == 6:
                    return i

def import_aaf():
    global log_level

    aaf_interface = AAFInterface()
    reaper_interface = ReaperInterface()

    if have_reaper:
        filename = reaper_interface.select_aaf()
        if filename is None: return
        target = reaper_interface.get_project_directory()
    else:
        if len(sys.argv) < 2:
            log("No input file provided.", ERROR)
            return
        filename = sys.argv[1]
        target = "sources"
        if not os.path.exists(target):
            os.mkdir(target)
        log_level = NOTICE

    if not aaf_interface.open(filename): return
    log("geting data from %s..." % filename)
    meta = aaf_interface.get_aaf_metadata()
    log("AAF created on %s with %s %s version %s using %s" % 
        (str(meta["date"]), meta["company"], meta["product"], meta["version"], meta["platform"])
    )

    if have_tk:
        def action(update):
            aaf_interface.extract_essence(target, update)
        count = aaf_interface.get_embedded_essence_count()
        UserInteraction.show_progressbar(count, action)
    else:
        aaf_interface.extract_essence(target, None)

    composition_list = aaf_interface.get_composition_list()
    composition_id = 0
    if len(composition_list) > 1:
        composition_id = UserInteraction.get_composition(composition_list)
    composition = aaf_interface.get_composition(composition_id)

    if have_reaper:
        reaper_interface.build_project(composition)
    else:
        print(json.dumps(composition))

if __name__ == "__main__":
    # sys.exit() or exit() would crash the script, so instead
    # we're using `return` within a main function
    import_aaf()

Last edited by gapalil001; 05-23-2022 at 04:08 AM.
gapalil001 is offline   Reply With Quote
Old 05-04-2022, 10:38 AM   #43
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default

Quote:
Originally Posted by gapalil001 View Post
i did, it told me "command not found". i'd also download pip and have to find the way to install this
I ran into this problem too. When I downloaded Python 3.10, it installed pip3. So that was the command I used- "pip3" instead of "pip". Then pyaaf installed correctly.
mattsoule is offline   Reply With Quote
Old 05-04-2022, 11:03 AM   #44
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by mattsoule View Post
I ran into this problem too. When I downloaded Python 3.10, it installed pip3. So that was the command I used- "pip3" instead of "pip". Then pyaaf installed correctly.
also this helps a lot with pip for dumb users like me
gapalil001 is offline   Reply With Quote
Old 05-04-2022, 11:42 AM   #45
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default

@gapalil001

We have a winner on MAC OSX 11.6.5!!

Well done. And well done to OP for this great script.
mattsoule is offline   Reply With Quote
Old 05-04-2022, 11:47 AM   #46
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by mattsoule View Post
@gapalil001

We have a winner on MAC OSX 11.6.5!!

Well done. And well done to OP for this great script.
Great! As i undestand from Ed’s words - it’s fast fix for macOS and needs to be reviewed (and changed) by @pterodox for finally properly working
gapalil001 is offline   Reply With Quote
Old 05-04-2022, 04:33 PM   #47
cejotabass
Human being with feelings
 
Join Date: Apr 2022
Posts: 1
Default

Quote:
Originally Posted by pterodox View Post
Hey. Thanks for checking out the script. Are you sure you are running the script using Python 3? Such error message would typically show up under Python 2.
Hi! I got the same error. So far I know I think I have to python versions installed, but I don't know what folder is and cant find the right one to redirect Reaper.

I don't know anything about Python and it's been days trying to solve this on my own with no luck.

Any Idea where the folder is on OSX 11.6.5 ?? The one that says .dylib path

Thank in advance! Sorry if I miss something in the thread.
cejotabass is offline   Reply With Quote
Old 05-05-2022, 12:47 AM   #48
oudi le oudi
Human being with feelings
 
Join Date: Jun 2018
Location: Paris
Posts: 22
Default

thank you for your script :-)
and for fix the mac problem...
but no tkinter window in my mac...

try from big aaf from final cut.....;no problem
don't work from big aaf from avid media composer
but i will try again.


Script execution error

WARNING:root:fat sector count missmatch difat: 1132 header: 693
WARNING:root:range lock sector has data
Traceback (most recent call last):
File "importaaf.py", line 734, in <module>
import_aaf()
File "importaaf.py", line 718, in import_aaf
aaf_interface.extract_essence(target, None)
File "importaaf.py", line 269, in extract_essence
filename = os.path.join(target, master_mob.name + slot.name + ".wav")
TypeError: can only concatenate str (not "NoneType") to str
oudi le oudi is offline   Reply With Quote
Old 05-05-2022, 01:39 PM   #49
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default

Quote:
Originally Posted by cejotabass View Post
Hi! I got the same error. So far I know I think I have to python versions installed, but I don't know what folder is and cant find the right one to redirect Reaper.

I don't know anything about Python and it's been days trying to solve this on my own with no luck.

Any Idea where the folder is on OSX 11.6.5 ?? The one that says .dylib path

Thank in advance! Sorry if I miss something in the thread.

If python3 is installed properly, then it will be in:
/Library/Frameworks/Python.framework/Versions/3.10/lib

This is what you will put in the Reaper Reascript preferences in the "Custom Path to python.dll" field

Then you would put libpython3.10.dylib (or whatever version of python3 you installed) in the "Force Reascript to use specific Python .dylib
mattsoule is offline   Reply With Quote
Old 05-07-2022, 07:19 AM   #50
oudi le oudi
Human being with feelings
 
Join Date: Jun 2018
Location: Paris
Posts: 22
Default

hi
do you think it's possible to keep volume and gain from aaf.
i've try from pro tools and final cut the aaf works but no volume or gain.

and if it's possible to send aaf from reaper your script will be perfect.
thank you very much for your share and your work.
best
jf

edit: look ok with final cut....

Last edited by oudi le oudi; 05-09-2022 at 05:49 AM. Reason: error
oudi le oudi is offline   Reply With Quote
Old 05-09-2022, 10:03 AM   #51
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by oudi le oudi View Post
hi
do you think it's possible to keep volume and gain from aaf.
i've try from pro tools and final cut the aaf works but no volume or gain.

and if it's possible to send aaf from reaper your script will be perfect.
thank you very much for your share and your work.
best
jf

edit: look ok with final cut....
i have properly good AAF with envelopes if re-export AAF from Logic pro. also, Logic fixes several issues with some AAF sessions (seems it's capability issue, because vordio do the same mistakes). for me, Logic is "doctor" before i'll import AAF to reaper

[EDITED]

The main issue is happens to material that recorded to multichannel as single wav file (zoom, sounddevices). final cut and logic are solve this issue, but Premiere Pro doesn't. on my screenshot above (AAF directly from Premiere Pro) you will see that top three tracks are the same, but shouldn't be. Vordio have the same issue

Last edited by gapalil001; 05-12-2022 at 12:22 AM.
gapalil001 is offline   Reply With Quote
Old 05-13-2022, 03:57 AM   #52
oudi le oudi
Human being with feelings
 
Join Date: Jun 2018
Location: Paris
Posts: 22
Default

hi
thanx for reply...
same problem with aaf from protools open with logic and re exported to reaper.
no gain or volume.
vordio have some problem too .
but i've just have try with limitation of demo.
and i've you fine an issu for fix the prob with tkinter?
i've no window from reaper but the script work.
my knowledge in lua and python are too weak to help .
oudi le oudi is offline   Reply With Quote
Old 05-13-2022, 08:43 AM   #53
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

Hmm, what do I do, when I could open an AAF but nothing happens? Is there a log file somehwere?
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 05-13-2022, 09:16 AM   #54
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by _Stevie_ View Post
Hmm, what do I do, when I could open an AAF but nothing happens? Is there a log file somehwere?
do you use macOS? if yes let's have a look for solution in my post 42 above. (you will know what to do)
P.S. thank you a lot for supporting us!
gapalil001 is offline   Reply With Quote
Old 05-13-2022, 12:37 PM   #55
_Stevie_
Human being with feelings
 
_Stevie_'s Avatar
 
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
Default

Quote:
Originally Posted by gapalil001 View Post
do you use macOS? if yes let's have a look for solution in my post 42 above. (you will know what to do)
Hey gapalil!

Yes, it's my macOS machine
And yeah, the first thing I did was copying your code and replaced the original script :P
But somehow, nothing happens after I clicked on the AAF.
I guess that's not expected? I'm running Catalina on that machine.

Quote:
Originally Posted by gapalil001 View Post
P.S. thank you a lot for supporting us!
You are more than welcome! <3
__________________
My Reascripts forum thread | My Reascripts on GitHub
If you like or use my scripts, please support the Ukraine: Ukraine Crisis Relief Fund | DirectRelief | Save The Children | Razom
_Stevie_ is offline   Reply With Quote
Old 05-13-2022, 12:52 PM   #56
gapalil001
Human being with feelings
 
gapalil001's Avatar
 
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
Default

Quote:
Originally Posted by _Stevie_ View Post
Hey gapalil!

Yes, it's my macOS machine
I did was copying your code
not actually my
Quote:
I guess that's not expected? I'm running Catalina on that machine.
it may be an issue, i am on Monterey and have two completed projects. i am not sure, but you may look up on python (and libraries) specs for different systems, but seems you may know it's better than me. i had the same issue as yours with original code
gapalil001 is offline   Reply With Quote
Old 05-14-2022, 03:26 AM   #57
AxelFox
Human being with feelings
 
Join Date: Dec 2019
Location: Cave (RM)
Posts: 1
Default Script exe error

Script execution error

Traceback (most recent call last):
File "importaaf.py", line 732, in <module>
import_aaf()
File "importaaf.py", line 714, in import_aaf
UserInteraction.show_progressbar(count, action)
File "importaaf.py", line 601, in show_progressbar
action(update_call)
File "importaaf.py", line 712, in action
aaf_interface.extract_essence(target, update)
File "importaaf.py", line 266, in extract_essence
if source_mob.essence:
AttributeError: 'NoneType' object has no attribute 'essence'

Hello! I'm on windows, last reaper update. Everything is installed in the correct way (i think), Reaper is seeing python310.dll, pyaaf2 installed, script imported in the action list.
Am i missing something?
Thanks and have a good day!!
AxelFox is offline   Reply With Quote
Old 05-27-2022, 09:57 AM   #58
rubendax
Human being with feelings
 
Join Date: Aug 2021
Posts: 2
Default

I've been really trying to make this work. I've tried on three different Mac computers, system 10.14.6, 10.15.7, and 12.3.1. I have experienced the same behavior on all three machines. Running the regular script from gitlab https://gitlab.com/skysphr/reaper-aaf just seems to do nothing after selecting the aaf. The alternative script from post #42 results in this script execution error:

WARNING:root:fat sector count missmatch difat: 1132 header: 273
WARNING:root:range lock sector has data
Traceback (most recent call last):
File "importaaf.py", line 717, in <module>
import_aaf()
File "importaaf.py", line 701, in import_aaf
aaf_interface.extract_essence(target, None)
File "importaaf.py", line 252, in extract_essence
filename = os.path.join(target, master_mob.name + slot.name + ".wav")
TypeError: can only concatenate str (not "NoneType") to str
rubendax is offline   Reply With Quote
Old 06-01-2022, 01:40 PM   #59
marina
Human being with feelings
 
Join Date: May 2022
Posts: 1
Default Reaper 6.58 doesn't recognise a python 3

Quote:
Originally Posted by pterodox View Post
Hey. Thanks for checking out the script. Are you sure you are running the script using Python 3? Such error message would typically show up under Python 2.
Firstly, when I had noticed that reaper didn't recognise python3 installed, I tried to integrate python2 into ReaScript. I've installed pyaaf2 via python2 pip (the process was completed successfuly, there just was a notification that a pip version was out of date. But the error ocurred again and I've installed a virtualenv. After that a new directory with python folder appeared: user/Library/Python/2.7. Before that there was the only directory SSD/Library/Frameworks/Python.framework//Versions/3.10 or 2.7
After I have forced Reaper to use a library directory libpython3.10.dylib a new error occured. Now the problem is:
"Script execution error
Traceback (most recent call last)
File "import.py", line 3, in <module>
import aaf
ModuleNotFoundError: No module named 'aaf2'
As I've understood now I have to reinstall pyaaf2 with python3 pip. But when I request a current version number via Terminal it informs, that there is "xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
[Process completed]"
What should I do now to solve that problem? Thank you in advance. And all the best!
marina is offline   Reply With Quote
Old 07-14-2022, 10:26 AM   #60
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default PT AAF to Reaper OSX

Hi all. I wanted to revisit this thread again because of the great potential of the script. Our studio is Reaper based (OBVI) and we anticipate having to work with PT sessions from vendors and other post houses. AAF will definitely be the way to go.

I have been testing AAF using AATranslator, but it seems there have been a lot of sync issues and fade issues. However, I have been able to import stereo files that contain clip/item gain info and automation.

Using this script the fades and the sync seem to be pretty solid. However, no clip/item gain, and no volume automation and Mono only.

I was importing with the audio not embedded. So no audio would come in, and I'd have to relink by restarting the session and pointing Reaper in the right direction.

So, I wanted to see if there was any potential to get to the bottom of those issues for Pro Tools AAF import on MAC OSX (Monterey):

1. Stereo files
2. Clip item gain
3. Automation

I wish I was a better scriptor. . Alas. . . I am not. At all. This community is awesome, and I'd love to see if we can figure it out!
mattsoule is offline   Reply With Quote
Old 07-14-2022, 11:02 AM   #61
MonkeyBars
Human being with feelings
 
MonkeyBars's Avatar
 
Join Date: Feb 2016
Location: Hollyweird
Posts: 2,630
Default

Just a heads-up that Vordio has worked great for me to import AAFs (from DaVinci Resolve) into Reaper, and John the developer provides excellent support.
MonkeyBars is offline   Reply With Quote
Old 07-15-2022, 08:24 AM   #62
Runaway
Human being with feelings
 
Runaway's Avatar
 
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
Default

Quote:
Originally Posted by mattsoule View Post
I have been testing AAF using AATranslator, but it seems there have been a lot of sync issues and fade issues.
That shouldn't happen
If you want to either PM or email me info@aatranslator.com.au I'm sure we can get that sorted
__________________
AATranslator
Runaway is offline   Reply With Quote
Old 07-15-2022, 10:09 AM   #63
Minutes to Midnighht
Human being with feelings
 
Minutes to Midnighht's Avatar
 
Join Date: Mar 2021
Location: United Kingdom
Posts: 6
Default

Quote:
Originally Posted by gapalil001 View Post
thank you, it works!
Amazing, thanks, it works on my macOS Monterey as well.

Last edited by Minutes to Midnighht; 07-18-2022 at 06:06 AM.
Minutes to Midnighht is offline   Reply With Quote
Old 07-27-2022, 04:35 PM   #64
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default

Quote:
Originally Posted by Runaway View Post
That shouldn't happen
If you want to either PM or email me info@aatranslator.com.au I'm sure we can get that sorted
So, of course this was user error (MY FAULT). After having AAT properly installed the .ptx to .RPP translation is pretty out of sight. Very impressive. Other than a clip gain issue, this looks solid.
mattsoule is offline   Reply With Quote
Old 08-02-2022, 02:26 AM   #65
Runaway
Human being with feelings
 
Runaway's Avatar
 
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
Default

Quote:
Originally Posted by mattsoule View Post
So, of course this was user error (MY FAULT). After having AAT properly installed the .ptx to .RPP translation is pretty out of sight. Very impressive. Other than a clip gain issue, this looks solid.
From memory this was a PT2022 PTX and that clip gain issue should now be sorted - AVID's new full-time sport is to keep me on my toes LOL
__________________
AATranslator
Runaway is offline   Reply With Quote
Old 08-02-2022, 12:19 PM   #66
mattsoule
Human being with feelings
 
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
Default

Quote:
Originally Posted by Runaway View Post
From memory this was a PT2022 PTX and that clip gain issue should now be sorted - AVID's new full-time sport is to keep me on my toes LOL
Yeah, this is sorted all right. Really impressed. Clip gain and clip automation (which translates to take automation in Reaper) both come over nicely.

I really like that when there is multichannel pan automation in the PT session, reasurroundpan is put on the tracks with said automation.

Also, the Pro tools channel order for interleaved surround files (film mode) is adjusted to SMPTE layout in Reaper. Really great. Congrats!
mattsoule is offline   Reply With Quote
Old 08-03-2022, 12:48 AM   #67
Runaway
Human being with feelings
 
Runaway's Avatar
 
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
Default

Quote:
Originally Posted by mattsoule View Post
Really great. Congrats!
Appreciate the feedback
__________________
AATranslator
Runaway is offline   Reply With Quote
Old 09-02-2022, 06:32 AM   #68
Tim Rideout
Human being with feelings
 
Tim Rideout's Avatar
 
Join Date: Jan 2013
Location: Montreal, Canada
Posts: 258
Default

AATranslator... Vordio... and now this script! It is indeed nice to have AAF options for Reaper. AAF is *such* a huge pain in my ass. Why doesn't everybody just use REAPER. Like, for everything**

__________________
---
www.TimRideout.com
Tim Rideout is offline   Reply With Quote
Old 09-03-2022, 01:17 PM   #69
dfix
Human being with feelings
 
Join Date: Jan 2021
Posts: 2
Default

Script execution error

Traceback (most recent call last):
File "/Applications/REAPER.app/Contents/Plugins/reaper_python.py", line 2, in <module>
from ctypes import *
File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/ctypes/__init__.py", line 8, in <module>
from _ctypes import Union, Structure, Array
TypeError: attribute name must be string, not 'str'

What and where could I have made a mistake? help kind people
dfix is offline   Reply With Quote
Old 09-20-2022, 10:17 AM   #70
Wheelbreak
Human being with feelings
 
Join Date: Jun 2022
Posts: 1
Default same error as AxelFox

I am doing a one off to help a friend mix a short movie so I really don't want to drop $200 on a program to use once. This script seems perfect for what I need.
After a little confusion in windows 10 cmd I got the newest python installed along with the newest version of reaper (maybe this is the problem?)
I run the script and about half way through I get the error at the bottom of this post and it just freezes. I hope that work is still being done on this script as it seems like an amazing solution.
The AAF is coming out of Premiere which I see some people say it has some issues with and it uses audio recorded with a tascam recorder. In fact the file it freezes on says
this in the Importing progress bar

"Extracting*TASCAM_0366S34.wavA Slot.wav..."

could the file type or name be the problem?

Here is a copy paste of the error

Script execution error

WARNING:root:fat sector count missmatch difat: 9380 header: 9294
Traceback (most recent call last):
File "importaaf.py", line 732, in <module>
import_aaf()
File "importaaf.py", line 714, in import_aaf
UserInteraction.show_progressbar(count, action)
File "importaaf.py", line 601, in show_progressbar
action(update_call)
File "importaaf.py", line 712, in action
aaf_interface.extract_essence(target, update)
File "importaaf.py", line 270, in extract_essence
self.essence_data[master_mob.name][slot.slot_id] = self.extract_embedded_essence(source_mob, filename)
File "importaaf.py", line 239, in extract_embedded_essence
with open(filename, "wb") as f:
OSError: [Errno 22] Invalid argument: 'C:\\Users\\Jared Preston\\Documents\\REAPER Media\\*TASCAM_0366S34.wavA Slot.wav'
Wheelbreak is offline   Reply With Quote
Old 09-27-2022, 04:16 PM   #71
buddhajuke
Human being with feelings
 
Join Date: Jun 2012
Posts: 277
Default

Quote:
Originally Posted by buyrunvelocity View Post
Thanks a lot for your effort.
Lack of Cross Platform file import-export seems like the bottle neck for Reapers Audio Post Production capabilities.
I am a happy Vordio user but it would be great to import-export AAF near natively with script.
However I could not make it work. This is the error massage.
Very exiting post thanks again.

Script execution error

Traceback (most recent call last):
File "importaaf.py", line 638
nonlocal selection
^
SyntaxError: invalid syntax
I have this problem as well. I don't think it was addressed yet
buddhajuke is offline   Reply With Quote
Old 10-14-2022, 11:28 AM   #72
Paulo1708
Human being with feelings
 
Join Date: Oct 2022
Posts: 1
Default Looped item

I really liked the script you created and I was really excited to use it in my work but I had a problem.
When importing an AAF (exported from Premiere) it could not interpret the cuts made by the video editor in the audio file.
The script ended up taking the first audio item and looping it until the end of the video.

Here is the link to the folder with the Reaper project with the imported AAF and also the AAF file itself.
https://www.dropbox.com/sh/ud3mqnwdm...d_adf0Esa?dl=0

Note: I tested importing it in other programs and the AAF worked just fine. That's why I'm reporting this bug.
Attached Images
File Type: png AAF LOOP.PNG (43.9 KB, 126 views)
Paulo1708 is offline   Reply With Quote
Old 10-17-2022, 04:50 PM   #73
ryanisprism
Human being with feelings
 
Join Date: Mar 2018
Posts: 1
Default

Im having the same issues as the user above. At all edit points, the WAV file loops.
ryanisprism is offline   Reply With Quote
Old 10-18-2022, 04:11 AM   #74
Marenta
Human being with feelings
 
Join Date: Mar 2021
Posts: 6
Default

also having the same issues

M
Marenta is offline   Reply With Quote
Old 11-16-2022, 04:12 PM   #75
Kostas Stylianou
Human being with feelings
 
Join Date: Jun 2021
Posts: 2
Default New Code didn't work for me sadly

[QUOTE=gapalil001;2554214]thank you, it works! also pyaaf2 installed properly! but it didn't affects my issue in any way =( also @mattsoule have the same issue i guess

completely the same here

[edited]

some guys told that perhaps script don't works on macs because of Tkinter won't work on macs. also they recommend to try Gooey

[edited again]

FIXED!!!! by @edkashinsky
tested by me on macOS 12.3.1
Code:
#!/bin/python

import aaf2
import os
import sys
import wave
import urllib.parse
import urllib.request
import pprint
import json

have_reaper = True  
have_tk = False

[NOTICE, WARNING, ERROR, NONE] = range(4)
log_level = WARNING

def log(message, level=NOTICE):
    if log_level > level: return
    if have_reaper:
        RPR_ShowConsoleMsg(message + "\n")
    else:
        print(message)



class ReaperInterface:

    def __init__(self):
        self.insertion_track = None

    def select_aaf(self):
        ok, filename, _, _ = RPR_GetUserFileNameForRead("", "Import AAF", ".aaf")
        if not ok: return None
        return filename

    def get_project_directory(self):
        directory, _ = RPR_GetProjectPath("", 512)
        return directory

    def create_track(self, name):
        track_index = RPR_GetNumTracks()
        RPR_InsertTrackAtIndex(track_index, False)
        track = RPR_GetTrack(0, track_index)
        RPR_GetSetMediaTrackInfo_String(track, "P_NAME", name, True)
        return track

    def set_track_volume(self, track, volume):
        RPR_SetMediaTrackInfo_Value(track, "D_VOL", volume)

    def set_track_volume_envelope(self, track, volume_data):
        RPR_SetOnlyTrackSelected(track)
        RPR_Main_OnCommand(40406, 0)  # ReaSlang for "toggle volume envelope visible"
        envelope = RPR_GetTrackEnvelopeByName(track, "Volume")
        for point in volume_data:
            value = RPR_ScaleToEnvelopeMode(1, point["value"])
            RPR_InsertEnvelopePoint(envelope, point["time"], value, 0, 0.0, False, True)
        RPR_Envelope_SortPoints(envelope)

    def set_track_panning(self, track, panning):
        RPR_SetMediaTrackInfo_Value(track, "D_PAN", panning)

    def set_track_panning_envelope(self, track, panning_data):
        RPR_SetOnlyTrackSelected(track)
        RPR_Main_OnCommand(40407, 0)  # Toggle pan envelope visible
        envelope = RPR_GetTrackEnvelopeByName(track, "Pan")
        for point in panning_data:
            RPR_InsertEnvelopePoint(envelope, point["time"], point["value"], 0, 0.0, False, True)
        RPR_Envelope_SortPoints(envelope)

    def create_item(self, track, src, offset, pos, dur):
        RPR_SetOnlyTrackSelected(self.insertion_track)
        RPR_MoveEditCursor(-1000, False)
        RPR_InsertMedia(src, 0)
        item = RPR_GetTrackMediaItem(self.insertion_track, 0)
        RPR_MoveMediaItemToTrack(item, track)
        RPR_SetMediaItemInfo_Value(item, "D_POSITION", pos)
        RPR_SetMediaItemInfo_Value(item, "D_LENGTH", dur)
        take = RPR_GetMediaItemTake(item, 0)
        RPR_SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offset)
        return item

    def set_item_fades(self, item, fadein=None, fadeout=None, fadeintype=0, fadeouttype=0):
        if fadein:
            RPR_SetMediaItemInfo_Value(item, "D_FADEINLEN", fadein)
            RPR_SetMediaItemInfo_Value(item, "C_FADEINSHAPE", fadeintype)
        if fadeout:
            RPR_SetMediaItemInfo_Value(item, "D_FADEOUTLEN", fadeout)
            RPR_SetMediaItemInfo_Value(item, "C_FADEOUTSHAPE", fadeouttype)

    def set_item_volume(self, item, volume):
        RPR_SetMediaItemInfo_Value(item, "D_VOL", volume)

    def create_marker(self, pos, name="", colour=None):
        colour_code = 0
        if colour:
            colour_code = RPR_ColorToNative(colour["r"], colour["g"], colour["b"]) | 0x1000000
        RPR_AddProjectMarker2(0, False, pos, 0.0, name, 0, colour_code)

    def build_project(self, data):
        self.insertion_track = self.create_track("Insertion")

        for track_data in data["tracks"]:
            track = self.create_track(track_data["name"])

            if "volume" in track_data:
                self.set_track_volume(track, track_data["volume"])
            if "panning" in track_data:
                self.set_track_panning(track, track_data["panning"])
            if "volume_envelope" in track_data:
                self.set_track_volume_envelope(track, track_data["volume_envelope"])
            if "panning_envelope" in track_data:
                self.set_track_panning_envelope(track, track_data["panning_envelope"])

            if "items" not in track_data: continue
            for item_data in track_data["items"]:
                item = self.create_item(
                    track,
                    item_data["source"],
                    item_data["offset"],
                    item_data["position"],
                    item_data["duration"]
                )
                if "fadein" in item_data or "fadeout" in item_data:
                    self.set_item_fades(
                        item,
                        item_data.get("fadein", None),
                        item_data.get("fadeout", None),
                        item_data.get("fadeintype", 0),
                        item_data.get("fadeouttype", 0)
                    )
                if "volume" in item_data:
                    self.set_item_volume(item, item_data["volume"])

        for marker_data in data["markers"]:
            self.create_marker(marker_data["position"], marker_data.get("name", ""), marker_data.get("colour", None))

        RPR_DeleteTrack(self.insertion_track)
        self.insertion_track = None



class AAFInterface:

    def __init__(self):
        self.aaf = None
        self.encoder = ""
        self.aaf_directory = ""
        self.essence_data = {}

    def open(self, filename):
        try:
            self.aaf = aaf2.open(filename, "r")
        except Exception:
            log("Could not open AAF file.", ERROR)
            return False
        try:
            self.encoder = self.aaf.header["IdentificationList"][0]["ProductName"].value
        except Exception:
            log("Unable to find file encoder", WARNING)
        self.aaf_directory = os.path.abspath(os.path.dirname(filename))
        self.essence_data = {}
        return True

    def build_wav(self, fname, data, depth=16, rate=48000, channels=1):
        with wave.open(fname, "wb") as f:
            f.setnchannels(channels)
            f.setsampwidth(int(depth / 8))
            f.setframerate(rate)
            f.writeframesraw(data)
            f.close()

    def aafrational_value(self, rational):
        return rational.numerator / rational.denominator

    def get_point_list(self, varying, duration):
        data = []
        for point in varying["PointList"]:
            data.append({
                "time": point.time * duration,
                "value": point.value
            })
        return data

    def get_linked_essence(self, mob):
        try:
            url = mob.descriptor.locator.pop()["URLString"].value
            # file:///C%3a/Users/user/My%20video.mp4
            url = urllib.parse.urlparse(url)
            url = url.netloc + url.path
            # /C%3a/Users/user/My%20video.mp4
            url = urllib.parse.unquote(url)
            # /C:/Users/user/My video.mp4
            url = urllib.request.url2pathname(url)
            # C:\\Users\\user\\My video.mp4

            # If the AAF was built on another computer,
            # chances are the paths will differ.
            # Typically the source files are in the same directory as the AAF.
            if not os.path.isfile(url):
                local = os.path.join(self.aaf_directory, os.path.basename(url))
                if os.path.isfile(local):
                    url = local
            return url

        except Exception:
            log("Error retrieving file url for %s" % mob.name, WARNING)
            return ""

    def extract_embedded_essence(self, mob, filename):
        log("Extracting essence %s..." % filename)
        stream = mob.essence.open()
        data = stream.read()
        stream.close()

        meta = mob.descriptor
        data_fmt = meta["ContainerFormat"].value.name if "ContainerFormat" in meta else ""
        if data_fmt == "MXF":
            sample_depth = meta["QuantizationBits"].value
            sample_rate = meta["SampleRate"].value
            sample_rate = self.aafrational_value(sample_rate)
            self.build_wav(filename, data, sample_depth, sample_rate)
        else:
            with open(filename, "wb") as f:
                f.write(data)
                f.close()

        return filename

    def extract_essence(self, target, callback):
        for master_mob in self.aaf.content.mastermobs():
            self.essence_data[master_mob.name] = {}
            for slot in master_mob.slots:

                if isinstance(slot.segment, aaf2.components.Sequence):
                    source_mob = None
                    for component in slot.segment.components:
                        if isinstance(component, aaf2.components.SourceClip):
                            source_mob = component.mob
                            break
                    else:
                        self.essence_data[master_mob.name][slot.slot_id] = ""
                        log("Cannot find essence for %s slot %d" % (master_mob.name, slot.slot_id), WARNING)
                elif isinstance(slot.segment, aaf2.components.SourceClip):
                    source_mob = slot.segment.mob

                if slot.segment.media_kind == "Picture":
                    # Video files cannot be embedded in the AAF.
                    self.essence_data[master_mob.name][slot.slot_id] = self.get_linked_essence(source_mob)
                    continue
                if source_mob.essence:
                    filename = os.path.join(target, master_mob.name + slot.name + ".wav")
                    if callback:
                        callback("Extracting %s..." % (master_mob.name + slot.name + ".wav"))
                    self.essence_data[master_mob.name][slot.slot_id] = self.extract_embedded_essence(source_mob, filename)
                else:
                    self.essence_data[master_mob.name][slot.slot_id] = self.get_linked_essence(source_mob)

    def get_essence_file(self, mob_name, slot_id):
        try:
            return self.essence_data[mob_name][slot_id]
        except Exception:
            log("Cannot find essence for %s slot %d" % (mob_name, slot_id), WARNING)
            return ""

    def get_embedded_essence_count(self):
        count = 0
        for master_mob in self.aaf.content.mastermobs():
            for slot in master_mob.slots:
                if isinstance(slot.segment, aaf2.components.Sequence):
                    source_mob = None
                    for component in slot.segment.components:
                        if isinstance(component, aaf2.components.SourceClip):
                            source_mob = component.mob
                            break
                    else:
                        continue
                elif isinstance(slot.segment, aaf2.components.SourceClip):
                    source_mob = slot.segment.mob
                if slot.segment.media_kind == "Sound" and source_mob.essence:
                    count += 1
        return count


    # Instead of using per-item volume curves (aka take volume envelope),
    # we collect data from items and "render" it to the track volume envelope.
    def collect_vol_pan_automation(self, track):
        envelopes = {
            "volume_envelope": [],
            "panning_envelope": []
        }
        for envelope in envelopes:
            for item in track["items"]:
                if envelope in item:
                    for point in item[envelope]:
                        envelopes[envelope].append({
                            "time": item["position"] + point["time"],
                            "value": point["value"]
                        })
                    del item[envelope]
                else:
                    if not envelopes[envelope]: continue
                    # We don't want items without automation to be affected
                    # by automation added by other items
                    envelopes[envelope].append({
                        "time": item["position"],
                        "value": 1.0
                    })
                    envelopes[envelope].append({
                        "time": item["position"] + item["duration"],
                        "value": 1.0
                    })

        # Add only if not empty
        if envelopes["volume_envelope"]:
            track["volume_envelope"] = envelopes["volume_envelope"]
        if envelopes["panning_envelope"]:
            track["panning_envelope"] = envelopes["panning_envelope"]

        return track

    # Function is meant to be called recursively.
    # It is supposed to gather whatever information it can and pass it to
    # its caller, who will append the new data to its own.
    # The topmost caller sets "position" and "duration", as well as fades,
    def parse_operation_group(self, group, edit_rate):

        item = {}

        # We could base volume envelope extraction on either group.operation.name
        # or group.parameters[].name depending on which is more prone to be constant.
        # For now both conditions have to be met, which may cause some automation to
        # be ignored if other software picks different operation or parameter names.
        if group.operation.name in ["Mono Audio Gain", "Audio Gain"]:
            for p in group.parameters:
                if p.name not in ["Amplitude", "Amplitude multiplier", "Level"]: continue
                if isinstance(p, aaf2.misc.VaryingValue):
                    item["volume_envelope"] = self.get_point_list(p, group.length / edit_rate)
                elif isinstance(p, aaf2.misc.ConstantValue):
                    item["volume"] = self.aafrational_value(p.value)

        if group.operation.name == "Mono Audio Pan":
            for p in group.parameters:
                points = self.get_point_list(p, group.length / edit_rate)
                if p.name == "Pan value":
                    item["panning_envelope"] = [{
                        "time": point["time"],
                        "value": point["value"] * -2 + 1
                    } for point in points]

        if group.operation.name == "Audio Effect":
            for p in group.parameters:
                if p.name == "":
                    # Vegas/MC saves per-item volume and panning automation
                    # but I haven't figured out a way to find out which is which
                    # since the parameter name is blank.
                    pass
                if p.name == "SpeedRatio":
                    item["playbackrate"] = self.aafrational_value(p.value)

        segment = group.segments[0]

        # Aaaargh, why is this a thing?
        if isinstance(segment, aaf2.components.Sequence):
            segment = segment.components[0]

        if isinstance(segment, aaf2.components.OperationGroup):
            item.update(self.parse_operation_group(segment, edit_rate))
        elif isinstance(segment, aaf2.components.SourceClip):
            item.update({
                "source": self.get_essence_file(segment.mob.name, segment.slot_id),
                "offset": segment.start / edit_rate,
            })

        return item

    def parse_sequence(self, sequence, edit_rate):
        items = []
        time = 0.0
        fade = 0  # 0 = no fade, 1 = fade, -1 = last component was filler
        fade_length = None
        fade_type = 0  # 0 = linear, 1 = power

        for component in sequence.components:
            try:
                duration = component.length / edit_rate

                if isinstance(component, aaf2.components.SourceClip):
                    item = {
                        "source": self.get_essence_file(component.mob.name, component.slot_id),
                        "offset": component.start / edit_rate,
                        "position": time,
                        "duration": duration,
                    }
                    if fade == 1:
                        item["fadein"] = fade_length
                        item["fadeintype"] = fade_type
                    fade = 0
                    items.append(item)
                    time += duration

                elif isinstance(component, aaf2.components.OperationGroup):
                    item = {
                        "position": time,
                        "duration": duration
                    }
                    item.update(self.parse_operation_group(component, edit_rate))
                    if fade == 1:
                        item["fadein"] = fade_length
                        item["fadeintype"] = fade_type
                    fade = 0

                    if "source" not in item:
                        log("Failed to find item source at %f seconds." % time, WARNING)
                        item["source"] = ""
                    if "offset" not in item:
                        log("Failed to find item offset at %f seconds." % time, WARNING)
                        item["offset"] = 0

                    items.append(item)
                    time += duration

                elif isinstance(component, aaf2.components.Transition):
                    fade_length = duration
                    fade_type = 0
                    try:
                        if component["OperationGroup"].value.parameters.value[0].interpolation.name == "PowerInterp":
                            fade_type = 1
                    except Exception:
                        pass
                    if fade == 0:
                        items[-1]["fadeout"] = fade_length
                        items[-1]["fadeouttype"] = fade_type
                    if fade != 1:
                        fade = 1
                    time -= duration

                elif isinstance(component, aaf2.components.Filler):
                    fade = -1
                    time += duration

            except Exception:
                log("Failed to parse component at %f seconds." % time)

        return items

    def get_picture_tracks(self, slot):
        data = []
        edit_rate = self.aafrational_value(slot.edit_rate)

        if isinstance(slot.segment, aaf2.components.NestedScope):
            for sequence in slot.segment.slots.value:
                seq_data = self.parse_sequence(sequence, edit_rate)
                if seq_data:
                    data.append({
                        "name": "",
                        "items": seq_data
                    })
        elif isinstance(slot.segment, aaf2.components.Sequence):
            seq_data = self.parse_sequence(slot.segment, edit_rate)
            if seq_data:
                data.append({
                    "name": slot.name,
                    "items": seq_data
                })

        return data

    def get_sound_track(self, slot):
        data = {
            "name": slot.name
        }
        edit_rate = self.aafrational_value(slot.edit_rate)
        segment = slot.segment
        if isinstance(segment, aaf2.components.OperationGroup):
            # Maybe we should check for segment.operation.name as well?
            for p in segment.parameters:
                if p.name == "Pan value":
                    data["panning"] = self.aafrational_value(p.value) * 2 - 1
                if p.name in ["Pan", "Pan Level"]:
                    # Sometimes segment.length is wrong so we have to use
                    # the length of the data segment instead.
                    real_length = segment.length / edit_rate
                    if self.encoder == "DaVinci Resolve":
                        real_length = segment.segments[0].length / edit_rate
                    points = self.get_point_list(p, real_length)
                    data["panning_envelope"] = [{
                        "time": point["time"],
                        "value": point["value"] * -2 + 1
                        # Reaper can't make up its mind 
                    } for point in points]
            data["items"] = self.parse_sequence(segment.segments[0], edit_rate)
        elif isinstance(segment, aaf2.components.Sequence):
            data["items"] = self.parse_sequence(segment, edit_rate)
        return data

    def get_markers(self, slot):
        markers = []
        edit_rate = self.aafrational_value(slot.edit_rate)
        for component in slot.segment.components:
            marker = {
                "name": component["Comment"].value,
                "position": component["Position"].value / edit_rate
            }
            if "CommentMarkerColour" in component:
                col = component["CommentMarkerColour"].value
                marker["colour"] = {
                    "r": int(col["red"] / 256),
                    "g": int(col["green"] / 256),
                    "b": int(col["blue"] / 256)
                }
            markers.append(marker)
        return markers

    def get_composition_list(self):
        return [composition.name for composition in self.aaf.content.compositionmobs()]

    def get_composition(self, composition):
        data = {
            "tracks": [],
            "markers": []
        }

        for slot in list(self.aaf.content.compositionmobs())[composition].slots:
            try:
                if slot.media_kind == "Picture":
                    picture_tracks = self.get_picture_tracks(slot)
                    if picture_tracks:
                        data["tracks"] += picture_tracks
                elif slot.media_kind in ["Sound", "LegacySound"]:
                    track_data = self.get_sound_track(slot)
                    track_data = self.collect_vol_pan_automation(track_data)
                    data["tracks"].append(track_data)
                elif slot.media_kind == "DescriptiveMetadata":
                    data["markers"] += self.get_markers(slot)
            except Exception:
                log("Failed parsing slot %s" % slot.name, WARNING)
        return data

    def get_aaf_metadata(self):
        try:
            identity = self.aaf.header["IdentificationList"][0]
            return {
                "company": identity["CompanyName"].value,
                "product": identity["ProductName"].value,
                "version": identity["ProductVersionString"].value,
                "date": identity["Date"].value,
                "platform": identity["Platform"].value
            }
        except Exception:
            warn("Could not get file identity metadata.", WARNING)
            return {}



class UserInteraction:

    @staticmethod
    def show_progressbar(item_count, action):

        def update_call(message):
            if len(message) > 50:
                message = message[:48] + "..."
            try:
                label.config(text=message)
                progressbar.step()
                progressbar.update()
            except Exception:
                # User closed the window, probably
                pass

        window = tkinter.Tk()
        window.title("Importing...")
        window.columnconfigure(0, weight=1)

        frame = tkinter.Frame(window, borderwidth=10)
        frame.grid(column=0, row=0, sticky="NWSE")
        frame.columnconfigure(0, weight=1)

        label = tkinter.Label(frame, text="")
        label.grid(column=0, row=0, sticky="NW")

        progressbar = tkinter.ttk.Progressbar(frame, mode="determinate", maximum=item_count+1, length=500)
        progressbar.grid(column=0, row=1, sticky="WE")

        action(update_call)
        try:
            window.destroy()
            window.mainloop()
        except Exception:
            pass

    @staticmethod
    def get_composition(composition_list):
        if have_reaper:
            if have_tk:
                return UserInteraction.get_composition_gui(composition_list)
            else:
                return UserInteraction.get_composition_awkward(composition_list)
        else:
            return UserInteraction.get_composition_cli(composition_list)

    @staticmethod
    def get_composition_cli(composition_list):
        print("Select composition to parse:")
        for i, t in enumerate(composition_list):
            print("%d. %s" % (i, t))
        while True:
            try:
                composition_id = int(input("> "))
                composition_list[composition_id]
                break
            except Exception:
                print("Invalid input.")
        return composition_id

    @staticmethod
    def get_composition_gui(composition_list):

        selection = 0

        def ok_callback():
            nonlocal selection
            selection = listbox.curselection()[0]
            window.destroy()

        def doubleclick_callback(e):
            ok_callback()

        window = tkinter.Tk()
        window.title("Select composition")
        window.rowconfigure(0, weight=1)
        window.columnconfigure(0, weight=1)

        frame = tkinter.Frame(window, borderwidth=10)
        frame.grid(column=0, row=0, sticky="NWSE")
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(1, weight=1)

        label_text = "AAF contains multiple compositions. Select which one to import:"
        label = tkinter.Label(frame, text=label_text)
        label.grid(column=0, row=0, sticky="NW")

        listbox = tkinter.Listbox(frame)
        for comp in composition_list:
            listbox.insert("end", comp)
        listbox.selection_set(0)
        listbox.see(0)
        listbox.activate(0)
        listbox.bind('<Double-1>', doubleclick_callback)
        listbox.grid(column=0, row=1, sticky="NWSE", pady=10)

        button = tkinter.Button(frame, text="OK", command=ok_callback)
        button.grid(column=0, row=2, sticky="S")

        window.mainloop()
        return selection

    @staticmethod
    def get_composition_awkward(composition_list):
        while True:
            for i, comp in enumerate(composition_list):
                message = "Do you want to import composition %s?" % comp
                result = RPR_MB(message, "Select composition", 4)
                if result == 6:
                    return i

def import_aaf():
    global log_level

    aaf_interface = AAFInterface()
    reaper_interface = ReaperInterface()

    if have_reaper:
        filename = reaper_interface.select_aaf()
        if filename is None: return
        target = reaper_interface.get_project_directory()
    else:
        if len(sys.argv) < 2:
            log("No input file provided.", ERROR)
            return
        filename = sys.argv[1]
        target = "sources"
        if not os.path.exists(target):
            os.mkdir(target)
        log_level = NOTICE

    if not aaf_interface.open(filename): return
    log("geting data from %s..." % filename)
    meta = aaf_interface.get_aaf_metadata()
    log("AAF created on %s with %s %s version %s using %s" % 
        (str(meta["date"]), meta["company"], meta["product"], meta["version"], meta["platform"])
    )

    if have_tk:
        def action(update):
            aaf_interface.extract_essence(target, update)
        count = aaf_interface.get_embedded_essence_count()
        UserInteraction.show_progressbar(count, action)
    else:
        aaf_interface.extract_essence(target, None)

    composition_list = aaf_interface.get_composition_list()
    composition_id = 0
    if len(composition_list) > 1:
        composition_id = UserInteraction.get_composition(composition_list)
    composition = aaf_interface.get_composition(composition_id)

    if have_reaper:
        reaper_interface.build_project(composition)
    else:
        print(json.dumps(composition))

if __name__ == "__main__":
    # sys.exit() or exit() would crash the script, so instead
    # we're using `return` within a main function
    import_aaf()
[/QUOT

i have the same problem (the script seems to have been set up correctly but when i run it and select the aaf file it does nothing), i tried this fix and im using mac os 12.6 and python 3.11 and reascript outputs this error when using the new code:

Script execution error

Traceback (most recent call last):
File "importaaf.py", line 717, in <module>
import_aaf()
File "importaaf.py", line 701, in import_aaf
aaf_interface.extract_essence(target, None)
File "importaaf.py", line 252, in extract_essence
filename = os.path.join(target, master_mob.name + slot.name + ".wav")
~~~~~~~~~~~~~~~~^~~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str
Kostas Stylianou is offline   Reply With Quote
Old 12-05-2022, 08:01 AM   #76
charlienyc
Human being with feelings
 
charlienyc's Avatar
 
Join Date: Oct 2010
Location: chicago
Posts: 17
Default PC probs.

Hi,
This is a great concept - thanks! I am having trouble getting it all set up, however. I installed Python, pip, pyaaf2, etc., but Reaper shows "No compatible version of Python was found" I installed the latest 64 bit version (3.11) and am on Reaper v6.59. Any ideas?
Cheers
charlienyc is offline   Reply With Quote
Old 12-22-2022, 10:22 AM   #77
kris.audioplanet
Human being with feelings
 
Join Date: Feb 2019
Location: Poland
Posts: 137
Default

I have no idea how to do this bit:

You can install pyaaf2 via:
pip install pyaaf2

Where should I run this command?
kris.audioplanet is offline   Reply With Quote
Old 12-24-2022, 01:38 PM   #78
mtierney
Human being with feelings
 
Join Date: Nov 2022
Posts: 103
Default

I got it working on Mac OS 12.5 thanks to the updated code from gapalil001! Thanks everybody!
__________________
Reaper (latest)
MacOS Monterey 12.6.2
Macbook Pro 2021, M1 Max, 64GB RAM
mtierney is offline   Reply With Quote
Old 01-04-2023, 01:55 AM   #79
kris.audioplanet
Human being with feelings
 
Join Date: Feb 2019
Location: Poland
Posts: 137
Default

I got all this installed, but Reaper still says there is no python installed.
Attached Images
File Type: png Screenshot 2023-01-04 at 09.54.46.png (18.1 KB, 78 views)
kris.audioplanet is offline   Reply With Quote
Old 01-04-2023, 02:01 AM   #80
vitalker
Human being with feelings
 
vitalker's Avatar
 
Join Date: Dec 2012
Posts: 13,333
Default

Quote:
Originally Posted by kris.audioplanet View Post
I got all this installed, but Reaper still says there is no python installed.
Isn't Python preinstalled on macOS? Are you sure it is official Python? Download from here: https://www.python.org/downloads/macos/
vitalker is online now   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 05:27 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.