# SPDX-FileCopyrightText: 2010-2023 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later import bpy from bpy.types import ( FileHandler, Operator, ) from bpy.props import ( EnumProperty, FloatProperty, IntProperty, ) from bpy.app.translations import pgettext_rpt as rpt_ def _animated_properties_get(strip): animated_properties = [] if hasattr(strip, "volume"): animated_properties.append("volume") if hasattr(strip, "blend_alpha"): animated_properties.append("blend_alpha") return animated_properties class SequencerCrossfadeSounds(Operator): """Do cross-fading volume animation of two selected sound strips""" bl_idname = "sequencer.crossfade_sounds" bl_label = "Crossfade Sounds" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): strip = context.active_strip return strip and (strip.type == 'SOUND') def execute(self, context): scene = context.scene strip1 = None strip2 = None for strip in scene.sequence_editor.strips_all: if strip.select and strip.type == 'SOUND': if strip1 is None: strip1 = strip elif strip2 is None: strip2 = strip else: strip2 = None break if strip2 is None: self.report({'ERROR'}, "Select 2 sound strips") return {'CANCELLED'} if strip1.frame_final_start > strip2.frame_final_start: strip1, strip2 = strip2, strip1 if strip1.frame_final_end > strip2.frame_final_start: tempcfra = scene.frame_current scene.frame_current = strip2.frame_final_start strip1.keyframe_insert("volume") scene.frame_current = strip1.frame_final_end strip1.volume = 0 strip1.keyframe_insert("volume") strip2.keyframe_insert("volume") scene.frame_current = strip2.frame_final_start strip2.volume = 0 strip2.keyframe_insert("volume") scene.frame_current = tempcfra return {'FINISHED'} self.report({'ERROR'}, "The selected strips don't overlap") return {'CANCELLED'} class SequencerSplitMulticam(Operator): """Split multicam strip and select camera""" bl_idname = "sequencer.split_multicam" bl_label = "Split Multicam" bl_options = {'REGISTER', 'UNDO'} camera: IntProperty( name="Camera", min=1, max=32, soft_min=1, soft_max=32, default=1, ) @classmethod def poll(cls, context): strip = context.active_strip return strip and (strip.type == 'MULTICAM') def execute(self, context): scene = context.scene camera = self.camera strip = context.active_strip if strip.multicam_source == camera or camera >= strip.channel: return {'FINISHED'} cfra = scene.frame_current right_strip = strip.split(frame=cfra, split_method='SOFT') if right_strip: strip.select = False right_strip.select = True scene.sequence_editor.active_strip = right_strip context.active_strip.multicam_source = camera return {'FINISHED'} class SequencerDeinterlaceSelectedMovies(Operator): """Deinterlace all selected movie sources""" bl_idname = "sequencer.deinterlace_selected_movies" bl_label = "Deinterlace Movies" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): scene = context.scene return (scene and scene.sequence_editor) def execute(self, context): for strip in context.scene.sequence_editor.strips_all: if strip.select and strip.type == 'MOVIE': strip.use_deinterlace = True return {'FINISHED'} class SequencerFadesClear(Operator): """Removes fade animation from selected strips""" bl_idname = "sequencer.fades_clear" bl_label = "Clear Fades" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): strip = context.active_strip return strip is not None def execute(self, context): scene = context.scene animation_data = scene.animation_data if animation_data is None: return {'CANCELLED'} action = animation_data.action if action is None: return {'CANCELLED'} fcurves = action.fcurves fcurve_map = { curve.data_path: curve for curve in fcurves if curve.data_path.startswith("sequence_editor.strips_all") } for strip in context.selected_strips: for animated_property in _animated_properties_get(strip): data_path = strip.path_from_id() + "." + animated_property curve = fcurve_map.get(data_path) if curve: fcurves.remove(curve) setattr(strip, animated_property, 1.0) strip.invalidate_cache('COMPOSITE') return {'FINISHED'} class SequencerFadesAdd(Operator): """Adds or updates a fade animation for either visual or audio strips""" bl_idname = "sequencer.fades_add" bl_label = "Add Fades" bl_options = {'REGISTER', 'UNDO'} duration_seconds: FloatProperty( name="Fade Duration", description="Duration of the fade in seconds", default=1.0, min=0.01, ) type: EnumProperty( items=( ('IN_OUT', "Fade In and Out", "Fade selected strips in and out"), ('IN', "Fade In", "Fade in selected strips"), ('OUT', "Fade Out", "Fade out selected strips"), ('CURSOR_FROM', "From Current Frame", "Fade from the time cursor to the end of overlapping strips"), ('CURSOR_TO', "To Current Frame", "Fade from the start of strips under the time cursor to the current frame"), ), name="Fade Type", description="Fade in, out, both in and out, to, or from the current frame. Default is both in and out", default='IN_OUT', ) @classmethod def poll(cls, context): # Can't use context.selected_strips as it can have an impact on performances strip = context.active_strip return strip is not None def execute(self, context): from math import floor # We must create a scene action first if there's none scene = context.scene if not scene.animation_data: scene.animation_data_create() if not scene.animation_data.action: action = bpy.data.actions.new(scene.name + "Action") scene.animation_data.action = action strips = context.selected_strips if not strips: self.report({'ERROR'}, "No strips selected") return {'CANCELLED'} if self.type in {'CURSOR_TO', 'CURSOR_FROM'}: strips = [ strip for strip in strips if strip.frame_final_start < scene.frame_current < strip.frame_final_end ] if not strips: self.report({'ERROR'}, "Current frame not within strip framerange") return {'CANCELLED'} max_duration = min(strips, key=lambda strip: strip.frame_final_duration).frame_final_duration max_duration = floor(max_duration / 2.0) if self.type == 'IN_OUT' else max_duration faded_strips = [] for strip in strips: duration = self.calculate_fade_duration(context, strip) duration = min(duration, max_duration) if not self.is_long_enough(strip, duration): continue for animated_property in _animated_properties_get(strip): fade_fcurve = self.fade_find_or_create_fcurve(context, strip, animated_property) fades = self.calculate_fades(strip, fade_fcurve, animated_property, duration) self.fade_animation_clear(fade_fcurve, fades) self.fade_animation_create(fade_fcurve, fades) faded_strips.append(strip) strip.invalidate_cache('COMPOSITE') strip_string = "strip" if len(faded_strips) == 1 else "strips" self.report({'INFO'}, rpt_("Added fade animation to {:d} {:s}").format(len(faded_strips), strip_string)) return {'FINISHED'} def calculate_fade_duration(self, context, strip): scene = context.scene frame_current = scene.frame_current duration = 0.0 if self.type == 'CURSOR_TO': duration = abs(frame_current - strip.frame_final_start) elif self.type == 'CURSOR_FROM': duration = abs(strip.frame_final_end - frame_current) else: duration = calculate_duration_frames(scene, self.duration_seconds) return max(1, duration) def is_long_enough(self, strip, duration=0.0): minimum_duration = duration * 2 if self.type == 'IN_OUT' else duration return strip.frame_final_duration >= minimum_duration def calculate_fades(self, strip, fade_fcurve, animated_property, duration): """ Returns a list of Fade objects """ fades = [] if self.type in {'IN', 'IN_OUT', 'CURSOR_TO'}: fade = Fade(strip, fade_fcurve, 'IN', animated_property, duration) fades.append(fade) if self.type in {'OUT', 'IN_OUT', 'CURSOR_FROM'}: fade = Fade(strip, fade_fcurve, 'OUT', animated_property, duration) fades.append(fade) return fades def fade_find_or_create_fcurve(self, context, strip, animated_property): """ Iterates over all the fcurves until it finds an fcurve with a data path that corresponds to the strip. Returns the matching FCurve or creates a new one if the function can't find a match. """ scene = context.scene action = scene.animation_data.action searched_data_path = strip.path_from_id(animated_property) return action.fcurve_ensure_for_datablock(scene, searched_data_path) def fade_animation_clear(self, fade_fcurve, fades): """ Removes existing keyframes in the fades' time range, in fast mode, without updating the fcurve """ keyframe_points = fade_fcurve.keyframe_points for fade in fades: for keyframe in keyframe_points: # The keyframe points list doesn't seem to always update as the # operator re-runs Leading to trying to remove nonexistent keyframes try: if fade.start.x < keyframe.co[0] <= fade.end.x: keyframe_points.remove(keyframe, fast=True) except Exception: pass fade_fcurve.update() def fade_animation_create(self, fade_fcurve, fades): """ Inserts keyframes in the fade_fcurve in fast mode using the Fade objects. Updates the fcurve after having inserted all keyframes to finish the animation. """ keyframe_points = fade_fcurve.keyframe_points for fade in fades: for point in (fade.start, fade.end): keyframe_points.insert(frame=point.x, value=point.y, options={'FAST'}) fade_fcurve.update() # The graph editor and the audio wave-forms only redraw upon "moving" a keyframe. keyframe_points[-1].co = keyframe_points[-1].co class Fade: # Data structure to represent fades. __slots__ = ( "type", "animated_property", "duration", "max_value", "start", "end", ) def __init__(self, strip, fade_fcurve, ty, animated_property, duration): from mathutils import Vector self.type = ty self.animated_property = animated_property self.duration = duration self.max_value = self.calculate_max_value(strip, fade_fcurve) if ty == 'IN': self.start = Vector((strip.frame_final_start, 0.0)) self.end = Vector((strip.frame_final_start + self.duration, self.max_value)) elif ty == 'OUT': self.start = Vector((strip.frame_final_end - self.duration, self.max_value)) self.end = Vector((strip.frame_final_end, 0.0)) def calculate_max_value(self, strip, fade_fcurve): """ Returns the maximum Y coordinate the fade animation should use for a given strip Uses either the strip's value for the animated property, or the next keyframe after the fade """ max_value = 0.0 if not fade_fcurve.keyframe_points: max_value = getattr(strip, self.animated_property, 1.0) else: if self.type == 'IN': fade_end = strip.frame_final_start + self.duration keyframes = (k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end) if self.type == 'OUT': fade_start = strip.frame_final_end - self.duration keyframes = (k for k in reversed(fade_fcurve.keyframe_points) if k.co[0] <= fade_start) try: max_value = next(keyframes).co[1] except StopIteration: pass return max_value if max_value > 0.0 else 1.0 def __repr__(self): return "Fade {!r}: {!r} to {!r}".format(self.type, self.start, self.end) def calculate_duration_frames(scene, duration_seconds): return round(duration_seconds * scene.render.fps / scene.render.fps_base) class SequencerFileHandlerBase: @classmethod def poll_drop(cls, context): return ( (context.region is not None) and (context.region.type == 'WINDOW') and (context.area is not None) and (context.area.ui_type == 'SEQUENCE_EDITOR') ) class SEQUENCER_FH_image_strip(FileHandler, SequencerFileHandlerBase): bl_idname = "SEQUENCER_FH_image_strip" bl_label = "Image strip" bl_import_operator = "SEQUENCER_OT_image_strip_add" bl_file_extensions = ";".join(bpy.path.extensions_image) class SEQUENCER_FH_movie_strip(FileHandler, SequencerFileHandlerBase): bl_idname = "SEQUENCER_FH_movie_strip" bl_label = "Movie strip" bl_import_operator = "SEQUENCER_OT_movie_strip_add" bl_file_extensions = ";".join(bpy.path.extensions_movie) class SEQUENCER_FH_sound_strip(FileHandler, SequencerFileHandlerBase): bl_idname = "SEQUENCER_FH_sound_strip" bl_label = "Sound strip" bl_import_operator = "SEQUENCER_OT_sound_strip_add" bl_file_extensions = ";".join(bpy.path.extensions_audio) classes = ( SequencerCrossfadeSounds, SequencerSplitMulticam, SequencerDeinterlaceSelectedMovies, SequencerFadesClear, SequencerFadesAdd, SEQUENCER_FH_image_strip, SEQUENCER_FH_movie_strip, SEQUENCER_FH_sound_strip, )