|
|
|
05-03-2022, 09:54 AM
|
#41
|
Human being with feelings
Join Date: Jul 2019
Posts: 43
|
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 ?
|
|
|
05-03-2022, 10:02 AM
|
#42
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by dsyrock
|
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
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.
|
|
|
05-04-2022, 10:38 AM
|
#43
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
Quote:
Originally Posted by gapalil001
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.
|
|
|
05-04-2022, 11:03 AM
|
#44
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by mattsoule
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
|
|
|
05-04-2022, 11:42 AM
|
#45
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
@gapalil001
We have a winner on MAC OSX 11.6.5!!
Well done. And well done to OP for this great script.
|
|
|
05-04-2022, 11:47 AM
|
#46
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by mattsoule
@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
|
|
|
05-04-2022, 04:33 PM
|
#47
|
Human being with feelings
Join Date: Apr 2022
Posts: 1
|
Quote:
Originally Posted by pterodox
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.
|
|
|
05-05-2022, 12:47 AM
|
#48
|
Human being with feelings
Join Date: Jun 2018
Location: Paris
Posts: 22
|
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
|
|
|
05-05-2022, 01:39 PM
|
#49
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
Quote:
Originally Posted by cejotabass
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
|
|
|
05-07-2022, 07:19 AM
|
#50
|
Human being with feelings
Join Date: Jun 2018
Location: Paris
Posts: 22
|
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
|
|
|
05-09-2022, 10:03 AM
|
#51
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by oudi le oudi
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.
|
|
|
05-13-2022, 03:57 AM
|
#52
|
Human being with feelings
Join Date: Jun 2018
Location: Paris
Posts: 22
|
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 .
|
|
|
05-13-2022, 08:43 AM
|
#53
|
Human being with feelings
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
|
Hmm, what do I do, when I could open an AAF but nothing happens? Is there a log file somehwere?
|
|
|
05-13-2022, 09:16 AM
|
#54
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by _Stevie_
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!
|
|
|
05-13-2022, 12:37 PM
|
#55
|
Human being with feelings
Join Date: Oct 2017
Location: Black Forest
Posts: 5,054
|
Quote:
Originally Posted by gapalil001
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
P.S. thank you a lot for supporting us!
|
You are more than welcome! <3
|
|
|
05-13-2022, 12:52 PM
|
#56
|
Human being with feelings
Join Date: May 2016
Location: Kyiv, Ukraine
Posts: 544
|
Quote:
Originally Posted by _Stevie_
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
|
|
|
05-14-2022, 03:26 AM
|
#57
|
Human being with feelings
Join Date: Dec 2019
Location: Cave (RM)
Posts: 1
|
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!!
|
|
|
05-27-2022, 09:57 AM
|
#58
|
Human being with feelings
Join Date: Aug 2021
Posts: 2
|
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
|
|
|
06-01-2022, 01:40 PM
|
#59
|
Human being with feelings
Join Date: May 2022
Posts: 1
|
Reaper 6.58 doesn't recognise a python 3
Quote:
Originally Posted by pterodox
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!
|
|
|
07-14-2022, 10:26 AM
|
#60
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
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!
|
|
|
07-14-2022, 11:02 AM
|
#61
|
Human being with feelings
Join Date: Feb 2016
Location: Hollyweird
Posts: 2,630
|
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.
|
|
|
07-15-2022, 08:24 AM
|
#62
|
Human being with feelings
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
|
Quote:
Originally Posted by mattsoule
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
|
|
|
07-15-2022, 10:09 AM
|
#63
|
Human being with feelings
Join Date: Mar 2021
Location: United Kingdom
Posts: 6
|
Quote:
Originally Posted by gapalil001
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.
|
|
|
07-27-2022, 04:35 PM
|
#64
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
Quote:
Originally Posted by Runaway
|
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.
|
|
|
08-02-2022, 02:26 AM
|
#65
|
Human being with feelings
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
|
Quote:
Originally Posted by mattsoule
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
|
|
|
08-02-2022, 12:19 PM
|
#66
|
Human being with feelings
Join Date: Feb 2022
Location: Los Angeles
Posts: 14
|
Quote:
Originally Posted by Runaway
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!
|
|
|
08-03-2022, 12:48 AM
|
#67
|
Human being with feelings
Join Date: Jun 2009
Location: Sydney, Australia
Posts: 2,510
|
Quote:
Originally Posted by mattsoule
Really great. Congrats!
|
Appreciate the feedback
|
|
|
09-02-2022, 06:32 AM
|
#68
|
Human being with feelings
Join Date: Jan 2013
Location: Montreal, Canada
Posts: 258
|
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**
|
|
|
09-03-2022, 01:17 PM
|
#69
|
Human being with feelings
Join Date: Jan 2021
Posts: 2
|
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
|
|
|
09-20-2022, 10:17 AM
|
#70
|
Human being with feelings
Join Date: Jun 2022
Posts: 1
|
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'
|
|
|
09-27-2022, 04:16 PM
|
#71
|
Human being with feelings
Join Date: Jun 2012
Posts: 277
|
Quote:
Originally Posted by buyrunvelocity
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
|
|
|
10-14-2022, 11:28 AM
|
#72
|
Human being with feelings
Join Date: Oct 2022
Posts: 1
|
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.
|
|
|
10-17-2022, 04:50 PM
|
#73
|
Human being with feelings
Join Date: Mar 2018
Posts: 1
|
Im having the same issues as the user above. At all edit points, the WAV file loops.
|
|
|
10-18-2022, 04:11 AM
|
#74
|
Human being with feelings
Join Date: Mar 2021
Posts: 6
|
also having the same issues
M
|
|
|
11-16-2022, 04:12 PM
|
#75
|
Human being with feelings
Join Date: Jun 2021
Posts: 2
|
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
|
|
|
12-05-2022, 08:01 AM
|
#76
|
Human being with feelings
Join Date: Oct 2010
Location: chicago
Posts: 17
|
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
|
|
|
12-22-2022, 10:22 AM
|
#77
|
Human being with feelings
Join Date: Feb 2019
Location: Poland
Posts: 137
|
I have no idea how to do this bit:
You can install pyaaf2 via:
pip install pyaaf2
Where should I run this command?
|
|
|
12-24-2022, 01:38 PM
|
#78
|
Human being with feelings
Join Date: Nov 2022
Posts: 103
|
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
|
|
|
01-04-2023, 01:55 AM
|
#79
|
Human being with feelings
Join Date: Feb 2019
Location: Poland
Posts: 137
|
I got all this installed, but Reaper still says there is no python installed.
|
|
|
01-04-2023, 02:01 AM
|
#80
|
Human being with feelings
Join Date: Dec 2012
Posts: 13,333
|
Quote:
Originally Posted by kris.audioplanet
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/
|
|
|
Thread Tools |
|
Display Modes |
Linear Mode
|
Posting Rules
|
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts
HTML code is Off
|
|
|
All times are GMT -7. The time now is 05:27 AM.
|