bl_info = { "name": "VSE Volume Ducking (Active Strip)", "author": "Joel Eldo", "version": (1, 1), "blender": (3, 0, 0), "location": "Video Sequence Editor > Sidebar > Channel Volume Tab", "description": "Duck the active audio strip volume based on overlap with strips in a source channel", "category": "Sequencer", } import bpy # -------------------- # Properties # -------------------- class VSEVolumeDuckingProperties(bpy.types.PropertyGroup): source_channel: bpy.props.IntProperty( name="Source Channel", description="Channel to use as mask (e.g., dialogue)", default=2, min=1 ) fade_duration: bpy.props.IntProperty( name="Fade Duration", description="Fade in/out time in seconds", default=3, min=1 ) normal_volume: bpy.props.FloatProperty( name="Normal Volume", description="Volume when not ducked", default=0.5, min=0.0, max=1.0 ) faded_volume: bpy.props.FloatProperty( name="Faded Volume", description="Volume when ducked", default=0.25, min=0.0, max=1.0 ) # -------------------- # Operator # -------------------- class VSEDUCKING_OT_Execute(bpy.types.Operator): bl_idname = "vseducking.execute" bl_label = "Execute Ducking" bl_description = "Apply ducking to active strip using overlap with strips in source channel" def execute(self, context): scene = context.scene props = scene.vse_volume_ducking_props seq = scene.sequence_editor if not seq or not seq.active_strip: self.report({'ERROR'}, "No active strip selected.") return {'CANCELLED'} if props.source_channel == seq.active_strip.channel: self.report({'ERROR'}, "Active strip cannot be in the target channel.") return {'CANCELLED'} target = seq.active_strip # Active strip is the one to duck try: target.volume except: self.report({'ERROR'}, "Active strip does not contain audio.") return {'CANCELLED'} fps = scene.render.fps delta = int(props.fade_duration) # Get all strips all_strips = list(seq.sequences_all) # Find source strips in the given channel source_strips = [s for s in all_strips if s.channel == props.source_channel and s != target] # Sort source strips by frame_start source_strips.sort(key=lambda s: s.frame_start) # Collect ducking intervals duck_intervals = [] for strip in source_strips: fade_out_frame = strip.frame_start + strip.frame_offset_start fade_in_frame = strip.frame_final_end # Check if it overlaps the target strip at all if fade_out_frame > target.frame_final_end or fade_in_frame < target.frame_start: continue # no overlap duck_intervals.append((fade_out_frame, fade_in_frame)) # Merge overlapping duck intervals merged_intervals = merge_intervals(duck_intervals) # Clear existing volume keyframes #target.animation_data_clear() # Insert ducking keyframes for start, end in merged_intervals: target.volume = props.normal_volume target.keyframe_insert(data_path="volume", frame=start - delta) target.volume = props.faded_volume target.keyframe_insert(data_path="volume", frame=start) target.volume = props.faded_volume target.keyframe_insert(data_path="volume", frame=end) target.volume = props.normal_volume target.keyframe_insert(data_path="volume", frame=end + delta) self.report({'INFO'}, "Ducking applied to active strip.") return {'FINISHED'} # -------------------- # Interval Merging # -------------------- def merge_intervals(intervals): if not intervals: return [] intervals.sort() merged = [intervals[0]] for current in intervals[1:]: prev_start, prev_end = merged[-1] cur_start, cur_end = current if cur_start <= prev_end: merged[-1] = (prev_start, max(prev_end, cur_end)) else: merged.append(current) return merged # -------------------- # UI Panel # -------------------- class VSEDUCKING_PT_Panel(bpy.types.Panel): bl_label = "Volume Ducking" bl_idname = "VSEDUCKING_PT_panel" bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'UI' bl_category = "Channel Volume" def draw(self, context): layout = self.layout props = context.scene.vse_volume_ducking_props layout.label(text="Source strips (e.g. dialogue)") layout.prop(props, "source_channel") layout.separator() layout.prop(props, "fade_duration") layout.prop(props, "normal_volume") layout.prop(props, "faded_volume") layout.operator("vseducking.execute", text="Execute Ducking", icon='SOUND') # -------------------- # Register # -------------------- classes = ( VSEVolumeDuckingProperties, VSEDUCKING_OT_Execute, VSEDUCKING_PT_Panel, ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.vse_volume_ducking_props = bpy.props.PointerProperty(type=VSEVolumeDuckingProperties) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.vse_volume_ducking_props if __name__ == "__main__": register()